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).

Session cookies use Better Auth’s defaults:

PropertyValue
Cookie namebetter-auth.session_token
Cross-subdomainNot enabled
Custom prefix or attributesNone
Active organizationTracked 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.

TypeScript
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:

TypeScript
"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.

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?

TaskUse
Create an organization or invite membersPortal — these need a user session anyway
Mint a service key or register an OIDC clientPortal, or the API with a session cookie
Read or write configurationAPI, with a service key
Record usageAPI, with a service key
Inspect audit logs or analyticsEither

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.