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
| Setting | Default |
|---|---|
| Lifetime | 30 days |
| Token entropy | 256 bits (CSPRNG) |
| Storage | In-memory |
| Cookie HttpOnly | Yes when cookies enabled |
| Cookie SameSite | lax |
| Cookie Secure | true in non-dev mode |
Refresh
Rotate a session’s token without breaking the user’s sign-in:startSessionAutoRefresh(intervalSeconds:) that does this automatically).
Revoke
Sign out the current device
Set-Cookie with an expired value).
Sign out everywhere
List active sessions
Cookies vs bearer tokens
Pylon supports both transports for the sameSession. Pick based on client type:
| Transport | When |
|---|---|
| Bearer header | SPAs, native apps, server-to-server, curl |
| HttpOnly cookie | Multi-page web apps, server-rendered apps where XSS resistance matters |
pylon_session. It’s automatically set on:
/api/auth/magic/verifysuccess/api/auth/password/loginsuccess/api/auth/password/registersuccess/api/auth/callback/:providersuccess (both GET and POST)
/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 atenant_id to the session so policies like data.orgId == auth.tenantId can run automatically:
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):
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: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:
/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: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 checksexpires_at and removes the session if expired. For the SQLite-backed store, you can also trigger an explicit sweep:
Session storage backends
TheSessionStore accepts a pluggable SessionBackend:
- In-memory — default; lost on restart
- SQLite —
PYLON_SESSION_DB=pathenables it
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
HttpOnlycookies — JS can’t read the cookie via XSSSecurecookies in non-dev — refused over plain HTTPSameSite=lax— CSRF-resistant by default; switch tostrictif 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
/mereturns the runtime-resolved context, not a fresh DB lookup — admin-token requests show as admin even though they don’t have a session row