Server-minted sessions
Use server-minted sessions when your server already authenticates the
end user. You hold the bearer token, you know who’s logged in, and
you want to hand them a short-lived JWT they can use to call your
published /p/* endpoints — without ever shipping the bearer to the
browser or the mobile app.
If you instead want PerSQL to do the authentication (signup, login, password hashing), see Session auth.
When to use this
Section titled “When to use this”| Your auth story | Use |
|---|---|
| PerSQL stores users + passwords for you | auth_signup / auth_login endpoints |
| You have your own auth (NextAuth, Clerk, Better Auth, OAuth, custom) | POST /v1/sessions (this page) |
| You’re an agent / CI run / cron, not a per-end-user request | Use the bearer directly; no session needed |
Mint a session
Section titled “Mint a session”From your trusted server (Next.js route handler, Hono worker, Express endpoint — anywhere the bearer lives):
import { PerSQL } from "@persql/sdk";
const persql = new PerSQL({ token: process.env.PERSQL_TOKEN! });const db = persql.database("acme", "orders");
const session = await db.createSession({ userId: currentUser.id, email: currentUser.email, expiresIn: 3600,});
// Hand `session.token` to the untrusted client.return Response.json({ persqlSession: session.token, expiresAt: session.expiresAt,});curl -X POST https://api.persql.com/v1/sessions \ -H "Authorization: Bearer $PERSQL_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "namespaceSlug": "acme", "databaseSlug": "orders", "userId": "user_42", "email": "alice@example.com", "expiresIn": 3600 }'Response:
{ "success": true, "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expiresAt": "2026-05-16T15:30:00.000Z", "expiresIn": 3600 }}Using the session token
Section titled “Using the session token”The untrusted client passes the JWT as a bearer header when calling your published endpoints:
const res = await fetch("https://api.persql.com/p/acme/orders/list-mine", { headers: { Authorization: `Bearer ${persqlSession}` },});Endpoints marked auth: "session" will see $user_id bound to the
userId you minted the session with. Endpoints marked auth: "public"
ignore the session.
Parameters
Section titled “Parameters”| Field | Required | Notes |
|---|---|---|
namespaceSlug | yes | Must match the bearer token’s namespace. |
databaseSlug | yes | The session is scoped to this single database. |
userId | yes | Up to 256 chars. Becomes $user_id on session-bound endpoints. |
email | no | Up to 320 chars. Stored on the JWT as the email claim. |
expiresIn | no | Seconds. Default 3600 (1h). Min 60, max 86400 (24h). |
Authorization
Section titled “Authorization”POST /v1/sessions requires the bearer to have read access on the
target database under your scope rules (an unscoped token always
passes; a scoped token must include this database with role ≥ read).
The minted JWT inherits the database scope only — it cannot escalate beyond what the bearer can do.
Expiry and revocation
Section titled “Expiry and revocation”Sessions are stateless: the JWT validates against the database’s signing key, with no per-session row in the control plane. That means:
- Short TTLs are the primary revocation mechanism. Default 1h; cap 24h. Mint a fresh token on each page load / app launch if you need finer control.
- Logout-everywhere is supported by bumping
database.tokens_min_iat(a column on the database row). Any JWT whoseiatis older than that floor is rejected. The console surfaces this as “Revoke all sessions”. - Per-session revocation is not yet exposed via
/v1. If you need it, file an issue with your use case.
Security model
Section titled “Security model”- The bearer never leaves your server. Treat it like a database password.
- The JWT does leave your server but is scoped to one database and one user, with a hard expiry.
- Sessions don’t bypass token scopes,
rate limits, or the per-namespace balance gate. Every
/p/*call still bills the namespace that owns the database. - The
userIdis recorded in audit events alongside the bearer token id, so every mutation traces back to the end user.