Dashboard identity and authorization¶
Gettier's hosted dashboard authorizes an identity against the current organization on every
request. A valid signed session is authentication evidence, not authorization by itself:
resolve_dashboard_membership(subject, organization_slug) must return an active membership
before the request reaches a page or server action. Disabling a user or changing a role is
therefore effective on the next request.
Roles¶
| Role | Read dashboard | Resolve gates | Run sensors | Manage members |
|---|---|---|---|---|
| Viewer | yes | no | no | no |
| Member | yes | yes | yes | no |
| Admin | yes | yes | yes | yes |
| Owner | yes | yes | yes | yes |
Server actions enforce permissions independently of button visibility. Middleware replaces all caller-supplied identity headers with values resolved from the membership store.
Organization and project context¶
The signed dashboard session records the selected organization, project, and environment. Middleware resolves that exact tuple against the user's current membership and the persistent project catalog on every request, then overwrites the internal tenant/context headers. The storage resolver and all dashboard read models consume those verified headers. A context selector appears when an identity has more than one authorized tuple.
Changing a label in the browser cannot change tenancy. An unknown project/environment tuple
returns 403, and removing the organization membership invalidates every context on the next
request. Hosted mode also disables the web process's local sensor registry; sensors execute
only through the context-scoped customer relay.
Invitations and deprovisioning¶
Admins and owners can create an email-bound, seven-day invitation from Settings. Production
sends the acceptance URL through the configured transactional email provider; only SHA-256
of its gt_inv_… token is persisted. Acceptance
runs through OIDC, requires the verified ID-token email to match, is atomic, and can occur only
once before expiry or revocation. Owners cannot be removed through the member endpoint, and
an administrator cannot remove their own membership accidentally.
Resend and revoke rotate or invalidate the secret: a resent invitation always receives a new
database row and token, making the prior link unusable. Provider requests use
gettier-invitation/<invitation-id> as their idempotency key. Local development uses capture
mode and displays the acceptance URL without claiming delivery; capture mode is forbidden in
production.
GETTIER_PUBLIC_APP_URL='https://app.gettier.dev'
GETTIER_EMAIL_PROVIDER='resend'
GETTIER_EMAIL_FROM='Gettier <team@gettier.dev>'
RESEND_API_KEY='re_...'
Deprovisioning deletes the organization membership. Because middleware reauthorizes each
request, existing signed cookies stop working for that organization immediately. The APIs are
POST /api/invitations, POST /api/members/deprovision, and POST /api/context; all require
same-origin requests plus the appropriate persistent role.
An owner cannot be removed directly or remove themselves. Ownership must first be transferred
to an existing member using POST /api/members/transfer-ownership; the operation is serialized
per organization and atomically promotes the target while demoting the previous owner to admin.
Preview bootstrap¶
The current login form uses GETTIER_DASHBOARD_TOKEN to authenticate a named subject, then
requires that subject to have a persistent membership when DATABASE_URL is configured.
Provision it with the migration/admin role:
GETTIER_MIGRATE_URL='postgres://admin:…@db/gettier' \
pnpm --filter @gettier/core provision-member acme 'oidc|alice' alice@example.com admin 'Alice Admin'
The token login is a preview bootstrap, not the public-v1 identity provider. The session boundary deliberately stores a stable external subject so an OIDC authorization-code flow can replace token verification without changing membership or permission enforcement.
OIDC configuration¶
The dashboard implements OpenID Connect Authorization Code + PKCE S256. Discovery metadata must match the configured issuer. ID tokens are verified against the provider's remote JWKS, including signature algorithm, issuer, audience, expiry, subject, and nonce. State, nonce, PKCE verifier, expiry, and the relative return path are held in an HMAC-signed, HttpOnly transaction cookie and erased after callback.
GETTIER_SESSION_SECRET='at-least-32-random-characters'
GETTIER_OIDC_ISSUER='https://identity.example'
GETTIER_OIDC_CLIENT_ID='gettier-production'
GETTIER_OIDC_CLIENT_SECRET='provider-client-secret' # omit for a public client
GETTIER_OIDC_REDIRECT_URI='https://app.gettier.dev/api/auth/oidc/callback'
GETTIER_OIDC_SCOPES='openid email profile'
The redirect URI must match the provider registration exactly. Production issuer, discovery
endpoints, and redirect URI must use HTTPS. GETTIER_SESSION_SECRET must contain at least 32
characters. The preview token form is disabled in production unless
GETTIER_ENABLE_PREVIEW_LOGIN=1 is explicitly set.
The implementation follows OpenID Connect Core's authorization-code and nonce validation
requirements and OAuth 2.0 Security BCP guidance for PKCE. ID-token/JWKS verification uses
jose; membership authorization remains application-owned and is rechecked after callback
and on every protected request.
Standards and implementation references:
- OpenID Connect Core 1.0
- OAuth 2.0 Security Best Current Practice (RFC 9700)
joseJWT verificationjoseremote JWKS resolver
Remaining identity work¶
Add delivery webhooks/bounce handling, SCIM deprovisioning, signing-key rotation runbooks, and provider-specific conformance tests before enabling public self-service signup. Enterprise SAML should arrive through the same OIDC identity broker rather than a second application authorization path.
Email API behavior follows Resend's official send-email contract and idempotency-key guidance.