Sessions and the portal
Gate's own portal at /dashboard authenticates with a session cookie rather than a bearer token.
This page covers that mechanism and where its boundaries are — it is how Gate signs people in, not
how your product should.
If you are building a product that signs users in, you want Web app instead.
Account access and Gate administration
Registration creates a normal account that can sign in to connected applications. Access to
/dashboard and session-authenticated management APIs additionally requires the operator to set
"user".is_gate_admin to true in the database. Existing accounts are not automatically promoted.
Organization membership and roles still restrict each administrator's access; the flag does not
create membership or grant global access. Service keys retain their existing scopes.
Normal users visiting the panel are sent to /account, where they can sign out or switch accounts.
OAuth login, consent, and token flows remain available to normal accounts. Administrator access is
checked against the database on every protected request, so promotion and revocation do not require
signing in again. The repository's docs/gate-admin.md contains the migration and SQL instructions.
Sign-in methods
Email and password is always enabled, with a minimum password length of six characters.
GitHub, Google, and Apple are optional organization settings. Each project can inherit, override with complete credentials, or disable each provider. The client OIDC registration selects the project and therefore the login methods. The hosted login page hides unavailable providers; opening Gate directly shows email/password only. Configure providers under organization or project settings. Global provider environment variables are no longer used.
See Social login and account linking for configuration, provider
discovery, and client-facing link/unlink APIs. Apple's client secret is an ES256 JWT signed with the
.p8 key; replace it before expiry (six months maximum).
The session cookie
Session cookies use Better Auth’s defaults:
| Property | Value |
|---|---|
| Cookie name | better-auth.session_token |
| Cross-subdomain | Not enabled |
| Custom prefix or attributes | None |
| Active organization | Tracked on the session row as activeOrganizationId |
The active organization matters: OAuth clients are scoped by it. clientReference reads
session.activeOrganizationId, so a client registered while one organization is active belongs to
that organization.
Calling the API from the browser
Portal pages call /api/v1 with the cookie attached. Requests must come from an allow-listed origin
and include credentials.
export async function gateFetch<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`/api/v1${path}`, {
...init,
credentials: "include",
headers: { "content-type": "application/json", ...init?.headers },
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.title ?? payload.message ?? payload.errors?.[0]?.message);
}
return payload.data as T;
}Allowed origins are GATE_BASE_URL plus the product origins in src/server/origins.ts. Allowed
headers are content-type, authorization, x-api-key, x-request-id, and idempotency-key.
The client
Gate's own React client is thin — one createAuthClient with two plugins:
"use client";
import { oauthProviderClient } from "@better-auth/oauth-provider/client";
import { organizationClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
plugins: [organizationClient({ dynamicAccessControl: { enabled: true } }), oauthProviderClient()],
});No baseURL is passed, so it defaults to the current origin plus /api/auth. It exposes
authClient.signIn.email, authClient.signUp.email, authClient.signIn.social, and
authClient.oauth2.consent.
The consent screen
When one of your OIDC clients requests scopes, the user lands on /consent with client_id and
scope in the query string, and approves or denies. Approval is recorded so the same client and
scope set does not prompt again.
The eight grantable scopes are openid, profile, email, offline_access, project:read,
config:read, usage:write, and billing:manage.
Portal or API?
| Task | Use |
|---|---|
| Create an organization or invite members | Portal — these need a user session anyway |
| Mint a service key or register an OIDC client | Portal, or the API with a session cookie |
| Read or write configuration | API, with a service key |
| Record usage | API, with a service key |
| Inspect audit logs or analytics | Either |
Anything scriptable and unattended belongs on the API with a service key. The portal is for the operations a human performs once.
Organizations are archived, not deleted
DELETE /api/v1/organizations/{id} sets archivedAt and clears the caller's active organization; it
does not remove data. List archived records with ?state=archived (or ?state=all) and restore with
POST /organizations/{id}/restore. Projects and config entries behave the same way.
Restoring requires the same permission as archiving — organization:delete for organizations,
project:delete for projects.