createAdminHandler takes a single configuration object. Only prisma is
required — everything else has a sensible default.
Full example
Options
Tenant scoping
Use scope when a model must be isolated by the current tenant. The function
receives the same request context as authCheck and must return a non-empty
condition. Flat equality maps are recommended because they can also be applied
automatically on create:
The scope is composed with AND for lists, search, dashboard counts, detail
views, relation options, plugin reads, update, and delete. A missing tenant or
an empty scope fails closed: a condition that matched every row would fail open
exactly when it matters most, so it throws instead.
On create and update, the scope’s equality fields are imposed on the record
after foreign-key validation has run — which matters because the tenant column
is usually a relation scalar such as organizationId. A value submitted for a
scope column that conflicts with the scope is rejected, not overwritten: it
is server-determined, so a mismatch means either a forged POST or a form
offering a choice it should not offer. A scope column absent from the form, or
present but left empty — what a create form renders — is simply set.
Complex filters such as OR are allowed for reads but rejected on create,
because they cannot determine a single tenant-owned value.
Put the tenant model itself in exclude. Foreign-key validation checks a
submitted target against that target model’s scope, so an unscoped, visible
tenant table makes every existing tenant id an acceptable value:
Migrating from listWhere
If you used listWhere for tenant isolation, move that condition to scope.
The two are not equivalent, and the difference is the whole point:
listWhere hides rows from the list. It never stopped anyone who knew a record
id from opening /admin/<model>/<id>, editing it, or deleting it. If that was
your tenant boundary, treat it as having been open and audit accordingly.
Keep listWhere for what it is good at: narrowing a list view for reasons that
are not authorization — a default “only my drafts” or “archived hidden” view.
The two compose with AND when both are set.
What scope does not cover
scope applies to the requests this handler serves — everything under basePath. It does not reach the rest of your SvelteKit app: a prisma.invoice.findMany() in one of your own +page.server.ts files is not
scoped by anything here. To isolate the whole application, enforce it at the
data layer instead — a Prisma Client extension, or PostgreSQL row-level
security.
Concurrency
Relation targets submitted by a POST are re-checked inside the write
transaction. On PostgreSQL that check takes a FOR SHARE row lock, because SERIALIZABLE alone does not stop a concurrent transaction from moving the
target out of scope between the check and the write. Writes are retried up to
three times on a serialization failure or deadlock, which are transient by
definition; nothing else is retried.
Routes handled
The handler intercepts every request under basePath and generates HTML
on the fly — there are no SvelteKit route files to create:
See How It Works for the request-handling pipeline behind these routes.