
# Sessions

When a user authenticates, Sovrium issues a **server-managed session** (an `httpOnly` cookie backed by a row in the `auth.session` table). Sessions are inspectable, listable, and revocable through the auth API, and — for multi-tenant apps — augmented by an **active-scope session API** that lets a user choose which assignment is currently in context.

## Session Configuration

Core session behavior (lifetime, refresh window) is managed at the **Better Auth server level**, not in the app schema. There is no `auth.session` config block.

```yaml
auth:
  strategies:
    - type: emailAndPassword
  # Session lifetime/refresh are Better Auth server defaults — not schema fields.
```

| Concern                    | Where it lives                                                                      |
| -------------------------- | ----------------------------------------------------------------------------------- |
| Session lifetime / refresh | Better Auth server defaults (not exposed in `app.auth`).                            |
| Signing & encryption       | `AUTH_SECRET` environment variable. See [Environment Variables](/en/docs/env-vars). |
| Cookie security            | `secure` cookies in production; derived from `BASE_URL` / `NODE_ENV`.               |

:::callout
Rotating `AUTH_SECRET` invalidates every active session — all users are signed out. Treat it as a credential and rotate deliberately. See [Security Hardening](/en/docs/security-hardening).
:::

## Inspecting & Ending Sessions

The following endpoints are mounted automatically when `auth` is configured.

| Method | Endpoint                          | Purpose                                                       |
| ------ | --------------------------------- | ------------------------------------------------------------- |
| `GET`  | `/api/auth/get-session`           | Returns the current session and user, or a null body if none. |
| `GET`  | `/api/auth/list-sessions`         | Lists all active sessions for the authenticated user.         |
| `POST` | `/api/auth/sign-out`              | Invalidates the current session (idempotent — always `200`).  |
| `POST` | `/api/auth/revoke-session`        | Revokes a specific session by id.                             |
| `POST` | `/api/auth/revoke-other-sessions` | Revokes every session except the current one.                 |

Behavior worth noting:

- `get-session` returns a **null body** (not an error) when unauthenticated or after sign-out, so a client can safely poll it to render auth state.
- `list-sessions` is **session-isolated** — a user only ever sees their own sessions, each with `id`, `userId`, creation, and expiration timestamps.
- `sign-out` is **idempotent**: calling it without a session, or repeatedly, still returns `200`.

## Active-Scope Sessions (Multi-Tenant)

Apps that scope data per tenant use the `user_access` junction: a user can have multiple access rows for the same table (e.g. a fractional CFO assigned to three client companies). The **active-scope session API** lets that user pick which assignment is currently active, persisted in a per-table cookie and resolved by `$currentUser.activeAssignment`.

First, declare the scope tables:

```yaml
auth:
  strategies:
    - type: emailAndPassword
  roles:
    - name: customer-admin
  scopeTables: [clients]
  landingPath: /portal
  # No extra config — active-scope endpoints auto-mount for every scopeTables entry.
```

| Property      | Description                                                                                                                                                                                                |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `scopeTables` | Array of table slugs (from `app.tables[].name`) that `user_access` rows may reference. Non-empty; entries must be valid slug identifiers; no duplicates. Validated against `app.tables[].name` at startup. |

Then the engine auto-mounts these endpoints for each entry in `scopeTables`:

| Method   | Path                                   | Behavior                                                                                                                                   |
| -------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `POST`   | `/api/session/active-scope/:tableSlug` | Body `{ recordId }`. Sets the active-scope cookie when `recordId` is in the user's `user_access`; returns `403` otherwise (no cookie set). |
| `GET`    | `/api/session/active-scope/:tableSlug` | Returns `{ tableSlug, recordId }` from the cookie, or `null` when unset.                                                                   |
| `DELETE` | `/api/session/active-scope/:tableSlug` | Clears the cookie (returns `204`).                                                                                                         |

A `:tableSlug` that is not in `scopeTables` returns `404`.

### Security Model

The active-scope cookie is named `sovrium_active_assignment_<tableSlug>` (one per scope table).

1. **Cookie attributes** — `httpOnly`, `sameSite=lax`, and `secure` in production. Not readable by client JavaScript.
2. **Every write is server-validated** — `POST` verifies the `recordId` is in the user's `user_access` rows for that slug. Out-of-scope writes return `403` with no state change.
3. **Every read is server-validated** — when resolving `$currentUser.activeAssignment`, the engine re-checks the cookie against `user_access`. Tampered, stale, or now-revoked values are discarded and the resolver falls back to the user's first accessible record.

:::callout
**Tamper-proof by construction.** A user cannot edit the cookie to reach a record outside their assignments — the value is re-validated against `user_access` on every request, both when set and when read. This makes context-switching safe without re-authenticating.
:::

`$currentUser.activeAssignment` is consumable in `dataSource.filter` predicates and row-level permission `when` clauses, so every assignment-bound view refreshes automatically when the active scope changes. The related `$currentUser.assignments.<table>` token (the full set of a user's assignments) is covered in [Post-Login Landing](/en/docs/auth-post-login).

## Related Pages

- [Authentication Overview](/en/docs/auth-overview) — enabling auth and the `auth` block.
- [Post-Login Landing](/en/docs/auth-post-login) — `$currentUser.assignments` interpolation and per-role landing.
- [Table Permissions](/en/docs/table-permissions) — row-level `when` predicates that consume `$currentUser`.
- [Security Hardening](/en/docs/security-hardening) — `AUTH_SECRET` rotation and cookie hardening.
