Skip to content

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:

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.