Febasi Docs
Concepts

Sessions

Refresh tokens, rotation, atomic session limits, and the four reasons a session can end.

A session is the lifetime of a refresh token. Every successful /login opens one; every /logout closes one; rotations replace one without ever leaving a gap. The Auth API tracks every session it has ever opened and records why each one ended.

Anatomy of a refresh token

The refresh token returned to the client is a UUID. The Auth API stores only its SHA-256 hash (not bcrypt — these tokens are already high-entropy). The row also captures:

FieldPurpose
idUUID. Same value as the token returned to the client.
user_idOwner of the session.
tenant_idTenant scope.
token_hashSHA-256 hash for verification.
expires_atDefault 7 days; configurable per tenant.
revoked_atTimestamp when the session was closed. NULL when active.
revoke_reasonOne of USER_LOGOUT, TOKEN_ROTATION, AUTOMATIC_SESSION_LIMIT, MANUAL_REVOKE.
ip_addressThe client's IP at session creation. Useful for audit.
user_agentThe client's user-agent at session creation.

Session limits — why they're atomic

Each tenant configures a max-concurrent-sessions value. When a user logs in and is already at the limit, the Auth API performs the following inside a single transaction with SELECT FOR UPDATE:

Lock the user's active sessions

SELECT * FROM refresh_tokens
WHERE user_id = $1 AND revoked_at IS NULL AND expires_at > NOW()
FOR UPDATE

Count + decide

If the count is below the tenant limit, do nothing. If it's at or above, collect the oldest session(s) so the new one fits.

Revoke the oldest, mint the new

UPDATE refresh_tokens
SET revoked_at = NOW(), revoke_reason = 'AUTOMATIC_SESSION_LIMIT'
WHERE id IN (oldest_ids);

INSERT INTO refresh_tokens (...) VALUES (...);

This happens in the same transaction, so there is never a moment where the user has more than the configured limit of active sessions.

If the limit is configured strict enough that the user is rejected instead of being downgraded, the Auth API returns SESSION_LIMIT_EXCEEDED (HTTP 429) with the current and max counts in the payload.

Token rotation

Every call to /refresh rotates the token:

old refresh token  →  marked revoked (reason: TOKEN_ROTATION)
new refresh token  →  inserted, returned to client
new access token   →  generated from the freshest user state

The client must always replace its stored refresh token with the latest one. Reusing a revoked token is never accepted.

Why this matters

  • Stolen-token blast radius. A leaked refresh token is invalid the moment the legitimate client refreshes — token rotation cuts the attacker off.
  • Real-time logout. Revocation is immediate. The next refresh fails; outstanding access tokens still work until they expire (max 15 minutes by default), which is the cost of stateless JWTs.
  • Forensics. Every revoke_reason and revoked_at is preserved for audit.

How to revoke a session manually

There is no direct "revoke a single refresh token by id" endpoint exposed yet — closing a session goes through one of:

  • POST /api/v1/logout with the refresh token (reason: USER_LOGOUT).
  • A new login that bumps the oldest one (reason: AUTOMATIC_SESSION_LIMIT).
  • An admin operation against a tenant via internal tooling (reason: MANUAL_REVOKE). This is audited and exposed to consumers via the audit log.

Configuration knobs

These are stored in the tenant's auth_config:

KnobDefaultWhat it controls
accessTokenExpiresIn15mJWT exp window for access tokens.
refreshTokenExpiresIn7dLifetime of refresh tokens.
maxConcurrentSessions5Sessions allowed per user before rotation kicks in.

Update them via PATCH /api/v1/tenants/{id}/config. Existing sessions are not retroactively shortened — the new value applies to sessions created after the change.

On this page