SvelteKit

Pathrule2 Rules • 4 Memories • 1 Skill

A pattern bundle for SvelteKit 2 apps built on Svelte 5 runes. It encodes where data loading belongs, how to keep secrets server-side, and how form actions and remote functions stay type-safe and progressively enhanced. Use it so AI agents stop leaking private values into the client bundle and stop reaching for stores when runes are the right tool.

Suggested path map

Pathrule places each piece on the matching path, so your assistant only sees it where it belongs. This is the scoping you get on import; you can adjust it in your workspace.

/ workspace root
sveltekit-review
src/
Svelte 5 runes are the default; reactive classes replace stores
Async-in-components is experimental; reads after await are not tracked
routes/
Server-only data and secrets live in +page.server.ts
Mutations go through form actions or remote functions, never load
Avoid load waterfalls; stream non-critical data
Remote functions are experimental and the API is still moving

Rules

2
Server-only data and secrets live in +page.server.ts/src/routesPut auth, DB access, and private env in server load; keep universal load for public, serializable data.
1Any data that needs secrets, cookies, a database, or private APIs belongs in a server load function, not a universal one.
2 
3- Use `+page.server.ts` / `+layout.server.ts` for `load` that touches `$env/static/private`, `$env/dynamic/private`, `cookies`, or `locals`.
4- Reserve `+page.ts` / `+layout.ts` (universal) for public external APIs and non-serializable returns like component constructors.
5- Never import anything from `$lib/server`, a `*.server.ts` module, or a `$env/.../private` module into universal load or any client-reachable code. SvelteKit treats the whole import chain as unsafe and Vite refuses to build it; that build error is the signal the logic is in the wrong file, not something to work around.
6- When both load functions exist on a route, the server load runs first and its result reaches the universal load via the `data` property. Universal output cannot flow back to the server.
7- Server load runs only on the server. Universal load runs on the server during SSR, then again in the browser on hydration and on client-side navigation, so it must never assume a server-only global is present.
Mutations go through form actions or remote functions, never load/src/routeshighadvisoryWrites belong in form actions or remote form/command; load is read-only and reruns on navigation.
1`load` functions are for reading and must stay side-effect free, because SvelteKit reruns them on navigation, invalidation, and SSR. A write placed in `load` will silently re-fire.
2 
3- Handle writes with named `actions` in `+page.server.ts` plus a `<form method="POST">`, or with remote `form` / `command` functions in a `.remote.ts` file.
4- Validate input server-side. In a form action, return `fail(status, data)` on validation errors and a serializable object on success.
5- Use the `enhance` action for progressive enhancement instead of a hand-rolled `fetch`, and call `invalidate('app:key')` / `invalidateAll()` to refresh affected `load` data after the mutation.
6- Do not perform POST/PUT/DELETE logic inside a `load`, and do not call a mutating remote `command` from a `load`.

Memories

4
Svelte 5 runes are the default; reactive classes replace stores/srcPrefer $state/$derived/$props; never use $effect to sync state; use reactive classes in $lib over writable stores.
1This codebase targets Svelte 5, so reactivity is rune-based, not store-based.
2 
3- Use `$state` for mutable reactive values, `$derived` (or `$derived.by`) for computed values, `$props` for component inputs, and `$effect` only as an escape hatch for genuine side effects (DOM, analytics, subscriptions).
4- Do not use `$effect` to synchronise or recompute state from other state. The docs are explicit: use `$derived` instead. Writing to state that the same effect reads also risks an infinite loop; reach for `untrack` only when you deliberately need to read without depending.
5- Reach for `$state` only when a value drives the UI. Plain `const` / `let` is cheaper and clearer for everything else.
6- For shared logic, write a reactive class in `$lib` whose fields use `$state` / `$derived`, and import it, instead of authoring `writable` / `readable` stores. Runes work inside plain `.svelte.ts` modules and classes.
7- Avoid `$:` reactive statements and the implicit let-is-reactive model from Svelte 4. They do not exist in runes mode.
Avoid load waterfalls; stream non-critical data/src/routesFire independent fetches before await parent(); return unawaited promises for slow, non-essential data.
1Load performance hinges on not serializing requests that could run in parallel.
2 
3- Start independent `fetch` calls before `await parent()` so they do not block on parent data they do not need.
4- Return unresolved promises from a server `load` for non-essential data. SvelteKit streams them to the client so the page renders before they settle, and the markup can `{#await}` them.
5- Attach `.catch()` to any streamed promise that does NOT come from SvelteKit's injected `fetch`. An unhandled rejection in a streamed promise can crash the response.
6- Use the injected `fetch` argument inside `load` (not global `fetch`) so SvelteKit forwards credentials and cookies, resolves relative URLs, inlines the response during SSR, and tracks the request as a dependency for `invalidate`. For custom clients that bypass `fetch`, call `depends('app:key')` to register a manual dependency.
Remote functions are experimental and the API is still moving/src/routesquery/form/command/prerender in .remote.ts behind two experimental flags; validate args with a Standard Schema; expect breaking churn.
1Remote functions (available since SvelteKit 2.27) are still experimental as of mid-2026. Treat their surface as unstable and pin against the SvelteKit version in this repo before copying examples.
2 
3- Enable them with BOTH flags: `kit.experimental.remoteFunctions: true` and `compilerOptions.experimental.async: true` in `svelte.config.js`. They live in `*.remote.ts` files and export `query` (read, deduped + cached), `form` (progressively-enhanced writes bound to `<form>`), `command` (writes from anywhere, not form-bound), and `prerender` (build-time static data).
4- Every function that takes an argument must validate it with a Standard Schema validator (Zod, Valibot) passed as the first parameter. Do not trust raw input.
5- Use single-flight mutations: inside a `form` / `command` handler, call `.refresh()` / `.set()` on affected queries so the mutation and the data refresh travel in one round-trip. Use `getRequestEvent()` for request context, and `query.batch` to collapse N parallel calls into one request and avoid n+1.
6- Watch the breaking churn: `.run()` was removed from remote queries (await the query directly); `enhance` callbacks now receive a copy of the form instance rather than `{ form, data, submit }`; `query.live()` is the async-iterable real-time subscription helper. Confirm exact signatures against the installed version, not blog posts.
Async-in-components is experimental; reads after await are not tracked/srcawait in script/$derived/markup needs experimental.async; reactive reads after await or in setTimeout are not tracked; use $effect.pending.
1Svelte 5 (since 5.36) lets you use `await` directly in component `<script>`, in `$derived`, and in markup, but it is experimental and must be opted into via `compilerOptions.experimental.async` in `svelte.config.js`. The flag is slated to be removed in Svelte 6.
2 
3- Reactive values read ASYNCHRONOUSLY are not tracked. Anything read after an `await`, inside a `setTimeout`, or in a `.then()` callback will not register as a dependency, so the effect or derived will not re-run when it changes. Read the reactive value synchronously first, then await.
4- Use `$effect.pending()` to know how many async operations are still settling in the current boundary (it does not count child boundaries) when you need loading UI.
5- Svelte holds the UI in a consistent state while an `await` that depends on reactive state is in flight, rather than flashing intermediate values; rely on that instead of manual loading flags where possible.
6- This is independent of SvelteKit `load` streaming. Prefer `load` + streamed promises for route data and reserve in-component `await` for leaf-level async that is local to one component.

Skills

1
sveltekit-review/rootPre-merge checklist for SvelteKit 2 routes: data boundaries, secrets, runes, mutations, and remote functions.
1---
2name: sveltekit-review
3description: Review a SvelteKit 2 (Svelte 5) change before merge - verify load placement, secret isolation, runes usage, form actions, remote functions, and load performance. Use when reviewing or authoring routes, load functions, form actions, remote functions, or shared $lib reactivity.
4---
5 
6# SvelteKit review
7 
8## Secrets and data boundaries
9- [ ] Secrets, DB access, `cookies`, and `locals` appear only in `+page.server.ts` / `+layout.server.ts`, never in universal load or client code.
10- [ ] No `$lib/server`, `*.server.ts`, or `$env/.../private` import reaches a `+page.ts`, `+layout.ts`, or component (the build would fail; if it builds, the chain is clean).
11- [ ] Universal `load` returns only serializable data or intentional non-serializable values (components/classes); server `load` returns serializable data only.
12 
13## Mutations
14- [ ] `load` functions are read-only; all writes go through `actions` or remote `form` / `command`.
15- [ ] Form actions validate input server-side and use `fail(status, data)` for errors; `<form>` uses `enhance`.
16- [ ] Data is refreshed after mutations via `invalidate` / `invalidateAll` (or remote single-flight `.refresh()` / `.set()`), not manual refetch.
17 
18## Reactivity
19- [ ] Reactivity uses runes (`$state`, `$derived`, `$props`); no `$:` statements or new `writable` / `readable` stores where a reactive class fits.
20- [ ] `$effect` is used only for true side effects, never to synchronise or recompute state that `$derived` should produce.
21- [ ] If async-in-components is used, no reactive value is depended on only after an `await` / in a `setTimeout`.
22 
23## Load performance
24- [ ] `load` uses the injected `fetch` argument; independent fetches start before `await parent()`.
25- [ ] Slow non-critical data is streamed via returned promises, with `.catch()` on any promise not from the injected `fetch`.
26 
27## Remote functions (if used)
28- [ ] Both `experimental.remoteFunctions` and `experimental.async` flags are set; functions live in `*.remote.ts`.
29- [ ] Every argument-taking remote function validates input with a Standard Schema (Zod / Valibot).
30- [ ] API usage matches the installed SvelteKit version (no removed `.run()`; correct `enhance` callback shape).

Why this pattern

AI agents leak private values into the client bundle and pick the wrong data-loading or reactivity primitive in SvelteKit.

Built for Teams shipping full-stack apps on SvelteKit 2 with Svelte 5.

Keeps your assistant from:

  • Importing $env/static/private or $env/dynamic/private into universal load or client code, leaking secrets into the bundle
  • Putting database queries and auth in +page.ts universal load instead of +page.server.ts
  • Reaching for writable stores where Svelte 5 runes ($state, $derived) are the correct primitive
License
Apache-2.0
Version
1.0.0
Updated
2026-06-09
View source