Sessions
GuildGate sends an opaque session token to the browser and stores only its hash. The CSRF token is bound to the same session identity.
Creating a session
const created = await gate.createSession({
userId: discordUser.id,
locale: "tr",
metadata: {
loginMethod: "discord",
},
});
reply.header("Set-Cookie", created.setCookie);
The result contains:
session: the record written to the storesessionToken: the opaque value sent only to the clientcsrfToken: the token used by state-changing requestssetCookie: the secure cookie header
Time limits
security.session contains four bounds:
session: {
ttlMs: 7 * 24 * 60 * 60_000,
idleTimeoutMs: 30 * 60_000,
rotateAfterMs: 15 * 60_000,
maximumSessionsPerUser: 5,
}
ttlMs: absolute expiryidleTimeoutMs: expiry after inactivityrotateAfterMs: token rotation pointmaximumSessionsPerUser: active-session cap for one user
Do not rely only on cookie expiry; validate the store record.
Atomic per-user cap
In 1.1.0, the official memory, Redis, and PostgreSQL stores insert a session and evict excess sessions inside one store operation.
Implement this signature for a shared custom store:
interface SessionStore {
create?(record: SessionRecord, maximumSessionsPerUser: number): Promise<void>;
}
Calling listByUser() followed by separate delete() operations creates a race in multi-instance deployments. The kernel keeps a compatibility path when create() is absent, but an atomic implementation is preferred for production stores.
Rotation
When the session reaches its rotation threshold, action metadata may contain replacement session and CSRF tokens. The HTTP adapter must write the replacement cookie and the client must use the new CSRF value.
Do not log the old token or persist it in frontend state. The session cookie should be HttpOnly.
Revocation
await gate.revokeSession(sessionToken);
const revokedCount = await gate.revokeUserSessions(userId);
Revoking all sessions for a user is useful after account-security events, owner intervention, or OAuth credential changes.
Use a revocation bus for realtime transports so open connections for the same user close across workers.
Cookie configuration
The production default uses __Host-guildgate.sid. A __Host- cookie requires:
SecurePath=/- no domain attribute
SameSite may be Lax or Strict. Test the OAuth return and application routes before selecting it.
Clearing an expired cookie
In 1.1.0, an invalid session resolution adds clearSessionCookie metadata. The official Fastify, Express, and Hono adapters send a cookie with Max-Age=0.
A custom adapter that ignores this metadata causes the browser to keep sending an invalid cookie on every request.
Production checks
- Is the session token transported only over HTTPS?
- Does the store contain any raw token?
- Is the Redis or PostgreSQL session cap atomic?
- Is a rotation result written to the correct cookie?
- Do logout, owner revocation, and expiry close realtime connections?
- Does audit use
sessionIdHashrather than the token?