Skip to main content
Frameworks

SvelteKit on Easel

Deploy SvelteKit applications to Easel with SSR, streaming, prerendering, form actions, hooks, and API endpoints.

Easel runs SvelteKit applications built with the standard Vercel adapter. Prerendered pages and client assets go to the CDN, and server-rendered pages, load functions, form actions, hooks, and +server endpoints run in Easel Functions with streaming.

At a glance

Support levelProduction-ready
RenderingStatic, SSR, streaming, and client-side rendering
Server runtimeNode.js
Required adapter@sveltejs/adapter-vercel
Automatic detectionYes
Most recently tested withSvelteKit 2.70.x

Deploy a SvelteKit application

Install the Vercel adapter:

npm install -D @sveltejs/adapter-vercel

Then select it in svelte.config.js:

svelte.config.js
import adapter from "@sveltejs/adapter-vercel";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";

/** @type {import("@sveltejs/kit").Config} */
const config = {
  preprocess: vitePreprocess(),

  kit: {
    adapter: adapter(),
  },
};

export default config;

The adapter produces the deployable output Easel reads. It doesn't deploy to Vercel or need a Vercel account. Don't use adapter-auto. It picks an adapter from the build environment, so the output isn't reproducible.

Create a project from your Git repository or deploy with the CLI:

easel deploy

Easel detects SvelteKit and runs your build script or vite build. Install command, build command, root directory, and Node.js version live in build configuration.

What Easel deploys

SvelteKit outputEasel resource
Files in staticCDN assets
Client JavaScript and CSSCDN assets
Prerendered pagesCDN assets
Server-rendered pagesEasel Functions
Server load functionsEasel Functions
Form actionsEasel Functions
+server endpointsEasel Functions
Server hooksEasel Functions
Redirects and route metadataEasel request routing

By default the adapter packages every dynamic route into one shared server function, and SvelteKit routes requests inside it. Prerendered routes drop out of that bundle.

The adapter can also split routes into separate functions. Keep the default unless you need to isolate a heavy endpoint or give it different settings, and verify a split build in a preview deployment.

Supported features

FeatureSupportNotes
Server-side renderingSupportedDynamic routes run in Easel Functions
PrerenderingSupportedIncludes prerender = "auto" with entries
Client-side renderingSupportedSet with ssr and csr page options
Streaming server loadsSupportedReturned promises stream as they resolve
Streaming endpointsSupportedReadableStream responses aren't buffered
Universal and server loadSupportedFollow SvelteKit's normal lifecycle
Form actionsSupportedIncludes progressive enhancement with use:enhance
API endpointsSupported+server handlers
Server hooksSupportedhandle, handleFetch, and handleError
CookiesSupportedStandard cookies API
Route parameters, groupsSupportedIncludes optional, rest, and matched parameters
Environment modulesSupported$env/static/* and $env/dynamic/*
Server instrumentationSupportedIncluded when emitted by the adapter
Base paths, trailing slashSupportedkit.paths.base and the trailingSlash option
Service workersSupportedBuilt as static assets and served from the CDN
Adapter infrastructure optionsPartialSee Adapter options

Rendering

Pages are server-rendered by default. A server load function can read private environment variables, cookies, and locals, then return data to the page:

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

export const load: PageServerLoad = async ({ locals }) => {
  return {
    user: locals.user,
  };
};

The load function and the page render run in an Easel Function.

Prerendering

Export prerender from a page or layout to generate it at build time. Prerendered pages deploy as static files and don't invoke a function:

src/routes/(marketing)/+layout.ts
export const prerender = true;

Child routes can override the inherited value with prerender = false. Use "auto" with an entries function to prerender known parameter values while other values still render on the server:

src/routes/blog/[slug]/+page.server.ts
import type { EntryGenerator } from "./$types";

export const prerender = "auto";

export const entries: EntryGenerator = () => {
  return [{ slug: "introducing-easel" }, { slug: "deploying-sveltekit" }];
};

Client-side rendering

Set ssr = false in +page.ts to render a route only in the browser, or csr = false to ship static HTML without hydration.

Streaming

A server load function can return promises that resolve after the initial page data:

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

export const load: PageServerLoad = async ({ params, fetch }) => {
  const product = await loadProduct(params.id);

  return {
    product,
    reviews: fetch(`/api/products/${params.id}/reviews`).then((r) => r.json()),
  };
};

Render the streamed value with {#await data.reviews} in the page. Easel forwards the response as SvelteKit produces it instead of waiting for every promise to settle.

Once streaming starts, status and headers are fixed. Don't redirect or call setHeaders from inside a promise that resolves later. Streamed data needs browser JavaScript. Without it, the browser waits for the full response.

Form actions

Form actions run in the server function:

src/routes/contact/+page.server.ts
import { fail } from "@sveltejs/kit";
import type { Actions } from "./$types";

export const actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    const email = data.get("email");

    if (typeof email !== "string" || !email.includes("@")) {
      return fail(400, { email, invalid: true });
    }

    await saveContactRequest({ email });
    return { success: true };
  },
} satisfies Actions;

A plain <form method="POST"> works without JavaScript. Add use:enhance from $app/forms to submit without a full-page reload.

API endpoints

+server files become HTTP endpoints:

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

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

Handlers can return any standard Response, including redirects, files, Server-Sent Events, and ReadableStream bodies. Long-lived responses stay subject to the function's maximum duration (limits).

Hooks

Server hooks run inside the server function. Use handle to authenticate requests, populate event.locals, or change responses:

src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";

export const handle: Handle = async ({ event, resolve }) => {
  const session = event.cookies.get("session");

  event.locals.user = session ? await findUserBySession(session) : null;

  return resolve(event);
};

locals is then available to server loads, actions, and endpoints. Use handleFetch to change server-side requests made through SvelteKit's fetch.

SvelteKit's cookies API works in loads, actions, endpoints, and hooks. Keep session state in a database or shared store, not function memory.

Cache control

Set cache headers from a server load with setHeaders, or directly on an endpoint's Response:

src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ params, setHeaders }) => {
  const post = await loadPost(params.slug);

  setHeaders({
    "cache-control": "public, s-maxage=300, stale-while-revalidate=86400",
  });

  return { post };
};

The Easel CDN caches eligible responses from these headers. Don't mark personalized or authenticated responses public. See Caching.

Environment variables

SvelteKit splits variables by visibility and by when they're read:

ModuleVisibilityResolved
$env/static/privateServer onlyBuild time, inlined
$env/static/publicBrowser, PUBLIC_ prefixBuild time, inlined
$env/dynamic/privateServer onlyRequest time, from Easel
$env/dynamic/publicBrowser, PUBLIC_ prefixRequest time, from Easel

Static values need a new deployment after they change. SvelteKit blocks private modules from browser code. Recent SvelteKit 2 releases can opt into the explicit $app/env/private and $app/env/public modules declared in src/env.ts. Easel supplies the same values either way.

Set values per environment in Environment variables.

Adapter options

Easel reads the adapter's functions, static and prerendered assets, and routing config. Project settings stay authoritative for function memory, CPU, and scaling. Route maxDuration is honored when the adapter emits it. Don't rely on the adapter's other memory or region options. See Function configuration.

Build-time image processing works because it emits ordinary static assets. Test runtime image plugins in a preview deployment, since they may depend on provider-specific APIs.

Project configuration

SettingDefault
Install commandDetected from the package manager
Build commandPackage script or vite build
Output directoryRead from the adapter output
Development commandPackage script or vite dev

In a monorepo, set the root directory to the folder that holds the app's package.json and svelte.config.js.

Local development

Keep using npm run dev. The Vite dev server runs routing, SSR, loads, actions, hooks, and endpoints locally. Use a preview deployment to check adapter output, function packaging, streaming, and CDN caching before production.

Known limitations

SvelteKit runs in Easel Functions, which have no WebSockets, an ephemeral filesystem, and no work after the response unless you register it. See Function runtime and Background work.

Vercel-specific adapter options aren't reproduced. Configure infrastructure in Easel project settings.

Troubleshooting

Easel doesn't find a server application

Confirm that @sveltejs/adapter-vercel is installed and selected in svelte.config.js, not adapter-auto.

The deployment contains only static files

Check whether the root layout exports prerender = true. Fully prerendered routes leave the server bundle.

A route fails during prerendering

A prerendered route can't read cookies, headers, or other request data. Set prerender = false for it or move the request-dependent work elsewhere.

A dynamic route is missing from the prerendered output

SvelteKit needs to know which parameter values to generate. Export entries, or link to the route from another prerendered page.

An environment variable is undefined

Check that you import from the right module (static or dynamic, private or public). Browser-visible variables need the PUBLIC_ prefix. Static values need a new deployment after a change.

A streamed value never appears early

Return the promise from the server load instead of awaiting it. Headers and redirects must happen before streaming starts.

Compatibility policy

Easel's SvelteKit baseline is the version under At a glance. Test upgrades in a preview deployment, as described in the compatibility policy.

Next steps