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 }. Bearer headers and HttpOnly cookies both resolve through the same SessionStore. Because session tokens are opaque and server-resolved, they need no signing key and can be revoked immediately.

Session shape

Defaults

Sessions live in memory by default. That is 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, and every save or 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. The response shows only the first 8 chars for display. Power users can 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 an Authorization: Bearer header are present on the same request, the bearer header wins.

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. It looks up an OrgMember { userId, orgId } row and returns 403 NOT_A_MEMBER if it does not exist. Clients cannot impersonate an org they do not 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 cannot 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 admin-only path. Never expose it without admin auth. In non-dev mode, non-admin requests get 403 FORBIDDEN. Use it 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. Leaving expired rows in the table briefly does no harm.

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 and pass it 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