@concourse/auth is the reusable authentication boundary for Concourse. It combines Better Auth 1.7.1, Drizzle SQLite, organizations, system administration, and generic SSO.
Complete the repository quick start. Then read the application authentication boundary before you change this package.
Run the package checks from the repository root:
pnpm --filter @concourse/auth check-types
pnpm --filter @concourse/auth testUse pnpm e2e:full after a change affects sessions, email, invitations, organizations, SSO, impersonation, or administration.
src/ contains the authentication library and its unit tests.scripts/ contains executable package commands.test/ contains public test helpers, auth test services, and certificate fixtures.The package has eight public entry points:
| Entry point | Purpose |
|---|---|
@concourse/auth/server | Create the server handler, narrow reads, and protected commands |
@concourse/auth/client | Create the typed React client |
@concourse/auth/configuration | Create a Better Auth configuration for a concrete app |
@concourse/auth/schema | Export Better Auth and custom Drizzle tables and relations |
@concourse/auth/policy | Export roles, permissions, domain rules, and policy extension helpers |
@concourse/auth/preview-admin | Reconcile a preview administrator through explicit app operations |
@concourse/auth/test | Create Better Auth test helpers with the official testUtils plugin |
@concourse/auth/test-services | Run reusable OIDC and SAML providers for acceptance tests |
The package does not load environment files or open a database connection. Each app passes explicit, validated dependencies to createAuth.
Required server options include:
Optional options configure trusted proxy headers, audit error reporting, and the runtime background-task handler.
The returned surface contains only handler, read.getSession, and protected commands. It does not expose unrestricted Better Auth APIs or session tokens.
Sessions last seven days. A sensitive change requires a session created within the last 15 minutes.
The signed session-cookie cache lasts 60 seconds. read.getSession uses it unless the caller sets query: { disableCookieCache: true }.
Use a cached read only for interface aids and ordinary navigation. A revoked session can remain in the cookie cache until it expires.
Use an authoritative read for every authorization boundary:
Every protected command bypasses the cookie cache automatically.
The package uses better-auth/minimal with the Drizzle adapter. It keeps database joins enabled.
Every membership has one fixed role:
owneradminmemberviewerUse the policy helper to add typed application resources. Owners receive every action on each new resource automatically.
The global systemAdmin role is separate. It does not include permission to set or view another user password.
All tenant data access must include the authorized organization identifier.
Return 404 for a missing or inaccessible organization slug. Return 403 for a denied action in an accessible organization.
Apply the auth schema migrations first. Then create the first administrator from the repository root:
pnpm auth:create-adminThe pinned Better Auth CLI assigns systemAdmin and marks the email address as verified. Later administration uses protected application commands.
Public email registration is disabled. A password account can be created only through a valid invitation.
Email verification is required. Password reset and sign-in responses use generic public errors to reduce account discovery.
An invitation link proves control of its destination email. Existing accounts become verified when the valid link opens. New invitees enter their name and password once, then sign in with the invited email. Invitation registration uses an opaque, expiring database claim to prevent concurrent account creation.
The link does not create membership. Better Auth accepts the invitation only after a verified user signs in with the matching email and selects Accept invitation. A mismatched session must sign out. Password and SSO sign-in keep the invitation callback.
Resending keeps the invitation ID and extends its expiry by 48 hours. Earlier email copies remain valid until acceptance or cancellation.
A valid Better Auth password-reset callback also verifies its user before the password form opens. Opening the link does not consume the reset token. Saving the password consumes it and returns the user to sign-in with the email filled.
SSO-created accounts remain passwordless. An explicit, fresh-session link flow is required when a password account already uses the provider email.
SSO supports generic OIDC and SAML. It does not use Microsoft social login.
One organization can have one provider with several globally unique domains. A system administrator must approve the provider before it can authenticate users.
Approval is a manual trust decision. The package does not use DNS verification.
Credential, mapping, protocol, endpoint, certificate, key, and domain changes return an unused provider to pending approval. Better Auth blocks linked identity-boundary changes.
Use provider replacement and account migration for linked issuer, identity endpoint, or SAML trust-anchor changes.
Production endpoints must use public HTTPS addresses. The package rejects private, loopback, link-local, wildcard, IP-address, and public-suffix identities.
The pinned OIDC and SAML test-provider origins are the only private trusted origins. Do not weaken Better Auth redirect, discovery, or server-request checks.
Provider JSON is not encrypted by the application. Encrypt the database at rest. Never return or log provider secrets after input.
Create a tenant-specific Entra application. Do not use the common issuer.
Add /api/auth/sso/callback/<provider-id> under the application origin as a web redirect URI. Request openid, profile, and email.
Add the optional email and xms_edov claims. Configure these provider mappings:
{
"email": "email",
"emailVerified": "xms_edov"
}The package rejects the identity when xms_edov is absent or false.
Store the Entra client ID, client secret, issuer, and public endpoints through the organization SSO form.
Read the Microsoft optional claims reference before you configure the tenant.
An app can pass backgroundTaskHandler to connect non-critical Better Auth work to its runtime lifecycle. The Next.js app uses after().
Public reset and verification email delivery can use this background path. Invitation delivery remains awaited because it exposes retry state.
Never defer required audit writes or state changes.
Import createAuthTestHelpers from @concourse/auth/test. Pass the same database, policy, origin, secret, and sender contracts used by the server.
The helper installs Better Auth testUtils. Use it for authenticated sessions and official auth fixtures instead of custom cookie construction.
Keep SQLite integration tests for transaction, relation, cascade, uniqueness, contention, and revocation behavior. The test helper does not replace database acceptance coverage.
The @concourse/auth/test-services entry point owns the reusable OIDC and SAML providers. Each application starts these services from its Playwright configuration.
The OIDC provider uses 127.0.0.1:8180. The SAML provider uses 127.0.0.1:8280.
The service derives the SAML callback from the validated AUTH_BASE_URL. The service has no fixed application path or port.
The certificate and private key are test fixtures. Do not use them outside acceptance tests.
Acceptance suites use fixed ports and must run in sequence. Run pnpm --filter @concourse/auth test-services:serve to start the providers directly.
Provider logs use the ignored data/auth/test-services.log file. Applications can attach the logs through collectAuthTestServiceLogs.
| Command | Purpose |
|---|---|
pnpm --filter @concourse/auth check-types | Run the package type check |
pnpm --filter @concourse/auth test | Run the authentication unit tests |
pnpm --filter @concourse/auth test-services:serve | Start the fixed-port OIDC and SAML providers |
pnpm e2e:full | Run the complete authentication lifecycle |
The provider command requires the test environment, including AUTH_BASE_URL. The root E2E command loads this environment automatically.
Authentication outcomes and security changes append rows to auth_audit_event. The table retains identifiers, request context, keyed fingerprints, and a coarse browser family.
It never stores passwords, tokens, assertions, provider configuration, raw IP addresses, or full user-agent headers.
Audit rows remain indefinitely in one unpartitioned table. Operators must monitor table and index growth.
createAuth trusts no forwarding header by default. Configure trusted headers only when the deployment proxy removes client values and writes its own value.
Authentication endpoint audit writes occur after Better Auth finishes the response. Configure onAuditError so operations can alert on a lost endpoint audit.
Protected application commands fail closed when a required audit append stops with an error.
Direct task changes commit their state and audit row in one transaction.
trustedOrigins.@concourse/auth is the reusable authentication boundary for Concourse. It combines Better Auth 1.7.1, Drizzle SQLite, organizations, system administration, and generic SSO.
Complete the repository quick start. Then read the application authentication boundary before you change this package.
Run the package checks from the repository root:
pnpm --filter @concourse/auth check-types
pnpm --filter @concourse/auth testUse pnpm e2e:full after a change affects sessions, email, invitations, organizations, SSO, impersonation, or administration.
src/ contains the authentication library and its unit tests.scripts/ contains executable package commands.test/ contains public test helpers, auth test services, and certificate fixtures.The package has eight public entry points:
| Entry point | Purpose |
|---|---|
@concourse/auth/server | Create the server handler, narrow reads, and protected commands |
@concourse/auth/client | Create the typed React client |
@concourse/auth/configuration | Create a Better Auth configuration for a concrete app |
@concourse/auth/schema | Export Better Auth and custom Drizzle tables and relations |
@concourse/auth/policy | Export roles, permissions, domain rules, and policy extension helpers |
@concourse/auth/preview-admin | Reconcile a preview administrator through explicit app operations |
@concourse/auth/test | Create Better Auth test helpers with the official testUtils plugin |
@concourse/auth/test-services | Run reusable OIDC and SAML providers for acceptance tests |
The package does not load environment files or open a database connection. Each app passes explicit, validated dependencies to createAuth.
Required server options include:
Optional options configure trusted proxy headers, audit error reporting, and the runtime background-task handler.
The returned surface contains only handler, read.getSession, and protected commands. It does not expose unrestricted Better Auth APIs or session tokens.
Sessions last seven days. A sensitive change requires a session created within the last 15 minutes.
The signed session-cookie cache lasts 60 seconds. read.getSession uses it unless the caller sets query: { disableCookieCache: true }.
Use a cached read only for interface aids and ordinary navigation. A revoked session can remain in the cookie cache until it expires.
Use an authoritative read for every authorization boundary:
Every protected command bypasses the cookie cache automatically.
The package uses better-auth/minimal with the Drizzle adapter. It keeps database joins enabled.
Every membership has one fixed role:
owneradminmemberviewerUse the policy helper to add typed application resources. Owners receive every action on each new resource automatically.
The global systemAdmin role is separate. It does not include permission to set or view another user password.
All tenant data access must include the authorized organization identifier.
Return 404 for a missing or inaccessible organization slug. Return 403 for a denied action in an accessible organization.
Apply the auth schema migrations first. Then create the first administrator from the repository root:
pnpm auth:create-adminThe pinned Better Auth CLI assigns systemAdmin and marks the email address as verified. Later administration uses protected application commands.
Public email registration is disabled. A password account can be created only through a valid invitation.
Email verification is required. Password reset and sign-in responses use generic public errors to reduce account discovery.
An invitation link proves control of its destination email. Existing accounts become verified when the valid link opens. New invitees enter their name and password once, then sign in with the invited email. Invitation registration uses an opaque, expiring database claim to prevent concurrent account creation.
The link does not create membership. Better Auth accepts the invitation only after a verified user signs in with the matching email and selects Accept invitation. A mismatched session must sign out. Password and SSO sign-in keep the invitation callback.
Resending keeps the invitation ID and extends its expiry by 48 hours. Earlier email copies remain valid until acceptance or cancellation.
A valid Better Auth password-reset callback also verifies its user before the password form opens. Opening the link does not consume the reset token. Saving the password consumes it and returns the user to sign-in with the email filled.
SSO-created accounts remain passwordless. An explicit, fresh-session link flow is required when a password account already uses the provider email.
SSO supports generic OIDC and SAML. It does not use Microsoft social login.
One organization can have one provider with several globally unique domains. A system administrator must approve the provider before it can authenticate users.
Approval is a manual trust decision. The package does not use DNS verification.
Credential, mapping, protocol, endpoint, certificate, key, and domain changes return an unused provider to pending approval. Better Auth blocks linked identity-boundary changes.
Use provider replacement and account migration for linked issuer, identity endpoint, or SAML trust-anchor changes.
Production endpoints must use public HTTPS addresses. The package rejects private, loopback, link-local, wildcard, IP-address, and public-suffix identities.
The pinned OIDC and SAML test-provider origins are the only private trusted origins. Do not weaken Better Auth redirect, discovery, or server-request checks.
Provider JSON is not encrypted by the application. Encrypt the database at rest. Never return or log provider secrets after input.
Create a tenant-specific Entra application. Do not use the common issuer.
Add /api/auth/sso/callback/<provider-id> under the application origin as a web redirect URI. Request openid, profile, and email.
Add the optional email and xms_edov claims. Configure these provider mappings:
{
"email": "email",
"emailVerified": "xms_edov"
}The package rejects the identity when xms_edov is absent or false.
Store the Entra client ID, client secret, issuer, and public endpoints through the organization SSO form.
Read the Microsoft optional claims reference before you configure the tenant.
An app can pass backgroundTaskHandler to connect non-critical Better Auth work to its runtime lifecycle. The Next.js app uses after().
Public reset and verification email delivery can use this background path. Invitation delivery remains awaited because it exposes retry state.
Never defer required audit writes or state changes.
Import createAuthTestHelpers from @concourse/auth/test. Pass the same database, policy, origin, secret, and sender contracts used by the server.
The helper installs Better Auth testUtils. Use it for authenticated sessions and official auth fixtures instead of custom cookie construction.
Keep SQLite integration tests for transaction, relation, cascade, uniqueness, contention, and revocation behavior. The test helper does not replace database acceptance coverage.
The @concourse/auth/test-services entry point owns the reusable OIDC and SAML providers. Each application starts these services from its Playwright configuration.
The OIDC provider uses 127.0.0.1:8180. The SAML provider uses 127.0.0.1:8280.
The service derives the SAML callback from the validated AUTH_BASE_URL. The service has no fixed application path or port.
The certificate and private key are test fixtures. Do not use them outside acceptance tests.
Acceptance suites use fixed ports and must run in sequence. Run pnpm --filter @concourse/auth test-services:serve to start the providers directly.
Provider logs use the ignored data/auth/test-services.log file. Applications can attach the logs through collectAuthTestServiceLogs.
| Command | Purpose |
|---|---|
pnpm --filter @concourse/auth check-types | Run the package type check |
pnpm --filter @concourse/auth test | Run the authentication unit tests |
pnpm --filter @concourse/auth test-services:serve | Start the fixed-port OIDC and SAML providers |
pnpm e2e:full | Run the complete authentication lifecycle |
The provider command requires the test environment, including AUTH_BASE_URL. The root E2E command loads this environment automatically.
Authentication outcomes and security changes append rows to auth_audit_event. The table retains identifiers, request context, keyed fingerprints, and a coarse browser family.
It never stores passwords, tokens, assertions, provider configuration, raw IP addresses, or full user-agent headers.
Audit rows remain indefinitely in one unpartitioned table. Operators must monitor table and index growth.
createAuth trusts no forwarding header by default. Configure trusted headers only when the deployment proxy removes client values and writes its own value.
Authentication endpoint audit writes occur after Better Auth finishes the response. Configure onAuditError so operations can alert on a lost endpoint audit.
Protected application commands fail closed when a required audit append stops with an error.
Direct task changes commit their state and audit row in one transaction.
trustedOrigins.