Skip to main content
A Pylon session is an opaque 256-bit token (prefixed pylon_) that maps to a Session { token, user_id, expires_at, device, created_at, tenant_id }. Tokens flow either as Authorization: Bearer <token> headers or HttpOnly cookies — both resolve through the same SessionStore. There’s no JWT, no signing key to rotate, no refresh-token dance. Just opaque strings you can revoke.

Session shape

Defaults

Sessions live in memory by default — fine for development but users get logged out on every restart. For production, point Pylon at a SQLite file:
Now sessions survive restarts, deploys, and crashes. The SQLite file is a write-through cache — reads still hit memory, every save/remove writes to disk. On Pylon Cloud, persistent sessions are configured automatically.

Refresh

Rotate a session’s token without breaking the user’s sign-in:
Response:
The old token is revoked; the new token has a fresh 30-day lifetime. Use this on long-running clients to keep sessions alive (the Swift SDK has startSessionAutoRefresh(intervalSeconds:) that does this automatically).

Revoke

Sign out the current device

Response:
Also clears the auth cookie (sets Set-Cookie with an expired value).

Sign out everywhere

Response:
Useful when a user changes their password or you suspect account compromise.

List active sessions

Response:
The full token is never returned — only the first 8 chars for display. Power users can use this to audit active sign-ins and revoke individual ones.

Cookies vs bearer tokens

Pylon supports both transports for the same Session. Pick based on client type: To enable cookie auth:
The cookie name is pylon_session. It’s automatically set on:
  • /api/auth/magic/verify success
  • /api/auth/password/login success
  • /api/auth/password/register success
  • /api/auth/callback/:provider success (both GET and POST)
And cleared on /api/auth/session DELETE. When both a cookie and a Authorization: Bearer header are present on the same request, the bearer header wins — explicit beats implicit.

Multi-tenant: switching organizations

For apps with workspaces/orgs, attach a tenant_id to the session so policies like data.orgId == auth.tenantId can run automatically:
The server verifies membership before committing — looks up an OrgMember { userId, orgId } row and returns 403 NOT_A_MEMBER if it doesn’t exist. Clients can’t impersonate an org they don’t belong to. Pass null to leave the org (drop back to the lobby):
After select-org, every request resolves to an AuthContext with tenantId set, and your row-scoped policies see it as auth.tenantId.

Guest sessions

For pre-login state (cart contents, theme preference, anonymous draft), mint a guest session:
Response:
Guests have a stable user_id (so their cart persists across page loads) but is_authenticated() returns false — AuthMode::User rejects them, so guests can’t access user-only routes. When the user signs in for real, upgrade the guest session in place:
The session token stays the same — the client doesn’t need to re-store it. /api/auth/upgrade is admin-gated and exists for backfill scripts; normal upgrade should flow through magic-code verify or OAuth callback, which mint a fresh user session and consume the guest token.

Programmatic session creation (admin only)

In dev mode or with admin auth, mint a session for any user:
This is the back door — never expose it without admin auth. In non-dev, non-admin requests get 403 FORBIDDEN. Use for:
  • Backfill scripts that need to sign in as a user
  • Tests
  • Impersonation features for support agents (gate on auth.hasRole('support') in your wrapper)

Sweep expired sessions

Background cleanup runs automatically — every authenticated request checks expires_at and removes the session if expired. For the SQLite-backed store, you can also trigger an explicit sweep:
Cloud runs this hourly. Self-hosted, the on-demand check is usually enough — there’s no harm in leaving expired rows in the table briefly.

Session storage backends

The SessionStore accepts a pluggable SessionBackend:
Pylon ships:
  • In-memory — default; lost on restart
  • SQLitePYLON_SESSION_DB=path enables it
Custom backends (Redis, DynamoDB, etc.) are a few lines of Rust — implement the trait, pass via SessionStore::with_backend. See crates/runtime/src/session_backend.rs for the SQLite reference impl.

Security defaults

  • Tokens are 256-bit CSPRNG — un-guessable
  • Constant-time token lookup — no timing leak on session resolution
  • HttpOnly cookies — JS can’t read the cookie via XSS
  • Secure cookies in non-dev — refused over plain HTTP
  • SameSite=lax — CSRF-resistant by default; switch to strict if you don’t have cross-site sign-in flows
  • Sessions can be revoked individually or en masse — no JWT-style “until expiry, can’t kill” problem
  • /me returns the runtime-resolved context, not a fresh DB lookup — admin-token requests show as admin even though they don’t have a session row