Session Management#
Dokploy's session lifecycle is managed by better-auth, configured in packages/server/src/lib/auth.ts. Sessions are persisted to PostgreSQL via a Drizzle ORM adapter and carry an activeOrganizationId to track the user's current organization context. The single betterAuth instance is created once per process and cached on globalThis .
Expiration and Renewal#
Sessions are configured with a 3-day absolute expiration and a 1-day updateAge:
session: {
expiresIn: 60 * 60 * 24 * 3, // 3 days absolute TTL
updateAge: 60 * 60 * 24, // slide expiration if active within 1 day
}
expiresIn(259,200 seconds): The maximum session lifetime. After this period the session is invalid regardless of activity.updateAge(86,400 seconds): better-auth extends the session'sexpiresAttimestamp if the session is used and its current expiration is withinupdateAgeof the present time. In practice this means daily-active users are rarely forced to re-authenticate.
Session Creation Hook#
When a new session is created, a database hook resolves the user's default organization (preferring isDefault=true, falling back to most recently created) and writes activeOrganizationId into the session row before it is persisted. A login audit log entry is also written at this point.
Session Deletion Hook#
On sign-out (session delete), a symmetric hook writes a logout audit log entry under the session's active organization.
Cookie Configuration#
Cookie attributes differ between self-hosted and cloud deployments, controlled by the IS_CLOUD constant :
| Attribute | Self-Hosted (IS_CLOUD=false) | Cloud (IS_CLOUD=true) |
|---|---|---|
secure | false | true (better-auth default) |
httpOnly | true | true (better-auth default) |
sameSite | "lax" | "lax" (better-auth default) |
path | "/" | "/" (better-auth default) |
useSecureCookies | false | true |
Known issue: Self-hosted instances hardcode secure: false regardless of whether HTTPS is in use. There is currently no environment variable to override this. If users accidentally access the app over HTTP (not HTTPS), the better-auth.session_token cookie can be transmitted in plaintext.
Session Database Schema#
The session table is defined in packages/server/src/db/schema/session.ts (the file is marked // OLD TABLE — it represents the legacy schema still used by the Drizzle migrations layer):
| Column | Type | Notes |
|---|---|---|
id | text | Primary key |
token | text | Unique; the value stored in the session cookie |
expires_at | timestamp | Computed from expiresIn at session creation |
created_at / updated_at | timestamp | Lifecycle timestamps |
ip_address | text | Optional; captured at sign-in |
user_agent | text | Optional; captured at sign-in |
user_id | text | FK → user.id, cascade delete |
impersonated_by | text | Set when an admin impersonates a user |
active_organization_id | text | Set by the session creation hook |
Session Validation at Request Time#
validateRequest in auth.ts is the single entry point for all tRPC and API route authentication. It:
- Checks for an
x-api-keyheader and, if present, takes the API-key path (returns a synthetic session object). - Otherwise calls
api.getSession(...)with the request'sCookieheader . - Enriches the returned session with the user's
role,ownerId, andenableEnterpriseFeaturesfields from themembertable .
A missing or expired session token returns { session: null, user: null }.
Common Issues and Diagnostics#
Premature Session Expiration (re-login every 1–2 days)#
Users may be forced to re-authenticate much sooner than the 3-day window if :
BETTER_AUTH_SECRETis not explicitly set — If the secret is generated at startup and the container restarts, all existing session tokens become invalid because they were signed with a different key. Fix: pinBETTER_AUTH_SECRET(orBETTER_AUTH_SECRET_FILE) as a stable environment variable/Docker secret.- Cookie stripping by reverse proxy — If a reverse proxy performs TLS termination and the downstream app sends
secure: falsecookies, some proxies strip or reject them. Ensure the proxy forwardsSet-Cookieheaders unmodified.
Sessions Not Persisting on HTTPS Self-Hosted#
The secure: false cookie flag means browsers may still send the cookie over HTTPS, but some strict browser policies or intermediaries may reject it. This is a known open issue with no configuration knob available yet.
Session-Rotation Race in 2FA (v0.29.12)#
After successful TOTP verification, better-auth 1.6.x rotates the session. If the client immediately refetches user state before the rotated session propagates, twoFactorEnabled appears false in the UI. This is a timing race — retry after a brief delay resolves it.
Related Articles#
- Authentication Secret Management — covers
BETTER_AUTH_SECRETandENCRYPTION_KEYlifecycle, key rotation, and the fail-open decryption behavior.