SoftwareGuide

SvelteKit 3: what changes and how to migrate your app

SvelteKit 3 moves config into Vite, swaps $lib for #lib and reworks env vars and errors. What changes for a SvelteKit 2 app, in the order to migrate.

Source-based. Written from the documents, reporting and reviews linked in the text. Nothing here was tested hands-on by The Ruling Desk. How we work

Close-up of a monitor full of colorful JavaScript source code, slightly out of focus at the edges
Photo: Markus Spiske / Wikimedia Commons, CC0

SvelteKit 3 is out, and if you run a SvelteKit 2 app, the upgrade touches your config file, your imports, your environment variables and your error handling. The Svelte team shipped it on October 1, 2026, with a migration command that converts as much code as it can and leaves a to-do list for the rest. This guide walks through what actually changes and the order to tackle it in, built from the official migration guide. We describe the documented steps; we didn't run them on a production app.

Key takeaways

  • SvelteKit 3.0.0 was published on October 1, 2026. Config moves from svelte.config.js into vite.config.ts, and the $lib alias becomes #lib.
  • The command npx sv migrate sveltekit-3 automates nine documented tasks, but swapping $lib for #lib is a manual step, according to the Svelte team.
  • Environment variables now live in one src/env.ts file, and every error, including ones you throw with error(), now passes through handleError.
  • You need Node 22.17, TypeScript 6 and Vite 8.0.12 or newer before you start.
  • Remote functions are still experimental in 3.0 and need the Async Svelte flag.

What SvelteKit 3 changes

The release announcement lists the headline changes: configuration now lives in vite.config.ts instead of svelte.config.js, $lib becomes #lib using standard subpath imports, environment variables get a new explicit system, service workers need less boilerplate, and error handling is reworked across the board. The 3.0.0 release notes on GitHub are dated October 1, 2026, at 17:21 UTC.

The reasons are mostly about removing glue code. In the release candidate post from August 13, the team explained that putting config in the Vite file gives the Vite plugin your settings immediately, without an asynchronous lookup. The #lib switch leans on Node's subpath imports, which Vite and TypeScript already understand, so SvelteKit no longer has to keep its own alias in sync across tools.

Several older APIs are gone or deprecated. The migration guide says $app/stores is removed in favor of $app/state, base, assets and resolveRoute are removed from $app/paths, the $service-worker module is deleted, and $app/environment is renamed to $app/env.

What is still experimental

Remote functions, SvelteKit's type-safe way to call server code from the client, are not finished. The announcement calls them the team's top priority and says they still require the experimental Async Svelte feature. The migration guide says you enable them with experimental.remoteFunctions: true plus compilerOptions.experimental.async: true. If your app doesn't use them, this changes nothing for you today.

Step 1: Check the new minimum versions

Before running anything, the migration guide lists these minimums:

DependencyMinimum for SvelteKit 3
Node22.17
TypeScript6
Svelte5.57.1 (the 3.0.0 changelog says 5.56.4)
Vite8.0.12
@sveltejs/vite-plugin-svelte7

Upgrade Node and TypeScript in your CI and hosting first, since those live outside your repo. Then commit or stash your work: the sv migrate documentation says the tool prompts before changing files when your git working tree is dirty, and a clean tree makes its diff easy to review.

Step 2: Run the SvelteKit 3 migration

The announcement gives the one-shot command, npx sv migrate sveltekit-3 --tasks all --confirm, which applies every task without prompting and writes a to-do list for what it can't fix. Run npx sv migrate sveltekit-3 without flags if you'd rather pick tasks one by one. Per the sv docs, the nine tasks are:

  1. package-json: updates package versions.
  2. tsconfig: extends the generated $app/tsconfig instead of .svelte-kit/tsconfig.json.
  3. svelte-config: moves supported settings from svelte.config.* into vite.config.*.
  4. environment: replaces the legacy environment modules with $app/env.
  5. paths: migrates the removed $app/paths APIs and path types.
  6. external-redirects: opts external redirects into the new behavior.
  7. shallow-routing: replaces pushState and replaceState with goto calls.
  8. params: gathers route parameter matchers into src/params.ts (or .js).
  9. app-state: moves $app/stores usage to $app/state.

Other flags in the docs include --files to limit the run to a glob, --no-install to skip installing dependencies afterward, and --no-git-check to skip the dirty-tree prompt.

Step 3: Switch $lib to #lib by hand

None of the nine tasks covers the alias, and the release candidate post says import paths have to be updated manually. The migration guide shows the setup: add an imports field to package.json, then replace $lib with #lib across your code.

{
  "imports": {
    "#lib": "./src/lib/index.js",
    "#lib/*": "./src/lib/*"
  }
}

The catch is file extensions. Subpath imports don't guess, so $lib/utils becomes #lib/utils.ts, and a folder import becomes #lib/foo/index.ts. A project-wide find and replace gets you most of the way; your type checker will flag the imports that still lack an extension.

Step 4: Move environment variables to src/env.ts

The $env/* modules still work but are deprecated, and the environment variables docs say they will be removed in SvelteKit 4. The new system has you declare every variable in src/env.ts with defineEnvVars, now imported from @sveltejs/kit/env. Each entry can set:

  • public: safe to send to the browser (variables are private by default);
  • static: inlined at build time, so the value is frozen to whatever existed during the build;
  • schema: a validator function or a Standard Schema library such as Zod or Valibot;
  • description: a note that shows up in your editor.

You then import private values from $app/env/private and public ones from $app/env/public. The docs say the app fails to start or build when validation fails, so a missing or invalid variable is caught when the app starts or builds, not later in production.

Step 5: Review your error handling

This is the step most likely to change behavior without breaking the build. According to the migration guide:

  • handleError now receives every error, including the ones you throw on purpose with error().
  • error() takes a string message as its second argument; any extra properties move to a third argument.
  • handleValidationError is removed. Validation errors reach handleError with kind: 'validation'.
  • App.Error always includes a status, and handleError can return a status to set the HTTP code.
  • Rendering errors now go through handleError and +error.svelte boundaries too, not only load errors.
  • Enhanced form actions respond with the status code you passed to fail().

If your handleError logs to an error tracker, expect more events, since intentional 404s now pass through it. Filter on status if you don't want them.

Step 6: Fix navigation, cookies and adapters

The migration guide flags a set of smaller breaking changes worth a search through your code:

  • goto() now rejects when a URL doesn't resolve to a route in your app. Use window.location.href for outside links.
  • invalidateAll is deprecated in favor of refreshAll(), which keeps page.state.
  • redirect() to another site needs { external: true } (the external-redirects migration task is meant to add this).
  • Cookies without an explicit path now default to '/', so they apply site-wide.
  • csrf.checkOrigin is gone; CSRF protection is always on, with csrf.trustedOrigins as the allowlist.
  • json() and text() are deprecated; use Response.json() and new Response(text).

Adapters changed too. The guide says adapter-node drops the ORIGIN environment variable in favor of paths.origin, adapter-vercel no longer supports the edge runtime, adapter-cloudflare needs wrangler ^4.67.0 and imports Cloudflare APIs from cloudflare:workers, and adapter-netlify needs Netlify CLI 17.31.0 or newer.

Step 7: Work through the to-do list and test

Finish with the list the migration tool generated, then run your type checker, your tests and a production build. Click through forms, redirects and any page that sets cookies, since those are the behavior changes a compiler won't catch. For more on frameworks and tools, see our software coverage and everything tagged SvelteKit.

Troubleshooting

  • Imports fail to resolve after the switch. Check for a missing .ts or .js extension on #lib imports, and confirm the imports field is in the root package.json.
  • A redirect to another site throws. Add { external: true }, or a list of allowed origins, to the redirect() call.
  • A cross-origin form post is rejected. The guide says a missing Content-Type header is now treated as CSRF; add the header or list the origin in csrf.trustedOrigins.
  • A 204 response lost its body. That's intended: empty 2xx responses now return no content, per the HTTP spec.
  • Your service worker stopped finding $service-worker. Import version from $app/env, file lists from $app/manifest and paths from $app/paths.

Bottom line

We think that for most SvelteKit 2 apps, upgrading to SvelteKit 3 is a careful review rather than a rewrite: bump your toolchain, run sv migrate, do the #lib swap by hand, and spend real time on error handling, which is where behavior quietly changes. If you depend on remote functions, keep treating them as experimental. A Hacker News thread on the release had passed 170 comments as of October 3, 2026, with some commenters saying they already run remote functions in production; that's their call, not the Svelte team's guarantee. Watch the Svelte blog for the remote functions milestone next.

FAQ

Is SvelteKit 3 stable?

Yes. Version 3.0.0 was published as a stable release on October 1, 2026, after a release candidate announced on August 13. Remote functions inside it are still experimental.

Does sv migrate change $lib to #lib?

No. The documented sveltekit-3 tasks don't include it, and the Svelte team says import paths must be updated by hand. You add an imports field to package.json and replace the imports, with file extensions.

What Node version does SvelteKit 3 need?

The migration guide lists Node 22.17 as the minimum, alongside TypeScript 6 and Vite 8.0.12.

Do I have to move my environment variables right away?

Not immediately. The $env/* modules are deprecated in SvelteKit 3, not removed, and the docs say they will be removed in SvelteKit 4. Moving to src/env.ts now gets you validation and type safety.

Filed under Software

Newsletter

New articles, in your inbox.

Free. Unsubscribe in one click. Your email is kept by beehiiv, our newsletter service, and used only for this newsletter.