SvelteKit 3 Migration Guide: Upgrading from SvelteKit 2 Step by Step

On August 13 2026 the Svelte team announced the SvelteKit 3 Release Candidate. The changes are mostly small. There are many of them. A key changes. Like where configuration lives the use of $lib and $app/stores. End up affecting nearly every file in a typical project.

I wrote this guide in the order you’ll likely encounter issues during the upgrade. Most examples come directly from the code of this blog. The site itself is a SvelteKit 2 project using Svelte 5, mdsvex, Paraglide and Vercel. It’s as straightforward as it gets. If something needs to change it will probably need to change in your project too.

Note: This guide is based on SvelteKit 3 RC. The team doesn’t expect any breaking changes, before the stable release. Still it’s an idea to review the official migration guide before you begin. You can find it at https://next.svelte.dev/docs/kit/migrating-to-sveltekit-3.

TL;DR: What Changes in SvelteKit 3

The short version, if you’re in a hurry:

AreaSvelteKit 2SvelteKit 3
Configurationsvelte.config.jssveltekit() options in vite.config.js
Library alias$lib/foo#lib/foo.js (Node subpath imports)
Page state$app/stores ($page)$app/state (page), stores removed
Environment$app/environment$app/env
Env variables$env/static/*, $env/dynamic/*src/env.ts + $app/env/public / private
Shallow routingpushState / replaceStategoto(url, { shallow: true })
Refreshing datainvalidateAllrefreshAll
Pathsbase, assets, resolveRouteresolve() and asset() only
TypeScriptextends ./.svelte-kit/tsconfig.jsonextends $app/tsconfig
ErrorshandleError skips expected errorshandleError receives every error, render included
Build toolVite 5 to 8Vite 8 required (Rolldown)
Response helpersjson(), text()Response.json(), new Response() (deprecated)
Remote functionsExperimentalStill experimental

Prepare the Ground

Don’t skip straight to 3.0. The team says you should go to the 2.x release first and there is a good reason for that. A lot of the changes that came in 3.0 were added to 2.x as features. For example config, in vite.config.js showed up in 2.62 and explicit environment variables arrived around the time. The recent 2.x version also gives you deprecation warnings. These warnings point directly to the lines you need to update. This means the latest 2.x release basically writes half of your to-do list for you.

pnpm add -D @sveltejs/kit@^2
pnpm dev   # note every deprecation warning in the terminal and browser console

Then create a branch. The codemod is going to touch dozens of files and you’ll want a diff you can actually review:

git switch -c chore/sveltekit-3

Step 1: Run the Automated Migration

SvelteKit ships a codemod through the sv CLI:

npx sv@next migrate sveltekit-3 --tasks all --confirm

It converts what it can and leaves a TODO list for the spots it isn’t sure about. For everything else, SvelteKit 3’s error messages do a decent job of telling you what to change. From there it’s the usual loop: start the dev server, read the error, fix it, try again.

That said, don’t skip the rest of this guide just because the codemod finished. Error handling, cookies, URLs that goto now rejects, deployment polling: in all of these the code still compiles but behaves differently. No tool can make those calls for you.

Step 2: Update the Toolchain

Every minimum version went up:

PackageMinimum version
Node.js22.17
TypeScript6
svelte5.57.1
vite8.0.12
@sveltejs/vite-plugin-svelte7
pnpm add -D @sveltejs/kit@next svelte@latest vite@^8 @sveltejs/vite-plugin-svelte@^7 typescript@^6

Vite 8.0.12 is the first release that ships stable Rolldown v1, a Rust bundler that does the job esbuild and Rollup used to split between them. Your builds get faster without touching a line of code.

Two things to check here:

  1. Node on CI and your host. Anything below 22.17 is out. Update engines in package.json, your .nvmrc, and the setting in your hosting dashboard. The error you get from building on an old Node version in CI isn’t always obvious at first glance.
  2. Vite plugins. Each one needs Vite 8 support. On this blog that means Tailwind (@tailwindcss/vite), @sveltejs/enhanced-img, Paraglide, the Traceway plugin and rollup-plugin-visualizer. Most Rollup plugins work fine under Rolldown, but when something breaks, check the plugins one by one before blaming SvelteKit.

Step 3: Move svelte.config.js into vite.config.js

I see this as the change you will notice: svelte.config.js is no longer read. All settings now go into the sveltekit() plugin. Everything under kit.* moves one level up. Sits next to Svelte settings such as compilerOptions and preprocess.

The reason behind this shift is technical. It makes sense. The Vite plugin needs the configuration immediately. Loading a second file asynchronously caused trouble with tools such, as Vitest when they start from a different directory. When everything lives in one file that trouble disappears.

I also like that we no longer have to jump between two config files; that is a bonus.

Before (SvelteKit 2):

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

import mdsvexConfig from "./mdsvex.config.js";

export default {
  extensions: [".svelte", ".md"],
  preprocess: [vitePreprocess(), mdsvex(mdsvexConfig)],
  kit: {
    adapter: adapter(),
    alias: {
      $store: "src/store/index.js",
      $posts: "src/posts",
    },
    csrf: { checkOrigin: false },
  },
};

After (SvelteKit 3):

// vite.config.js
import adapter from "@sveltejs/adapter-auto";
import { sveltekit } from "@sveltejs/kit/vite";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import { mdsvex } from "mdsvex";
import { defineConfig } from "vite";

import mdsvexConfig from "./mdsvex.config.js";

export default defineConfig({
  plugins: [
    sveltekit({
      adapter: adapter(),
      extensions: [".svelte", ".md"],
      preprocess: [vitePreprocess(), mdsvex(mdsvexConfig)],
      csrf: { trustedOrigins: ["https://trusted-site.com"] },
    }),
  ],
});

Options SvelteKit doesn’t recognize get passed on to vite-plugin-svelte, so settings like inspector go here too. The experimental key is shared: SvelteKit takes its own flags and forwards the rest.

Removed, added and changed options

OptionWhat to do
vitePluginRemoved. Pass vite-plugin-svelte options directly to sveltekit()
files.libRemoved. Use the #lib subpath import instead
preloadStrategyRemoved. modulepreload is always used now
prerender.originRemoved. Use paths.origin
csrf.checkOriginRemoved. CSRF is always on, allow hosts with csrf.trustedOrigins
experimental.tracingNow a top-level tracing option
experimental.instrumentationNo longer needed. src/instrumentation.server.js is picked up if present
experimental.handleRenderingErrorsNo longer needed. Rendering errors are always handled
paths.origin (new)Public origin for CSRF checks. Replaces the ORIGIN env var on adapter-node
output.linkHeaderPreload (new)Opt back into Link headers for preloads. Default is <link> elements
version.pollInterval (changed)Now defaults to one hour instead of no polling

The last row can easily be overlooked.

SvelteKit 3 checks, for a deployment every hour.

It also checks when someone returns to the tab, when the page becomes visible again and on every server data request or remote function call.

When it finds one it sets updated.current to true.

If your site shows a ” version available please refresh” banner people will see it a lot more often now.

Usually that is what you want.

Step 4: Replace $lib with #lib

SvelteKit no longer defines the $lib alias for you. It uses Node’s subpath imports instead. Vite and TypeScript already understand them, and you declare the alias yourself in package.json:

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

Your imports change like this:

// before
import { cn } from "$lib/utils";
import { Seo } from "$lib/components";

// after
import { cn } from "#lib/utils/index.js";
import { Seo } from "#lib/components/index.js";

This step will eat most of your time, because file extensions are now required. Node doesn’t guess with subpath imports, so #lib/utils won’t find #lib/utils/index.js on its own. Meanwhile extensionless barrel imports are practically the norm in SvelteKit projects. I counted on this blog: excluding the ones Paraglide generates, 63 of 64 $lib imports have no extension. $lib/utils alone shows up 38 times and $lib/components 15 times. This will probably be the noisiest part of your diff.

One more detail if you use TypeScript: you write .js in the import path even when the source file is .ts. It sounds backwards, but TypeScript maps it to the right file. If you’ve used NodeNext, you already know this rule.

Custom aliases

The official guide only retires $lib and leaves your own aliases alone. But since you’re already going through these files, it’s worth moving those to subpath imports too. That way Node, Vite and TypeScript all resolve the same path without separate config:

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

The only rule is that names must start with #. If you move aliases like $store or $posts, their names change too.

Step 5: Update tsconfig.json

Your tsconfig.json (or jsconfig.json) now extends $app/tsconfig. That file is generated into node_modules/$app and comes with recommended settings like isolatedModules and verbatimModuleSyntax already in place. You can probably delete most of your own compilerOptions block.

{
  "extends": "$app/tsconfig",
  "include": ["src", "test", "*"],
  "exclude": ["src/service-worker"]
}

include and exclude are no longer filled in for you, so write them out yourself. If you have a service worker, give it its own src/service-worker/tsconfig.json that extends $app/tsconfig/service-worker.

Step 6: Update $app/* Imports

$app/stores is removed

The Svelte 4 stores are gone for good. In their place is $app/state, which runs on Svelte 5 runes. Change the import and drop the $ prefix:

<script>
// before
// import { page } from "$app/stores";
import { page } from "$app/state";
</script>

<!-- before: {$page.url.pathname} --><p>Current path: {page.url.pathname}</p>

On this blog, 7 route files still import from $app/stores, most of them just to pass $page.url.pathname into the SEO component. Each one is a two-line fix, but if even one is left, SvelteKit 3 won’t build.

Also, page.url is now read-only. It’s typed as ReadonlyURL, and its query params as ReadonlyURLSearchParams. Writing page.url.searchParams.set(...) on filter pages used to be really common. Now you have to copy it first:

const url = new URL(page.url.href);
url.searchParams.set("q", "svelte");
goto(url);

$app/environment is now $app/env

Only the name changed. browser, dev, building and version now come from $app/env. As a bonus, you can import it inside a service worker too.

// before
import { browser, dev } from "$app/environment";

// after
import { browser, dev } from "$app/env";

$app/navigation: shallow routing, refreshAll and goto

Shallow routing now goes through goto with shallow: true instead of pushState and replaceState:

import { goto } from "$app/navigation";

// before: pushState("/photos/42", { selected: 42 });
goto("/photos/42", { shallow: true, state: { selected: 42 } });

// before: replaceState("/photos/42", { selected: 42 });
goto("/photos/42", { shallow: true, replace: true, state: { selected: 42 } });

Watch out: shallow navigations now also run beforeNavigate, onNavigate and afterNavigate. If your onNavigate starts View Transitions or sends an analytics event, check the shallow field on the navigation object and skip those cases. Otherwise opening a photo modal plays your page transition, or logs a fake page view in analytics.

There’s a nice addition too: persistState: true. It restores page.state after a reload, so a modal tied to the URL survives someone hitting refresh.

Other goto changes:

SvelteKit 2SvelteKit 3
invalidateAll()refreshAll()
goto(url, { invalidateAll: true })goto(url, { refreshAll: true })
goto(url, { keepFocus: true, noScroll: true })goto(url, { reset: false })
goto(url, { replaceState: true })goto(url, { replace: true })

Two behaviour changes matter in particular:

  • goto now throws for URLs that don’t match any route. It used to do that only for external addresses. To leave your app, use window.location.href = url.
  • delta is only set on popstate navigations. For link clicks and goto calls it’s undefined.

There’s a small but useful difference between refreshAll and invalidateAll: refreshAll doesn’t reset page.state. And if you call invalidate or refreshAll while a navigation is in progress, that navigation no longer gets cut short.

If any of your code reads the result of preloadData(...), handle the new { type: "error", status, error } case too. It used to return loaded with a 200 status even for pages that failed.

$app/paths: only resolve and asset remain

The deprecated base, assets and resolveRoute are gone for good. The part that needs real attention: pathnames no longer start with /. The leading slash only stays on route IDs:

import { asset, resolve } from "$app/paths";

// pathname: no leading slash
const about = resolve("about");

// route ID + params: leading slash
const post = resolve("/blog/[slug]", { slug: "hello-world" });

// static asset: no leading slash
const logo = asset("logo.png");

Type names changed as well: Pathname is now Path, and Asset is now AssetPath. I’d search the project for resolve("/ and asset("/ and look at each result individually. The codemod can’t always tell whether an argument is a pathname or a route ID.

Step 7: Switch to Explicit Environment Variables

The $env/static/* and $env/dynamic/* modules are deprecated in SvelteKit 3 and will be removed in SvelteKit 4. They still work for now, so you could put this step off. But while you’re already in these files, it makes sense to do it.

In the new approach, you declare every variable you use in src/env.ts. You mark which ones may reach the browser and, if you like, validate them with any Standard Schema library.

// src/env.ts
import { defineEnvVars } from "@sveltejs/kit/env";
import * as v from "valibot";

import { building } from "$app/env";

export const variables = defineEnvVars({
  PUBLIC_SITE_URL: {
    public: true,
    schema: v.pipe(v.string(), v.url()),
  },
  PUBLIC_MEASUREMENT_ID: {
    public: true,
    schema: v.pipe(v.string(), v.regex(/^G-[A-Z0-9]+$/)),
  },
  SECRET_TURNSTILE_KEY: {
    // optional at build time, required when the server starts
    schema: building ? v.optional(v.string()) : v.string(),
  },
});

Then you import from the new modules:

// before
import { PUBLIC_MEASUREMENT_ID } from "$env/static/public";
import { SECRET_TURNSTILE_KEY } from "$env/static/private";

// after
import { SECRET_TURNSTILE_KEY } from "$app/env/private";
import { PUBLIC_MEASUREMENT_ID } from "$app/env/public";

It looks like extra work at first, but it pays off. A missing or wrong value no longer quietly turns into undefined at runtime. It fails when the build or server starts. Types come from the schema, so you don’t write them separately. Nothing reaches the browser unless you say public: true, and $app/env/private can’t even be imported in client code. You can also use public variables in app.html as %sveltekit.env.PUBLIC_MEASUREMENT_ID%, which comes in handy for analytics snippets.

This blog has 13 files using $env/static/*: SEO, analytics, the footer, RSS, the sitemap and the contact form API. Moving them all is boring but not hard. In return, a production deploy without a Turnstile key gets caught at build time, not after the contact form has already broken.

Step 8: Review Error Handling

SvelteKit 2 had to keep supporting Svelte 4, and Svelte 4 had no such thing as error boundaries. That’s why +error.svelte could only show errors that happened during load. SvelteKit 3 requires Svelte 5 outright and uses <svelte:boundary> under the hood.

What changed:

  1. Rendering errors are caught too. If a component throws while rendering, the error goes through handleError first and is then shown by the nearest +error.svelte.
  2. handleError receives every error. That includes ones you create on purpose with error(404, ...). Before, those never reached handleError.
  3. handleError can change the status code. Just add a status to its return value.
  4. App.Error now always includes a status.
  5. The error() signature changed. The second argument can only be a string, extra fields go in a third argument.
  6. handleValidationError is removed. Validation errors reach handleError with kind: "validation".
  7. Stack traces use sourcemaps by default.

In practice, point 2 is the one that will give you headaches. This blog sends errors to Traceway, and its SvelteKit 2 hooks.client.js currently looks like this:

// src/hooks.client.js (SvelteKit 2)
export function handleError({ error, event }) {
  captureExceptionWithAttributes(error, {
    route: event.route?.id ?? event.url.pathname,
  });
  return { message: "Something went wrong" };
}

If this file moves to SvelteKit 3 as is, every visitor who lands on a missing page creates a new error in Traceway. Picture opening the dashboard the morning after a bot crawled your site overnight. Filtering out expected errors is a must:

// src/hooks.client.js (SvelteKit 3)
import { isHttpError } from "@sveltejs/kit";

/** @type {import("@sveltejs/kit/hooks").HandleClientError} */
export function handleError({ error, event, status }) {
  // expected errors like 404s now end up here too
  if (!isHttpError(error) || status >= 500) {
    captureExceptionWithAttributes(error, {
      route: event.route?.id ?? event.url.pathname,
    });
  }

  return { message: "Something went wrong", status };
}

Hook types moved as well: Handle, HandleClientError and the rest now live in @sveltejs/kit/hooks. If the handleError in hooks.client.js is async, turn on compilerOptions.experimental.async so it can be awaited during rendering.

Step 9: Server, Security and Response Changes

Each item in this section is small on its own, but any of them can quietly break something in production:

  • json() and text() are deprecated. Use the platform’s own APIs instead: Response.json(data, init) and new Response(text). Four API routes on this blog use json(), and the fix is one line in each.
  • 204 responses are now empty. When +server.js returns a 2xx with no body, SvelteKit no longer adds its own JSON. That’s what the HTTP spec expects anyway.
  • Cookies are handled by cookie v2. Cookie names can only use ASCII characters, so names with accented letters like á are now rejected. Cookies set without a path also default to / instead of the request path.
  • Redirects to external URLs need opting in: write redirect(307, "https://example.com", { external: true }) or pass a list of allowed origins.
  • Cross-origin form submissions without a Content-Type header are treated as CSRF and rejected.
  • The definition of server-only modules got wider. Any file with server in its name (server.ts, db.server.ts) and any folder named server, not just src/lib/server, is now server-only. If you have something like src/lib/components/server, it may suddenly stop being importable on the client.
  • Param matchers move out of the src/params folder into a single src/params.ts file. You use defineParams from @sveltejs/kit/params, and a matcher can now be a Standard Schema directly.
  • data-sveltekit-* attributes now take false instead of "off".
  • Clicking a link to the page you’re already on used to do nothing. Now it triggers refreshAll().
  • config in +page.js now takes priority over the one in +page.server.js.
  • Service workers are registered with type: "module". The old $service-worker module is gone, replaced by $app/env, $app/manifest, $app/paths and the new typed $app/service-worker.

Step 10: Check Your Adapter

All first-party adapters require SvelteKit 3. On top of that, each one changed:

AdapterChange
adapter-vercelThe edge runtime is no longer supported. Move edge routes to the Node.js runtime
adapter-nodeBundled with Rolldown. ORIGIN env var removed (use paths.origin). Static files are fixed at build time
adapter-cloudflareplatform.env and platform.context are gone. Import env and waitUntil from cloudflare:workers
adapter-netlifyUses the stable Netlify Frameworks API. Requires Netlify CLI 17.31+

If you’re on Vercel, pay close attention to edge support going away. If a route exports config = { runtime: "edge" }, it has to move to the Node.js runtime before you deploy. This blog has no edge routes, but in some projects that one line can stop a deploy cold.

adapter-node has a small surprise as well. The list of static files is collected at build time, and files you copy into the output folder afterwards aren’t served. If you generate files at runtime, serve them through a route.

Remote Functions in SvelteKit 3

If you met SvelteKit Remote Functions during the 2.x days, know this: they’re still experimental in SvelteKit 3. The flags move into vite.config.js along with everything else:

sveltekit({
  compilerOptions: { experimental: { async: true } },
  experimental: { remoteFunctions: true },
});

What changed for your existing remote code:

  • Any file with remote in its name (posts.remote.ts, remote.ts) counts as a remote module. If one of these sits in the project without the flag turned on, you get an error.
  • Reading event.url, event.params or event.route inside a query now throws. Pass whatever you need as an argument instead.
  • The error field on queries and forms is typed as App.Error | undefined, not any.
  • Form inputs must take their attributes from a field, for example myForm.fields.message.as("text"). A hand-written name="message" is no longer accepted.
  • Types like RemoteQuery and RemoteForm moved to $app/server.

Performance Impact

This is a performance blog, so I can’t skip this part. What does SvelteKit 3 change for users and for builds?

Build time. Probably the first thing you’ll notice. With Vite 8, Rolldown takes over from both esbuild and Rollup.

How preloading works. SvelteKit 3 preloads modules with <link rel="modulepreload"> elements in the HTML instead of Link headers. On some hosts those headers got too big and caused problems, and this change fixes that too. Since modulepreload is supported in every modern browser, the preloadStrategy option is no longer needed.

Fewer re-renders. With the $page store, any field changing re-ran every subscribed component. With $app/state, a component only updates when the field it actually reads changes. On sites with lots of page-to-page navigation, that gives INP a small boost without any extra work.

Readable error reports. Since sourcemaps are applied to stack traces, tracking down the source of a production error gets easier.

Version checks. A small request goes out once an hour and when someone returns to the tab. Users won’t notice, but you’ll see it in your logs, so don’t be surprised.

If you prerender pages and use Speculation Rules, nothing in SvelteKit 3 conflicts with that. Just remember to update any data-sveltekit-preload-data="off" to "false".

SvelteKit 3 Migration Checklist

Paste this into your pull request description and tick things off as you go:

  • Upgraded to the latest SvelteKit 2.x and fixed deprecation warnings
  • Ran npx sv@next migrate sveltekit-3 --tasks all --confirm and reviewed the TODO list
  • Node 22.17+, TypeScript 6, Svelte 5.57.1+, Vite 8.0.12+, vite-plugin-svelte 7
  • All Vite plugins verified against Vite 8 and Rolldown
  • svelte.config.js merged into vite.config.js and deleted
  • csrf.checkOrigin, prerender.origin, preloadStrategy, vitePlugin removed or replaced
  • #lib declared in package.json, every $lib import rewritten with a file extension
  • tsconfig.json / jsconfig.json extends $app/tsconfig with explicit include / exclude
  • $app/stores replaced with $app/state, no code mutates page.url
  • $app/environment replaced with $app/env
  • pushState / replaceState / invalidateAll and old goto options migrated
  • resolve() and asset() calls checked for leading slashes, base / assets removed
  • src/env.ts created and $env/* imports moved to $app/env/public / private
  • handleError filters expected errors before reporting, error() calls use a string message
  • json() / text() replaced with Response.json() / new Response()
  • Cookies, external redirects and param matchers reviewed
  • Adapter notes applied (Vercel edge, ORIGIN, Cloudflare bindings)
  • Production build passes, key pages checked by hand on a preview deploy

Conclusion

Don’t think of SvelteKit 3 as a new framework. This release is more of a cleanup. SvelteKit is finally dropping the baggage it carried for years because of Svelte 4 and older tooling. Runes replace stores, Node’s own import system replaces $lib, svelte.config.js moves into the Vite config, and errors thrown during rendering finally get caught.

Most of the work is mechanical, and the codemod handles a good share of it. The parts that really need your attention are where behaviour changes. Which errors will you report? Which goto calls might now throw? Do your cookies and redirects still work the way they did? Does your adapter rely on something that was removed? My advice: work through the checklist in order on a separate branch, and always try it on a preview deploy before it goes to production.

Stuck on your own migration? Every project hits its own snags: a plugin that won’t play nice with Rolldown, an alias that refuses to resolve, a deploy that only fails in production. If you run into a problem like that, get in touch. Tell me what you’re seeing and I’ll help you get past it. I also take on full SvelteKit 3 migrations and performance work.

Frequently Asked Questions

Is SvelteKit 3 stable?

SvelteKit 3 entered the Release Candidate phase on August 13, 2026. The Svelte team has said no further breaking changes are planned before the stable release, so migrating now is low risk for most apps, but you should test thoroughly before shipping to production.

How do I upgrade from SvelteKit 2 to SvelteKit 3?

First upgrade to the latest SvelteKit 2.x and fix any deprecation warnings. Then run npx sv@next migrate sveltekit-3, which rewrites most of your code automatically and generates a TODO list for the rest. Finally, bump Node to 22.17+, TypeScript to 6, Svelte to 5.57.1+, Vite to 8 and vite-plugin-svelte to 7.

Is svelte.config.js still supported in SvelteKit 3?

No. All configuration moves into the sveltekit() plugin call inside vite.config.js or vite.config.ts. Options that used to live under kit.* become top-level plugin options, and Svelte options such as compilerOptions and preprocess sit alongside them.

Why was $lib replaced with #lib?

SvelteKit 3 relies on Node’s native subpath imports, which Vite and TypeScript already understand. You declare #lib in the imports field of package.json, and SvelteKit no longer has to coordinate a custom alias between tools. The catch is that imports must include a file extension, for example #lib/utils/index.js instead of $lib/utils.

What replaces $app/stores in SvelteKit 3?

$app/stores is removed. Import page, navigating and updated from $app/state instead, and drop the $ prefix, so $page.url becomes page.url. Note that page.url is now read-only, so copy it with new URL(page.url.href) before mutating it.

Are $env/static/public and $env/static/private removed?

They are deprecated in SvelteKit 3 and scheduled for removal in SvelteKit 4. The replacement is explicit environment variables declared in src/env.ts with defineEnvVars and imported from $app/env/public or $app/env/private, with optional Standard Schema validation.

Do remote functions become stable in SvelteKit 3?

Not yet. Remote functions are still behind the experimental.remoteFunctions flag in SvelteKit 3, together with compilerOptions.experimental.async. Some of their behaviour did change, for example query functions can no longer read event.url or event.params.

entr