The admin handler intercepts every request under basePath (default /admin) and generates HTML on the fly. There is no SvelteKit routing
involved — no route files to create, nothing to register beyond the single
hook from Installation.
Pipeline
- Schema introspection. On startup, the Prisma schema at
prismaSchemaPathis parsed to discover every model, its fields, their types and attributes (@id,@unique,@default,@updatedAt), and — in a second pass — the relation graph between models (which fields are foreign keys, which relations are one-to-many vs. many-to-many, and so on; see Relations). - Routing. Each incoming request path under
basePathis matched against the model/action it targets (dashboard, list, create, edit, delete) using the models discovered in step 1. - Data access. Reads and writes go through a data layer that wraps
your Prisma client — applying model configuration (
hidden,readonly,listFields, relationwherescoping), coercing values to the correct types, and buildingwhereclauses safely from query parameters. - Rendering. Pages (dashboard, list, form, not-found) are real Svelte components, server-rendered to HTML for each request — not static templates.
Routes handled
Any request path that doesn’t match one of these shapes renders a not-found page rather than falling through to your app’s own routing.
Plugins may register additional path patterns (for example /admin/user/1/graph). Those pages still render inside the same layout;
see Plugins.
When a write is refused
A successful create, update, or delete answers with a 303 redirect back to
the list. A refused one answers with 422 and re-renders the same form,
carrying the values that were just submitted — a create form is not emptied,
and an edit form is not reset to the row as it still stands in the database.
The values shown are the raw ones the browser sent, not the coerced payload: abc typed into an Int field is shown back as abc, so the mistake stays
visible instead of being silently replaced by an empty field. An unchecked
checkbox stays unchecked, and many-to-many boxes keep exactly the selection
that was submitted.
Two kinds of field are deliberately not repopulated:
- anything listed in
models[].hidden, and anything whose name looks sensitive (password,hash,secret,token) — the same rule that keeps those fields out of list views, search, filters and the audit payload. A password therefore has to be retyped after a refused submit. - read-only fields (the primary key,
@default(now())/@updatedAttimestamps, anything inmodels[].readonly) — these keep their stored value, since they were never editable.
When the refusal names a field, its message is rendered next to that field and
wired to it with aria-invalid and aria-describedby. When it doesn’t — a
unique-constraint conflict, whose column is not guessed from driver
metadata — the message appears in the page-level alert instead. Either way the
message is one the library wrote — a database driver’s own error text is never
rendered. See Model Configuration for hidden and readonly, and the Configuration Reference for scope.
Why a single hook instead of generated route files
Generating actual SvelteKit route files (or injecting them into config.kit.files.routes) would require build-time codegen tied to your
Prisma schema, and would need to re-run whenever the schema changes.
Intercepting requests in a handle hook means the admin always reflects
the current schema at request time, with zero build step and zero
generated files to keep in sync.