v2.0.0 replaces the single long-lived JWT with an access token / refresh token pair. This page explains the model, the settings that govern it, and the two configuration mistakes that break a deployment.
Under the old model a token was minted at login and stayed valid for its whole lifetime. Deactivating a user or changing their role did nothing until that token expired — potentially days later.
In v2.0.0 the access token is short-lived and the session is bounded by a
separate refresh token. Renewal is not a rubber stamp: POST /app/auth/refresh
re-checks the user against the database on every renewal — still active, role
unchanged. That is what turns “revoke this user” into an action that takes
effect within one access-token lifetime instead of within one session.
| Token | Lifetime | Audience | Where it lives | What it does |
|---|---|---|---|---|
| Access | JWT_EXPIRES_IN, default 15m | cypex-platform | token cookie | Authenticates API and PostgREST requests |
| Refresh | the session length — cypex.t_config.jwt_exp | cypex-refresh | refresh_token cookie | Buys a new access token, nothing else |
Consequences worth understanding:
- The refresh token is never returned in a JSON body. It is cookie-only, to keep it off the XSS surface.
- The refresh audience is deliberately different (
cypex-refresh, not configurable). PostgREST is configured to requirecypex-platform(PGRST_JWT_AUD), so a stolen refresh token cannot be spent as a database credential at the data API. - The refresh token is not rotated on renewal. Only the access cookie is rewritten.
- Logout clears both cookies. Clearing only the access cookie would leave the session alive, since the refresh cookie alone can mint a new one.
- Sessions are synchronised across browser tabs, and the session identity is reset on re-login.
Platform Admin → Configuration → Session length writes
cypex.t_config.jwt_exp. The field label is Session length; the storage
key remains jwt_exp for compatibility. This is the refresh token
lifetime — the session length — not the access-token lifetime.
WarningThe value is validated. It must be at least 2× the access-token lifetime (30m at the default 15m access TTL). A shorter session is incoherent: it would end when the access token expires rather than when you configured it to, and no renewal would ever run — which means the database re-check that bounds a revoked user would never fire either. Values below the minimum are rejected withtoo_short; malformed values are rejected withmalformed.
Accepted format: a whole number followed by s, m, h, or d —
30m, 12h, 7d. Nothing else parses.
Set in .env. These are operator-level and are not editable from the admin
panel.
| Variable | Default | Notes |
|---|---|---|
JWT_EXPIRES_IN | 15m | Access-token lifetime. Short on purpose — it bounds how long a deactivated user or a changed role survives. |
JWT_REFRESH_EXPIRES_IN | 7d | Refresh lifetime, used as the fallback default. Precedence is cypex.t_config.jwt_exp (the admin-panel session length) → this variable → the built-in default. Set it to define the starting value; once an administrator saves a session length in the admin panel, that value wins. The SSO gateway reads the same variable, so both services agree by default. |
JWT_ISSUER | cypex-api | Issuer stamped on minted tokens. |
JWT_AUDIENCE | cypex-platform | Audience on access tokens. Must match PGRST_JWT_AUD in docker-compose.yml. |
BCRYPT_ROUNDS | 10 | Password-hashing cost. Valid range 10–15, clamped rather than rejected. Each increment roughly quadruples login CPU cost — raise it behind a load test, not on principle. Existing hashes keep working at the cost they were created with. |
REDIS_URL | redis://cypex_redis:6379 | See below. |
WarningJWT_ISSUERis not free-form. Verifiers accept the configured issuer plus a fixed list of accepted names. Setting it to something outside that list produces a deployment that signs tokens it then rejects. Leave it alone unless you have a specific reason.
InfoThe issuer default changed fromcypex-sso-gateway(v1.9.x) tocypex-api. This is not breaking:cypex-sso-gatewayis retained as a legacy accepted issuer, so live sessions survive the upgrade.
The SSO Gateway writes OAuth state, sessions, and refresh-token records to Redis. Before renewing a gateway-issued refresh token, the backend checks it against that revocation list.
The check applies to SSO-origin tokens only. Password sessions do not consult
Redis in any state — a deployment that does not use SSO is unaffected by
Redis entirely, and REDIS_URL can be left unset.
Where it matters, the two failure modes are not the same:
| Redis state | Result |
|---|---|
| Reachable, record present | Renewal proceeds |
| Reachable, record missing or flagged revoked | Renewal refused |
| Unreachable or not configured | The check is skipped and renewal proceeds; a warning is logged once, not per request |
WarningThe middle row is the operational trap. A missing record is read as revoked — so a Redis that is reachable but has lost its dataset signs out every SSO user at their next renewal. Run it with persistence; the bundled compose service uses--appendonly yeson a named volume.
Confirm each of these before cutting traffic:
-
JWT_AUDIENCEandPGRST_JWT_AUDagree. A mismatch produces authenticated users who get 401s from the data API only — the admin panel appears to work. -
cypex.t_config.jwt_expis at least 2×JWT_EXPIRES_IN. An existing value below the new minimum will be refused on the next edit. - Redis is running and persistent if you use SSO.
- External JWT verifiers accept the new claims — see Upgrade to v2.0.0.
- Any integration that read a long-lived token from a login response has been updated. Access tokens now expire in 15 minutes and the refresh token is cookie-only.
TipA service-to-service integration that logged in once and reused the token for days will now break every 15 minutes. Either callPOST /app/auth/refreshon 401, or give the integration its own role and a token issued outside the interactive login flow.
- Configuration — where the session length is edited.
- Single Sign-On — federated login and the SSO Gateway.
- User lifecycle — pending, active, and rejected SSO identities.
- Roles & Capabilities — the claims the access token carries.
- Upgrade to v2.0.0 — the operator upgrade path.