# Auth (/product/infrastructure/auth)



## The three layers

1. **The proxy.** A cookie-shape check that redirects a signed-out visitor to sign-in before the request reaches a server component. It verifies nothing; it exists so a signed-out admin shell never hydrates.
2. **The verified session.** A gate over the whole admin tree reads and cryptographically verifies the session cookie, and every server action does the same on entry. This is the authentication boundary.
3. **Row-level security.** Even a verified user can only read and change the rows the database policies allow. This is the authorization boundary.

Each layer can refuse independently. The proxy is never trusted alone.

## Identity

Each tenant has its own Neon Auth instance, a managed Better Auth, with its own signing keys. A user signs in on the tenant's host, the auth instance issues a token signed by that tenant's keypair, and a cookie carries the session. Because the auth instance runs on Neon's domain and the app on its own, a proxy route on the app's origin bridges them so the session cookie lands on the app's domain.

Users live in the tenant's database, so the platform's own tables can join against them: onboarding, the pending-admin handoff, and the claim flow that hands a store from the console to its owner all work on local rows.

The sign-in page is kikstart's own, rendered on the tenant's host under the tenant's theme tokens, so logo, colour and copy are already tenant-controlled.

## From session to query

1. A server action or component asks for a data client. The client attaches the session's token to each request.
2. Neon verifies the token against the tenant's keys and switches the connection to the `authenticated` database role.
3. The database reads the caller's identity from the verified token claims, never from anything the app forwarded.
4. Row-level policies read that identity and decide which rows are visible and writable.

The app never tells the database who the user is. Neon does.

## Where the checks go

Server actions call the session check on entry, because an action is its own entry point and the page gates never ran for it. Admin pages do not: every admin route renders inside the route gate, so a second check there can never fire and only costs a second cookie read that keeps the page out of its static shell.

API route handlers answer a signed-out caller with a 401 rather than a redirect to a sign-in page.

## Roles and permissions

A user holds one role. What is plural is the role's capabilities: a set of permission codes, each `<resource>.<verb>`. Every resource has a `manage`; only resources whose reads are actually gated carry a `read`.

The database resolves a session's role to its permission codes, and every resource-gating policy compares one. An admin passes because the admin role holds every permission, not because the check special-cases it. Only the role ladder itself, assigning roles and guarding the last admin, still asks whether the caller is an admin, so a permission grant can never widen the set of permissions.

The same permission codes gate the admin sidebar and the routes behind it from one table, so a typed URL cannot reach a page the sidebar would hide. The roles screen edits a role's permission set; it revokes before it grants, so a failure between the two leaves fewer permissions, never more.

Two roles ship with every tenant: `admin` and `user`. A business process maps onto those. A field manager is an admin. More roles are supported but are downstream custom work.

## Signing in without a human

A test harness signs in with one credential pair, an admin of the tenant under test. Playwright posts to the sign-in API and stores the session state. A browser session takes a development-only route that signs in server-side and redirects with the cookies set, so the password never leaves the server. That route answers 404 in production or when the credentials are unset.
