SvelteKit skill for Codex
Server loads, real forms, the right adapter. Codex places each piece of SvelteKit code in the right route file, loads data in parallel with streaming for slow parts, and writes form actions that validate input, check ownership and keep working with JavaScript off. Secrets stay behind $env private modules and $lib/server, auth is enforced in every server load and action rather than one layout, and the adapter matches the real host.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- sveltekit Installs to ~/.agents/skills/sveltekit/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install sveltekit The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project sveltekit Installs to .agents/skills/sveltekit/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/sveltekit.zipunzip -o sveltekit.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) sveltekit PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $sveltekit, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds SvelteKit 2 apps - filesystem routes with +page, +layout and +server files, universal versus server load functions, form actions with use:enhance and fail, hooks for auth, $app/state, invalidation with depends, page options for prerender and ssr, environment modules, and choosing adapter-node, adapter-static, adapter-vercel or adapter-cloudflare. Use when adding routes, loading data, handling form submissions, protecting pages, fixing waterfalls or leaked secrets, or preparing a SvelteKit app for deployment.
Use it when
- Adding routes, layouts, endpoints or load functions to a SvelteKit app
- Handling form submissions with validation and progressive enhancement
- Protecting pages and actions behind authentication
- Choosing or changing a deployment adapter
Use something else when
- The question is component reactivity, runes or Svelte 4 migration: use svelte-5-runes
How to check the result
svelte-check and the production build pass, every form works with JavaScript disabled, and no client file imports a private env module or $lib/server.
The mistake it prevents
Checking authentication only in a parent layout load, while child server loads and form actions run independently and stay open to direct requests.
Done well means: data loads in parallel on the server where it can, secrets never leave
+page.server.ts / $lib/server, every mutation is a form action or +server.ts
endpoint that validates input and checks auth, forms work without JavaScript and enhance
with it, and the adapter matches where the app actually runs.
Inspect first
- Read
AGENTS.md,package.json(@sveltejs/kit2.x,svelte5.x), andsvelte.config.js(adapter,kit.alias,kit.experimental,prerendersettings). - Read
src/hooks.server.tsfor auth/session handling andsrc/app.d.tsforApp.Locals,App.PageData,App.Errortypes. - Map the route tree:
find src/routes -name '+*' | sort.
Route files
| File | Runs | Use for |
|---|---|---|
+page.svelte | server (SSR) + browser | UI |
+page.ts | server on first load, browser on navigation | Public API fetches, non-serializable data (components, classes) |
+page.server.ts | server only | DB, secrets, cookies, form actions |
+layout(.server).ts | as above, for all child routes | Shared data (user, nav) |
+server.ts | server only | JSON/REST endpoints, webhooks, files (GET, POST exports) |
+error.svelte | Error UI for the subtree | |
(group)/ | Layout grouping without a URL segment | |
[slug], [[optional]], [...rest], [id=integer] | Params, with matchers in src/params/ |
Load functions
// src/routes/projects/[id]/+page.server.ts
import { error } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
import { db } from "$lib/server/db";
export const load: PageServerLoad = async ({ params, locals, depends }) => {
if (!locals.user) error(401, "Sign in required");
depends("app:project");
const project = await db.project.find(params.id, locals.user.id);
if (!project) error(404, "Project not found");
return {
project, // awaited: needed for first paint
activity: db.activity.recent(project.id), // not awaited: streamed to the page
};
};
<!-- +page.svelte -->
<script lang="ts">
let { data } = $props();
</script>
<h1>{data.project.name}</h1>
{#await data.activity}
<p>Loading activity...</p>
{:then items}
{#each items as a (a.id)}<p>{a.text}</p>{/each}
{:catch}
<p>Could not load activity.</p>
{/await}
Rules:
- In v2,
error()andredirect()throw by themselves; do not writethrow error(...)in new code, and never call them inside atryblock that swallows them. - Avoid waterfalls: start independent queries together (
Promise.all) and do notawait parent()unless you need its data; when you do, call it after starting your own fetches. - Use the
fetchpassed toload, not the global one: it forwards cookies, works with relative URLs on the server, and inlines responses into the HTML so the browser does not refetch. - Returned data from
+page.server.tsmust be serializable by devalue (no functions or class instances). Dates, Maps, Sets and BigInt are fine. - Layout loads rerun when their dependencies change; a layout that reads
urlreruns on every navigation. Read only what you need.
Form actions
// +page.server.ts
import { error, fail, redirect } from "@sveltejs/kit";
import { z } from "zod";
import type { Actions } from "./$types";
const Rename = z.object({ name: z.string().trim().min(1).max(80) });
export const actions: Actions = {
rename: async ({ request, params, locals }) => {
if (!locals.user) error(401);
const raw = Object.fromEntries(await request.formData());
const parsed = Rename.safeParse(raw);
if (!parsed.success) {
return fail(400, { name: String(raw.name ?? ""), errors: z.flattenError(parsed.error).fieldErrors });
}
await db.project.rename(params.id, locals.user.id, parsed.data.name);
return { saved: true };
},
delete: async ({ params, locals }) => {
if (!locals.user) error(401);
await db.project.delete(params.id, locals.user.id);
redirect(303, "/projects");
},
};
<script lang="ts">
import { enhance } from "$app/forms";
let { data, form } = $props();
</script>
<form method="POST" action="?/rename" use:enhance>
<input name="name" value={form?.name ?? data.project.name} aria-invalid={!!form?.errors?.name} />
{#if form?.errors?.name}<p role="alert">{form.errors.name[0]}</p>{/if}
<button>Save</button>
</form>
- Without JS the form posts and the page re-renders with
form; withuse:enhanceit updates without reload and rerunsloadfunctions automatically. - Return user input in
fail()so fields keep their value; never return passwords. - Customize enhance with a callback (
use:enhance={() => async ({ update }) => { await update({ reset: false }) }}) for optimistic UI or to keep input. - Actions are POST endpoints anyone can call. Check
locals.userand ownership in each one. - SvelteKit rejects cross-origin form posts by default (CSRF check via
Origin); do not disablecsrf.checkOriginto fix a proxy problem, setORIGIN/ trusted origins instead.
Remote functions (query, form, command in .remote.ts files) are experimental
behind kit.experimental.remoteFunctions. Use them only if the project already opted in.
Hooks and auth
// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
event.locals.user = await getUserFromSession(event.cookies.get("session"));
return resolve(event);
};
Protect data in each server load and action, not only in a layout: a +page.server.ts
load runs in parallel with its parent layout and does not wait for the layout’s check
unless it calls await parent().
Client state and navigation
import { page, navigating } from "$app/state"(Kit 2.12+, runes-based) replaces$app/stores.invalidate("app:project")reruns loads that calleddepends("app:project");invalidateAll()reruns everything. Prefer the targeted form.goto(url, { replaceState: true, noScroll: true })for filter changes; keep filters inurl.searchParamsso they survive reload and sharing.
Environment and page options
| Module | Contains | Usable in |
|---|---|---|
$env/static/private | build-time secrets | server files only |
$env/dynamic/private | runtime secrets | server files only |
$env/static/public / dynamic/public | PUBLIC_* vars | anywhere |
Importing a private module or $lib/server/* from client code fails the build; that is
the guard, do not work around it.
Page options exported from +page.ts/+layout.ts: prerender = true | 'auto',
ssr = false (SPA section), csr = false (no JS), trailingSlash. Prerendered pages
cannot use form actions or per-request data.
Adapters
| Target | Adapter |
|---|---|
| Node server, Docker | @sveltejs/adapter-node (node build, set ORIGIN, PORT) |
| Fully static site or SPA | @sveltejs/adapter-static (all routes prerendered, or fallback: '200.html') |
| Vercel | @sveltejs/adapter-vercel |
| Netlify | @sveltejs/adapter-netlify |
| Cloudflare Workers or Pages | @sveltejs/adapter-cloudflare |
| Unsure / prototype | adapter-auto (replace before production) |
Switching adapters changes runtime APIs (Node built-ins, file system, request size limits). Deploying is outward-facing: build and preview locally, then ask before running any deploy command.
Verification
npx svelte-kit sync && npx svelte-check --threshold warning
npm run build && npm run preview
npx playwright test # if configured; test forms with JS disabled too
- Disable JavaScript and submit each form: it must still work.
- Network tab: no duplicate fetch after hydration for data loaded through
load’sfetch. grep -rn "\$env/static/private\|\$lib/server" src/routes --include=*.sveltereturns nothing.
Output
List routes added or changed with their load and action files, auth checks per action,
page options, env vars required, and the adapter plus any deploy settings (ORIGIN,
body size limits).
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "SvelteKit"
short_description: "Server loads, real forms, the right adapter"
default_prompt: "$sveltekit Add a project detail page with a server load and a rename form action" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.