Skip to main content
SvelteKit is a full-stack web framework built on top of Svelte, offering a flexible approach to routing, data loading, and rendering. SvelteKit integrates with Vite for fast development and provides built-in tools for server endpoints, form actions, and progressive enhancement. Its compile-time approach reduces client-side JavaScript, resulting in simple and efficient applications that do not rely heavily on runtime frameworks. SvelteKit can only be used on Application Hosting; it cannot be deployed as a static site. We recommend the following best practices to get the best performance when using SvelteKit on Sevalla:
  • Use SvelteKit load functions for efficient server-side data fetching.
  • Add appropriate Cache-Control headers to pages and server route responses.
  • Enable SvelteKit’s prefetching and preloading to enhance client navigation speed.
  • Implement APIs using server routes (+server.ts) for better control and performance.
  • Ensure compression is enabled in your server configuration.
  • Use streaming with async load functions to deliver content progressively and reduce TTFB.

Configuration

To configure your SvelteKit application for Sevalla, you must ensure @sveltejs/adapter-node is properly configured in svelte.config.js . The following is an example svelte.config.js file for deploying SvelteKit on Sevalla:
// svelte.config.js
import adapter from "@sveltejs/adapter-node";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter({
      out: "build",
      precompress: false,
      envPrefix: "",
    }),
  },
};

export default config;

Containerization

Dockerfile

The build for Dockerfiles is fully customizable. The following is an example Dockerfile for SvelteKit:
# Dockerfile for SvelteKit on Sevalla

FROM node:lts-alpine AS deps
WORKDIR /app

COPY package*.json ./
RUN npm ci

FROM node:lts-alpine AS builder
WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY . .

RUN npm run build

FROM node:lts-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV PORT=3000
ENV HOST=0.0.0.0

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 svelteuser

COPY --from=builder --chown=svelteuser:nodejs /app/build ./build
COPY --from=builder --chown=svelteuser:nodejs /app/package*.json ./
COPY --from=builder --chown=svelteuser:nodejs /app/node_modules ./node_modules

USER svelteuser

EXPOSE 3000

CMD ["node", "build"]

Nixpacks

You can customize the Nixpacks build process by defining a nixpacks.toml file and using Nixpacks-specific environment variables. This allows you to fine-tune how dependencies are installed, how your application is built, and which runtime settings are applied. The following is an example nixpacks.toml configuration:
[phases.setup]
nixPkgs = ["nodejs", "yarn"]

[phases.install]
cmds = ["yarn install --frozen-lockfile"]

[phases.build]
cmds = ["yarn build"]

[start]
cmd = "node build"
Nixpacks will automatically detect your project’s lock file and select the appropriate package manager during deployment. Additionally, you can specify the Node.js version used during the build by setting the NIXPACKS_NODE_VERSIONenvironment variable.

Buildpacks

If you’re using Buildpacks, you cannot modify the underlying build phases directly or control dependencies. You must rely on the runtime environment that Buildpacks detects. Ensure your package.json has the correct scripts for the buildpack deployment:
{
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "preview": "vite preview",
    "start": "node build"
  }
}
Required:
  • build script: Compiles your SvelteKit application.
  • start script: Runs the production server (buildpack uses this).
Make sure @sveltejs/adapter-node is configured in svelte.config.js:
import adapter from "@sveltejs/adapter-node";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";

export default {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter({
      out: "build",
    }),
  },
};

CDN

Sevalla provides a premium, Cloudflare-powered CDN for Application Hosting at no additional cost. To get the most out of Sevalla’s CDN when deploying your SvelteKit application, we recommend the following best practices:
  • Enable the CDN for all production applications to ensure global, low-latency delivery.
  • Set appropriate Cache-Control headers on API routes to ensure proper caching behavior.
  • Purge CDN cache after critical deployments to ensure users receive updated content.
  • Rely on SvelteKit’s versioned assets in the .svelte-kit/output/ directory, which are safe to cache indefinitely.
  • Store static files in the static/ directory, allowing the CDN to serve them efficiently.

Image Optimization

<!-- src/routes/+page.svelte -->
<script>
  // Your component logic
</script>

<img
  src="/images/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  loading="lazy"
/>

Static Assets

Place static assets in the static/ directory:
static/
├── images/
├── fonts/
└── favicon.ico
SvelteKit automatically serves these with optimal caching headers.

Edge caching

Edge caching stores your Sevalla site cache on Cloudflare’s 260+ global data centers, delivering responses from the location nearest to each visitor for faster performance. To maximize the benefits of Sevalla’s Edge Caching for your SvelteKit application, we recommend the following best practices:
  • Set appropriate Cache-Control headers in loaders to control caching behavior.
  • Combine edge caching with the CDN for a complete caching strategy.
  • Use SvelteKit’s invalidateAll or invalidate for client-side revalidation.

Cache-Control

With Sevalla’s Cloudflare integration,Cache-Controlheaders are respected at the edge, giving you precise control over how content is cached and served globally. The following are some common directives you can use in your SvelteKit application:
  • public, s-maxage=3600 - Caches the response on the CDN for 1 hour, improving performance for frequently accessed content.
  • public, max-age=31536000, immutable - Ideal for versioned static assets, allowing them to be cached for up to 1 year with no revalidation.
  • private - Prevents CDN caching and ensures the response is only cached by the end user’s browser, for personalized or sensitive data.
Sevalla does not yet support the stale-while-revalidate Cache-Control directive. To prevent unexpected caching behavior, we recommend not using this directive in your API or asset caching settings.

Cache-Control headers for server routes

// src/routes/api/public-data/+server.ts
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";

export const GET: RequestHandler = async () => {
  const data = await fetchPublicData();

  return json(data, {
    headers: {
      "Cache-Control": "public, max-age=3600, s-maxage=3600",
    },
  });
};

Page load function caching with headers

// src/routes/products/+page.server.ts
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ setHeaders }) => {
  const products = await fetchProducts();

  setHeaders({
    "Cache-Control": "public, max-age=60, s-maxage=3600",
  });

  return { products };
};
<!-- src/routes/products/+page.svelte -->
<script lang="ts">
  import type { PageData } from "./$types";

  export let data: PageData;
</script>

<div>
  {#each data.products as product (product.id)}
    <div>{product.name}</div>
  {/each}
</div>

Server routes with caching

// src/routes/api/products/+server.ts
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";

export const GET: RequestHandler = async () => {
  const products = await fetchProducts();

  return json(products, {
    headers: {
      "Cache-Control": "public, s-maxage=3600",
    },
  });
};

Client-side revalidation

<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
  import { invalidateAll } from "$app/navigation";
  import { browser } from "$app/environment";
  import { onMount } from "svelte";

  onMount(() => {
    if (browser) {
      const handleFocus = () => {
        invalidateAll();
      };

      window.addEventListener("focus", handleFocus);
      return () => window.removeEventListener("focus", handleFocus);
    }
  });
</script>

<article>
  <!-- Your blog post content -->
</article>

Health checks

Ensure your application remains available during deployments by implementing health checks:
  • Always implement health checks for production applications.
  • Keep checks lightweight; responses should complete in under 1 second.
  • Verify critical dependencies (e.g., databases, Redis) as part of the checks.
  • Return 200 for degraded states to allow deployments to continue smoothly.
  • Return 503 only for critical failures that require pod restarts.
  • Close connections cleanly (e.g., database pools, Redis) to prevent resource leaks.
  • Test deployments in staging with monitoring scripts to validate health checks.

Basic health check

// src/routes/api/health/+server.ts
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";

export const GET: RequestHandler = async () => {
  return json(
    { status: "ok", timestamp: new Date().toISOString() },
    { status: 200 }
  );
};

Graceful shutdown

SvelteKit with adapter-node provides automatic graceful shutdown, but you can add custom cleanup logic, such as closing database connections, by handling SIGTERM and SIGINT yourself. The example below demonstrates how to shut down a PostgreSQL connection pool during termination:
// src/lib/server/db.ts
import { Pool } from "pg";

export const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,
  idleTimeoutMillis: 30000,
});

// Graceful pool shutdown
const gracefulPoolShutdown = async () => {
  console.log("Closing database pool...");
  await pool.end();
  console.log("Database pool closed");
};

process.on("SIGTERM", async () => {
  await gracefulPoolShutdown();
  process.exit(0);
});

process.on("SIGINT", async () => {
  await gracefulPoolShutdown();
  process.exit(0);
});

Multi-tenancy

Sevalla fully supports multi-tenancy. You can build multi-tenant SvelteKit applications using wildcard domains, allowing you to efficiently and securely serve multiple tenants from separate subdomains. Use the following best practices for multi-tenancy on Sevalla with your SvelteKit application:
  • Use wildcard domains to serve subdomain-based tenants (e.g., tenant1.app.com).
  • Add custom domains individually for tenants who have their own domains.
  • Cache tenant data to reduce database queries and improve performance.
  • Validate tenant existence before rendering pages to prevent errors or unauthorized access.
  • Isolate tenant data in the database using separate schemas or a tenant_id column.
  • Leverage free SSL certificates, which are automatically provided for both wildcard and custom domains.

SvelteKit multi-tenant implementation

Extract tenant from subdomain in load function:
// src/routes/+page.server.ts
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ request, url }) => {
  const hostname = url.hostname;
  const subdomain = hostname.split(".")[0];

  // Skip for main domain
  if (subdomain === "www" || subdomain === "yourdomain") {
    return { tenant: null };
  }

  const tenant = await getTenantData(subdomain);
  return { tenant };
};
<!-- src/routes/+page.svelte -->
<script lang="ts">
  import type { PageData } from "./$types";

  export let data: PageData;
</script>

<div>
  <h1>Welcome to {data.tenant?.name || "Our Platform"}</h1>
</div>
Tenant-specific data:
// src/lib/server/tenant.ts
import { pool } from "./db";

export async function getTenantData(tenantId: string) {
  const result = await pool.query(
    "SELECT * FROM tenants WHERE subdomain = $1",
    [tenantId]
  );

  return result.rows[0] || null;
}

Custom domains per tenant

Allow tenants to use their own domains (e.g., customdomain.com → tenant data). Map custom domains to tenants:
// src/lib/server/tenant.ts
export async function getTenantByDomain(hostname: string) {
  // First, check if this is a custom domain
  const customDomain = await pool.query(
    "SELECT tenant_id FROM custom_domains WHERE domain = $1",
    [hostname]
  );

  if (customDomain.rows.length > 0) {
    const tenantId = customDomain.rows[0].tenant_id;
    return await getTenantById(tenantId);
  }

  // Otherwise, extract from subdomain
  const subdomain = hostname.split(".")[0];
  return await getTenantData(subdomain);
}