---
title: "Authentication"
description: "Magic-link-first Better Auth integration with password fallback, social providers, organizations, and MCP bearer credentials."
---

# Authentication

<!-- i18n-copy-ignore: Agent-Native is a proper name; punctuation is locale-invariant. -->

Agent-Native apps use [Better Auth](https://better-auth.com) for authentication with an account-first design. Users create an account on first visit and get real identity from day one.

For an administered, invite-only deployment, set the signup and organization
creation policies in [Administered Deployments](/docs/administered-deployments).
This is separate from domain auto-join. A verified allowed domain can admit a
person to an organization, while an invitation is an explicit membership offer.

## Overview {#overview}

Auth is configured automatically via `autoMountAuth(app)` in the auth server plugin. There are three modes:

- **Default:** Magic-link-first Better Auth when outbound email is ready, with email/password fallback and social providers. Onboarding page shown on first visit.
- **Remote MCP OAuth:** Standard OAuth 2.1 for MCP hosts such as Claude Code and ChatGPT connectors.
- **Custom:** Bring your own auth via `getSession` callback.

<Diagram id="doc-block-f8rmmq" title="Three ways in, one session" summary="Browser visitors, MCP hosts, and custom providers all resolve to the same AuthSession that downstream scoping reads.">

```html
<div class="auth-modes">
  <div class="diagram-col">
    <div class="diagram-card">
      <span class="diagram-pill accent">Default</span
      ><strong>Better Auth</strong
      ><small class="diagram-muted"
        >magic link / password &middot; Google &middot; GitHub</small
      >
    </div>
    <div class="diagram-card">
      <span class="diagram-pill">Remote MCP OAuth</span
      ><strong>OAuth 2.1 + PKCE</strong
      ><small class="diagram-muted">Claude Code, ChatGPT connectors</small>
    </div>
    <div class="diagram-card">
      <span class="diagram-pill">Custom</span
      ><strong>getSession callback</strong
      ><small class="diagram-muted"
        >Clerk &middot; Auth0 &middot; Firebase</small
      >
    </div>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-panel center">
    <span class="diagram-pill ok">AuthSession</span
    ><small class="diagram-muted">email &middot; orgId &middot; orgRole</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-box">Request context &amp; data scoping</div>
</div>
```

```css
.auth-modes {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}
.auth-modes .diagram-col {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.auth-modes .diagram-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px 12px;
}
.auth-modes .diagram-arrow {
  font-size: 22px;
  line-height: 1;
}
.auth-modes .center {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
```

</Diagram>

The browser flow is the same Better Auth flow everywhere — there is **no dev auth bypass**, and `getSession()` never falls back to a `local@localhost` sentinel. What changes between environments is signup friction, not the login wall:

| Environment      | First-load behavior                                                                     | Email verification                                              |
| ---------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| **Local dev**    | Auto-creates a throwaway dev account and signs you in (no login wall)                   | Skipped by default (and when no email provider)                 |
| **QA / preview** | Normal signup, but verification can be skipped so testers don't wait on email           | Skip with `AUTH_SKIP_EMAIL_VERIFICATION=1`                      |
| **Production**   | Magic-link signup/login when outbound email is ready; email/password fallback otherwise | Required for magic links (when an email provider is configured) |

A few flags tune this; full details are in the [Environment Variables](#environment-variables) table:

- `AGENT_NATIVE_DISABLE_AUTO_DEV_ACCOUNT=1` — use the normal signup page in local dev instead of the auto dev account.
- `AUTH_DISABLED=true` — skip login/signup entirely and run every request as one shared user (local dev / previews / demos only, never production with real users).
- `AUTH_MODE=local` — affects only CLI/agent identity (which dev user `pnpm action` runs as); it is **not** a browser login bypass.

<Callout id="doc-block-18jli2j" tone="warning">

`AUTH_DISABLED=true` runs **every request as one shared user**. Use it only for local dev, previews, or demos — never in production with real users, where it would expose all data to anyone.

</Callout>

## Local development sign-in {#local-development-sign-in}

On a loopback development server, the shared sign-in page puts **Continue as
local dev** first. It creates or reuses the real Better Auth development
account and session, so this is the same authenticated path used elsewhere.

Choose **Show full sign in options** to reveal normal signup, password, magic
link, and social-provider options. If the local session cannot be created, the
page reveals those options automatically so you can continue with standard
authentication.

This convenience is available only when the browser connects through
`localhost`, `127.0.0.0/8`, or `::1` in development or test. It is disabled for
deploy previews and hosted production. It is not an authentication bypass.
Set `AGENT_NATIVE_DISABLE_AUTO_DEV_ACCOUNT=1` to opt out and show the normal
sign-in page in local development.

## Better Auth (Default) {#better-auth}

By default, Better Auth powers authentication. When outbound email is ready,
magic-link signup/login is the primary browser flow. Email/password remains
the fallback when email is unavailable, or when `AUTH_MAGIC_LINK=0` is set. It
provides:

- Magic-link registration and login, with email/password fallback
- Social providers (Google, GitHub, and 35+ others)
- Organizations with roles and invitations
- JWT tokens for API and A2A access
- Bearer token support for programmatic clients

Better Auth routes are mounted at `/_agent-native/auth/ba/*`. The framework also provides backward-compatible endpoints:

In an invite-only deployment, admission is checked before a Better Auth user
is created. The policy covers email/password, magic-link, and social sign-up.
See [Administered Deployments](/docs/administered-deployments#close-signup-and-organization-creation)
for the bootstrap-admin and allowed-domain rules.

- `GET /_agent-native/auth/session` — get current session
- `POST /_agent-native/auth/magic-link` — request a magic-link email
- `POST /_agent-native/auth/login` — email/password fallback login
- `POST /_agent-native/auth/register` — create account
- `POST /_agent-native/auth/logout` — sign out

## Cookie Realms {#cookie-realms}

The session cookie's realm follows the deployment shape, so apps that share a
database/origin share sign-in and apps that don't stay isolated:

| Deployment shape                            | Cookie realm                                                                                                         |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Standalone app                              | Isolated per app by slug (`APP_NAME`, or package name in local dev); stable `an` prefix in production                |
| Workspace mode (`AGENT_NATIVE_WORKSPACE=1`) | One shared realm — workspace apps share an origin and database                                                       |
| Custom same-database subdomains             | Opt into shared cookies with `COOKIE_DOMAIN`                                                                         |
| First-party hosted (`*.agent-native.com`)   | Isolated namespace per app (each has its own auth database); `COOKIE_DOMAIN=.agent-native.com` is ignored by default |

First-party hosted apps each have their own auth database, so cross-app sign-in
goes through [Cross-App SSO](/docs/cross-app-sso) rather than a shared cookie.
These deploys must provide `APP_NAME` or a derivable app URL (`APP_URL`,
`BETTER_AUTH_URL`, `VITE_BETTER_AUTH_URL`, `URL`, `DEPLOY_PRIME_URL`, or
`DEPLOY_URL`); otherwise startup fails instead of falling back to the shared
`an_session` name. To intentionally share one auth database across
subdomains, set `AGENT_NATIVE_SHARE_COOKIE_DOMAIN=1` alongside
`COOKIE_DOMAIN`.

## Packaged Desktop workspace SSO {#desktop-workspace-sso}

The signed Agent-Native Desktop SSO Canary composes hosted federation into one
workspace sign-in for canonical first-party apps. Dispatch remains the identity
authority, while every app mints and owns its normal local session. Electron
keeps the authority session in a dedicated partition and copies only the target
app's allowlisted session cookie into `persist:app-<id>`; provider credentials,
identity tokens, and another app's cookies never enter renderer IPC.

The Dispatch-owned `desktop.workspace-sso` feature flag is default-off and
controls availability only. Missing, malformed, unreadable, and explicit-Off
state preserve ordinary per-app sign-in and sign-out. Nonce binding, signed
exchange validation, exact origin and callback allowlists, authenticated-session
requirements, session isolation, and revocation remain unconditional in every
flag state. Custom and third-party apps are excluded.
Canonical hosted apps recognize packaged Desktop requests without per-app
identity-hub environment configuration. Ordinary browsers stay on normal
per-app sign-in. Self-hosted apps need `AGENT_NATIVE_IDENTITY_HUB_URL` only for
the remaining explicit Desktop or server-side federation path.
Every Desktop build may initialize the broker when the per-device
`desktopSsoEnabled` preference is true (the default); an explicit persisted
`false` remains an opt-out. The `desktop.workspace-sso` Dispatch flag must also
be enabled. The Canary user-agent marker no longer gates broker initialization;
it only identifies the update channel.
The Canary user-agent marker is an availability hint rather than remote
attestation. Authentication and authorization never trust it; supervised
acceptance binds the client through exact signed-artifact provenance.
For an eligible registered app, Desktop presents the workspace sign-in surface
over the app's sign-in flow. If the rollout is unavailable or the app is not
eligible, ordinary per-app sign-in remains available.

## QA Accounts {#qa-accounts}

Local development and tests skip password-signup email verification by default,
so the email/password fallback can be tested without waiting on an inbox. To
force verification locally while testing that fallback, set
`AUTH_SKIP_EMAIL_VERIFICATION=0`.

For hosted QA environments where testers need real accounts but should not wait
on email delivery, set:

```bash
AUTH_SKIP_EMAIL_VERIFICATION=1
```

When this flag is set, password fallback signup does not require email
verification and the signup verification email is not sent. It does not change
magic-link delivery. Use it only for QA or preview environments, and name test accounts with a `+autoz` address
(`name+autoz@example.com`) so they are easy to identify.

## Email Verification Policy {#email-verification-policy}

`AUTH_SKIP_EMAIL_VERIFICATION` is a local and QA convenience — a hosted
deployment ignores it. To state the policy for any environment, production
included, declare it in config:

```ts
defineAppConfig({ auth: { requireEmailVerification: false } });
```

The deployment alias is `AUTH_REQUIRE_EMAIL_VERIFICATION=0`. A declared value
outranks both `AUTH_SKIP_EMAIL_VERIFICATION` and the per-environment default.

`false` accepts an unverified address as a login credential — anyone can claim
any email — so use it only where signup is already restricted another way.
`true` on a deployment with no email provider disables password signup instead,
because the verification could never be delivered.

## Social Providers {#social-providers}

Set environment variables to enable social login. Better Auth auto-detects them:

```bash
# Google OAuth
GOOGLE_SIGN_IN_CLIENT_ID=your-low-scope-sign-in-client-id
GOOGLE_SIGN_IN_CLIENT_SECRET=your-low-scope-sign-in-client-secret

# Backwards-compatible fallback, and provider OAuth credentials for templates
# that connect to Google APIs such as Gmail or Calendar.
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret

# GitHub OAuth
GITHUB_CLIENT_ID=your-client-id
GITHUB_CLIENT_SECRET=your-client-secret
```

Templates that use `createGoogleAuthPlugin()` show a "Sign in with Google" page. The Google OAuth callback handles mobile deep linking for native apps automatically.

Prefer `GOOGLE_SIGN_IN_CLIENT_ID` / `GOOGLE_SIGN_IN_CLIENT_SECRET` for normal
app login. That client should request only identity scopes. Keep
`GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` for product integrations that need
Google API scopes, or as the legacy fallback when a deploy has not been split
yet. Mail and Calendar-style apps should use their own provider OAuth clients so
high-scope consent screens do not affect generic app sign-in.

### OAuth State Signing {#oauth-state-secret}

Set `OAUTH_STATE_SECRET` to a random 32+ char value in production so OAuth state envelopes (Google, Atlassian, Zoom) are HMAC-signed with a dedicated key independent of any third-party secret. See [Security — OAuth State Signing](/docs/security#oauth-state) for the full requirements and threat model.

## Organizations {#organizations}

The framework provides a built-in organization system. This is the framework's own `org/` module — backed by the `organizations` and `org_members` tables — not Better Auth's organization plugin, which is intentionally not registered. Every app supports:

- Creating organizations
- Inviting members with roles (`owner`, `admin`, `member`)
- Switching active organization
- Per-org data scoping via `org_id` columns

The active org is tracked on the session as `session.orgId`, and switching orgs changes the data the user and agent see. Data scoping itself happens further down the stack — see [Security & Data Scoping](/docs/security#data-scoping) for the full `session.orgId → AGENT_ORG_ID → SQL` pipeline and the access guards. The [Multi-Tenancy](/docs/multi-tenancy) docs cover the org-management surface. For the org-role vs. resource-role distinction, the full org REST route table, and a practical multi-user setup sequence, see [Organizations, Teams & Permissions](/docs/organizations-teams-permissions).

## Static MCP Bearer Tokens {#access-tokens}

`ACCESS_TOKEN` and `ACCESS_TOKENS` are not browser auth and do not make an app private. They remain only as static bearer credentials for MCP/connect clients that cannot use the OAuth flow.

```bash
# Single token
ACCESS_TOKEN=my-secret-token

# Multiple tokens
ACCESS_TOKENS=token1,token2,token3
```

Configuring these variables never renders a token login page for visitors. Web sign-in stays on Better Auth or your custom `getSession` provider.

## Remote MCP OAuth {#remote-mcp-oauth}

Every app's MCP endpoint can act as a standard protected MCP resource. OAuth-capable clients can be configured with only the remote MCP URL:

```text
https://mail.agent-native.com/mcp
```

Unauthenticated MCP requests return a `WWW-Authenticate` challenge pointing at `/.well-known/oauth-protected-resource`. The client then discovers the app's OAuth metadata, dynamically registers a public client, opens the app's authorization page, and exchanges an authorization code with PKCE for access and refresh tokens.

<Diagram id="doc-block-1pprto0" title="Remote MCP OAuth handshake" summary={"An OAuth-capable client bootstraps from just the MCP URL — challenge, discovery, dynamic registration, then a PKCE code exchange."}>

```html
<div class="mcp-flow">
  <div class="diagram-node">
    1 &middot; MCP request<br /><small class="diagram-muted">no token</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-node warn">
    2 &middot; 401 challenge<br /><small class="diagram-muted"
      >WWW-Authenticate</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-node">
    3 &middot; Discover metadata<br /><small class="diagram-muted"
      >.well-known</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-node">
    4 &middot; Register client<br /><small class="diagram-muted"
      >dynamic, public</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-node">
    5 &middot; Authorize + PKCE<br /><small class="diagram-muted"
      >code exchange</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
  <div class="diagram-node ok">
    6 &middot; Access + refresh<br /><small class="diagram-muted"
      >audience-bound</small
    >
  </div>
</div>
```

```css
.mcp-flow {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.mcp-flow .diagram-node {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 12px;
}
.mcp-flow .diagram-arrow {
  font-size: 20px;
  line-height: 1;
}
```

</Diagram>

Access tokens are signed with `A2A_SECRET` when set, otherwise `BETTER_AUTH_SECRET`. They carry the signed user/org identity and the `mcp:read`, `mcp:write`, `mcp:apps`, and/or `offline_access` scopes, and are audience-bound to the exact MCP resource URL. `offline_access` is advertised for hosts such as ChatGPT that require it before retaining refresh access. Refresh tokens are stored only as hashes and are not rotated: a refresh returns the same token, and refresh grants have no fixed expiry. Every request and every refresh re-checks that the user still belongs to the token's organization, so a removed member loses MCP access on their next request. Tool calls and MCP Apps resource reads run inside the same request context as the signed-in user; the embedded MCP App iframe never receives raw OAuth tokens.

New MCP authorization codes and refresh tokens record their owner at issuance. Token exchanges reject unbound legacy grants or an owner-binding mismatch with `invalid_grant`, without inferring a legacy owner. An intentional email rekey updates the issuance identity. It also retires the old address: access and Connect tokens signed for it before the rekey are refused, and no new ones are issued for it unless an account registers that address again. OAuth clients get tokens for the new address on their next refresh. Offboarding revokes or deletes grants without transferring the issuance binding.

The OAuth owner-binding update requires every existing OAuth connection to [reconnect once](/docs/external-agents#reconnect). Old OAuth access tokens are refused after rollout. Unbound legacy refresh tokens return `invalid_grant`, and pending old authorization codes must restart consent. Newly issued access tokens use their configured lifetime and live membership checks. Legacy A2A-format `mcp-connect` credentials and cross-app A2A tokens are unchanged by this cutover. Connections using old OAuth-format Connect tokens must reconnect too.

`npx @agent-native/core@latest connect <url> --client claude-code` writes the URL-only MCP entry for this standard flow. For clients that cannot perform remote MCP OAuth, use the Connect page or `npx @agent-native/core@latest connect --token <token>` fallback to write an explicit bearer-token entry.

## Bring Your Own Auth {#byoa}

Pass a custom `getSession` callback to use any auth provider (Clerk, Auth0, Firebase, etc.):

```ts filename="server/plugins/auth.ts"
import { createAuthPlugin } from "@agent-native/core/server";

export default createAuthPlugin({
  getSession: async (event) => {
    // Your custom auth logic here
    const session = await myAuthProvider.verify(event);
    if (!session) return null;
    return { email: session.email };
  },
  publicPaths: ["/api/webhooks"],
});
```

Custom authentication must also configure the optional `AuthOptions.getBrowserSession` callback for UI capabilities and browser approvals. It accepts an `H3Event` and returns `AuthSession | null`, like `getSession`, but must read browser cookies only, never bearer tokens, query tokens, or other headers. Without this explicit hook, those browser-only operations return `401`.

## Public Workspace Apps {#public-workspace-apps}

Workspace apps are internal by default. To let anonymous visitors load a public
site while keeping management pages behind auth, declare route access in
`apps/<id>/package.json`:

```json
{
  "agent-native": {
    "workspaceApp": {
      "audience": "public",
      "protectedPaths": ["/admin"]
    }
  }
}
```

For the inverse shape, keep the default internal audience and expose only
specific public pages:

```json
{
  "agent-native": {
    "workspaceApp": {
      "publicPaths": ["/", "/share"]
    }
  }
}
```

`publicPaths` and `protectedPaths` use prefix matching, so `"/admin"` also
covers `"/admin/users"`. These settings open page navigation only. Framework
routes (`/_agent-native/*`) and custom API routes (`/api/*`) still require auth
unless the app explicitly adds those prefixes to
`createAuthPlugin({ publicPaths: [...] })`.

## Session API {#session-api}

The session object returned by `getSession(event)` has this shape:

```ts
interface AuthSession {
  email: string; // User's email (primary identifier)
  userId?: string; // Better Auth user ID
  token?: string; // Session token
  name?: string; // Display name from the auth provider, when available
  image?: string; // Profile image from the auth provider, when available
  orgId?: string; // Active organization ID
  orgRole?: string; // Role in active org (owner/admin/member)
}
```

On the client, use the `useSession()` hook:

```ts
import {
  useSession,
} from "@agent-native/core/client/hooks"
function MyComponent() {
  const { session, isLoading } = useSession();
  if (isLoading) return <p>Loading...</p>;
  if (!session) return <p>Not signed in</p>;
  return <p>Hello, {session.email}</p>;
}
```

## Sign-In with Return URL {#sign-in-return-url}

Templates with **public pages** (share links, embeds, marketing pages) often need an in-page CTA that asks anonymous viewers to sign in and brings them back to the page they were on. The framework provides a single entry point for this:

```
/sign-in?return=<same-origin-path>
```

When an anonymous viewer hits this URL, the framework's login page is served. After a successful sign-in (magic link, email/password, token, or Google OAuth), the browser returns to `return`.

The `return` parameter is validated as a **same-origin path**. Network-path references (`//evil.com/...`), absolute URLs, `data:` / `javascript:` schemes, and embedded control characters all fall back to `/`. The validated path is reconstructed from the URL parser, not echoed back from the input.

**From a React component:**

```tsx
import { Button } from "@/components/ui/button";

function SignInCta() {
  const onClick = () => {
    const ret = window.location.pathname + window.location.search;
    window.location.href = "/sign-in?return=" + encodeURIComponent(ret);
  };
  return <Button onClick={onClick}>Sign in</Button>;
}
```

### Bookmarked private paths

Normal app pages use one impersonal, public-cacheable SSR shell for every visitor. `AppProviders` checks the session on the client; on a private path such as `/dashboard`, it redirects an anonymous visitor to `/sign-in?return=%2Fdashboard` before mounting private app UI. After sign-in, the visitor returns to `/dashboard`. Pass `isPublicPath` for public/SEO routes, or `sessionBypass` for a surface that authenticates through another scoped mechanism such as an MCP embed token.

How long that shell is cached is a deployment-wide setting — see [SSR Caching](/docs/ssr-caching) for `AGENT_NATIVE_SSR_CACHE`. Shortening or disabling the cache changes duration only; SSR still renders the anonymous branch with cookies stripped.

### Behind the scenes: Google OAuth

Both flows (the explicit `/sign-in` entrypoint and the bookmarked-path case) thread the return URL through the OAuth state. The legacy `/_agent-native/sign-in` alias remains accepted for older links. The state is HMAC-signed, so it can't be forged in transit. On the callback, the return URL is re-validated as same-origin before the redirect — so a leaked signing key still can't be turned into an open-redirect oracle.

If your template wraps `/_agent-native/google/auth-url` directly (e.g. mail and calendar templates do, to widen scopes), accept a `?return=<path>` query and forward it via the options-object form of `encodeOAuthState`:

```ts
const returnUrl = getQuery(event).return;
const state = encodeOAuthState({
  redirectUri,
  desktop,
  returnUrl: typeof returnUrl === "string" ? returnUrl : undefined,
});
```

The default `/_agent-native/google/auth-url` route does this automatically — only override if your template needs custom OAuth handling.

## Environment Variables {#environment-variables}

| Variable                                | Purpose                                                                                                                                                |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `BETTER_AUTH_SECRET`                    | Signing key for Better Auth (auto-generated if not set)                                                                                                |
| `AUTH_SKIP_EMAIL_VERIFICATION`          | Set to `1` in QA/preview environments to let password-fallback signups proceed without verification; local dev/test skips by default                   |
| `AUTH_REQUIRE_EMAIL_VERIFICATION`       | Set to `0` or `1` to declare the password-signup verification policy for any environment, production included; outranks `AUTH_SKIP_EMAIL_VERIFICATION` |
| `AUTH_MAGIC_LINK`                       | Set to `0`, `false`, or `off` to force the email/password fallback even when outbound email is ready                                                   |
| `AUTH_DISABLED`                         | Set to `true` or `1` to skip login/signup; all requests run as one shared user (local dev/preview only — not for production with real users)           |
| `AGENT_NATIVE_DISABLE_AUTO_DEV_ACCOUNT` | Set to `1` to disable localhost auto-sign-in on a fresh dev database                                                                                   |
| `AUTH_MODE`                             | `local` resolves CLI/agent identity only (which dev user `pnpm action` runs as); never a browser login bypass                                          |
| `COOKIE_DOMAIN`                         | Opt into shared session cookies across same-database subdomains (see [Cookie Realms](#cookie-realms))                                                  |
| `AGENT_NATIVE_WORKSPACE`                | `1` runs in workspace mode — one shared session realm across workspace apps                                                                            |
| `AGENT_NATIVE_SHARE_COOKIE_DOMAIN`      | Set with `COOKIE_DOMAIN` to share one auth database across first-party subdomains                                                                      |
| `OAUTH_STATE_SECRET`                    | Dedicated HMAC key for OAuth state envelopes (see [Security — OAuth State Signing](/docs/security#oauth-state))                                        |
| `GOOGLE_SIGN_IN_CLIENT_ID`              | Preferred low-scope Google OAuth client ID for app login                                                                                               |
| `GOOGLE_SIGN_IN_CLIENT_SECRET`          | Preferred low-scope Google OAuth secret for app login                                                                                                  |
| `GOOGLE_CLIENT_ID`                      | Legacy Google login fallback, and provider OAuth client ID for Google API integrations                                                                 |
| `GOOGLE_CLIENT_SECRET`                  | Legacy Google login fallback, and provider OAuth secret for Google API integrations                                                                    |
| `GITHUB_CLIENT_ID`                      | Enable GitHub OAuth                                                                                                                                    |
| `GITHUB_CLIENT_SECRET`                  | GitHub OAuth secret                                                                                                                                    |
| `ACCESS_TOKEN`                          | Static bearer fallback for MCP/connect clients; not browser auth                                                                                       |
| `ACCESS_TOKENS`                         | Comma-separated static bearer fallbacks for MCP/connect clients; not browser auth                                                                      |
| `A2A_SECRET`                            | Shared secret for JWT-signed A2A cross-app identity verification and, when present, MCP OAuth access-token signing                                     |

## What's next

- [**Organizations, Teams & Permissions**](/docs/organizations-teams-permissions) — the org-role vs. resource-role RBAC model, and a practical multi-user setup sequence
- [**Multi-Tenancy**](/docs/multi-tenancy) — the org-management surface built on top of `session.orgId`
- [**Security — Data Scoping**](/docs/security#data-scoping) — the `session.orgId → AGENT_ORG_ID → SQL` pipeline and access guards
- [**Cross-App SSO**](/docs/cross-app-sso) — sharing sign-in across first-party hosted apps that each have their own auth database
- [**Sharing**](/docs/sharing) — sharing individual resources between authenticated users and orgs
- [**MCP Protocol**](/docs/mcp-protocol) — the MCP surface that Remote MCP OAuth authenticates into
