Skip to main content

Advanced

How It Works

The request-handling pipeline behind the admin panel — introspection, routing, and server-rendered views.


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

  1. Schema introspection. On startup, the Prisma schema at prismaSchemaPath is 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).
  2. Routing. Each incoming request path under basePath is matched against the model/action it targets (dashboard, list, create, edit, delete) using the models discovered in step 1.
  3. Data access. Reads and writes go through a data layer that wraps your Prisma client — applying model configuration (hidden, readonly, listFields, relation where scoping), coercing values to the correct types, and building where clauses safely from query parameters.
  4. Rendering. Pages (dashboard, list, form, not-found) are real Svelte components, server-rendered to HTML for each request — not static templates.

Routes handled

Route Behavior
/admin Dashboard with per-model statistics
/admin/[model] List view, with pagination
/admin/[model]/new Create form
/admin/[model]/[id] Edit form for that record

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()) / @updatedAt timestamps, anything in models[].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.