> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gavai.io/llms.txt
> Use this file to discover all available pages before exploring further.

# End-user identity

> The separate identity surface for the people who use a workspace's published app — distinct from the operators who build it.

export const PageMeta = ({audience, readingTime, prereqs, level}) => {
  const items = [audience && ({
    key: "audience",
    label: "AUDIENCE",
    value: audience
  }), readingTime && ({
    key: "time",
    label: "READ",
    value: readingTime
  }), level && ({
    key: "level",
    label: "LEVEL",
    value: level
  }), prereqs && prereqs.length > 0 && ({
    key: "prereqs",
    label: "PREREQS",
    value: prereqs.join(" · ")
  })].filter(Boolean);
  if (items.length === 0) return null;
  return <div className="gv-page-meta" role="group" aria-label="Page metadata">
      {items.map(item => <div className="gv-page-meta__item" key={item.key}>
          <span className="gv-page-meta__label">{item.label}</span>
          <span className="gv-page-meta__value">{item.value}</span>
        </div>)}
    </div>;
};

export const Def = ({term, type, defaultValue, required, children}) => <>
    <dt className="gv-def-list__term">
      <span className="gv-def-list__name">{term}</span>
      {type && <span className="gv-def-list__type">{type}</span>}
      {required && <span className="gv-def-list__required">required</span>}
      {defaultValue !== undefined && <span className="gv-def-list__default">
          default <code>{defaultValue}</code>
        </span>}
    </dt>
    <dd className="gv-def-list__description">{children}</dd>
  </>;

export const DefList = ({children}) => <dl className="gv-def-list">{children}</dl>;

export const ArrowItem = ({children}) => <li className="gv-arrow-item">
    <span className="gv-arrow-item__arrow" aria-hidden="true">{"↳︎"}</span>
    <span className="gv-arrow-item__content">{children}</span>
  </li>;

export const ArrowList = ({children}) => <ul className="gv-arrow-list">{children}</ul>;

export const FeatureTable = ({columns, rows, featuredRow, caption, variant}) => <div className={`gv-feature-table ${variant ? `gv-feature-table--${variant}` : ""}`}>
    <table>
      {caption && <caption className="gv-feature-table__caption">{caption}</caption>}
      <thead>
        <tr>
          {columns.map(col => <th key={col.key} scope="col">
              <span className="gv-feature-table__header">{col.label}</span>
            </th>)}
        </tr>
      </thead>
      <tbody>
        {rows.map((row, i) => {
  const rowClass = [i === featuredRow ? "gv-feature-table__row--featured" : "", row.chosen ? "gv-feature-table__row--chosen" : ""].filter(Boolean).join(" ");
  return <tr key={i} className={rowClass}>
              {columns.map(col => {
    const v = row[col.key];
    if (variant === "tradeoff" && Array.isArray(v)) {
      const pill = col.key === "pros" ? "gv-feature-table__pill--pro" : col.key === "cons" ? "gv-feature-table__pill--con" : "";
      return <td key={col.key}>
                      {v.map((item, j) => <span key={j} className={`gv-feature-table__pill ${pill}`}>{item}</span>)}
                    </td>;
    }
    return <td key={col.key}>{v}</td>;
  })}
            </tr>;
})}
      </tbody>
    </table>
  </div>;

export const BrandCard = ({eyebrow, title, href, icon, badge, cta, featured = false, horizontal = false, children}) => {
  const classes = ["gv-card", featured && "gv-card--featured", horizontal && "gv-card--horizontal"].filter(Boolean).join(" ");
  const inner = <>
      {icon && <span className="gv-card__icon" aria-hidden="true">
          <Icon icon={icon} />
        </span>}
      <div className="gv-card__main">
        {(eyebrow || badge) && <div className="gv-card__meta">
            {eyebrow && <Eyebrow label={eyebrow} tone="muted" />}
            {badge && <span className="gv-card__badge">{badge}</span>}
          </div>}
        <h3 className="gv-card__title">{title}</h3>
        {children && <div className="gv-card__body">{children}</div>}
        {cta && <span className="gv-card__cta">
            {cta}
            <svg className="gv-card__cta-arrow" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <line x1="7" y1="17" x2="17" y2="7" />
              <polyline points="7 7 17 7 17 17" />
            </svg>
          </span>}
      </div>
    </>;
  return href ? <a className={classes} href={href}>
      {inner}
    </a> : <div className={classes}>{inner}</div>;
};

export const Eyebrow = ({label, tone = "default"}) => <div className={`gv-eyebrow gv-eyebrow--${tone}`}>
    <span className="gv-eyebrow__bar" aria-hidden="true" />
    <span className="gv-eyebrow__label">{label}</span>
  </div>;

<Eyebrow label="CONCEPT" />

<PageMeta audience="Developers" readingTime="6 min" prereqs={["Multi-tenant model"]} level="Concepts" />

There are two distinct identity surfaces on the platform, and conflating them is the most common modeling mistake. **Operators** are the people who build a workspace — they sign in with their own gavAI account and hold scopes against the workspace's API. **End users** are the people who *use* the published app — they sign in to that workspace's app, not to gavAI. By the end of this page you will know what the end-user identity surface looks like, what authentication methods it supports, and how end-user sessions appear in capabilities and audit.

## Two identity surfaces, side by side

```mermaid theme={null}
flowchart LR
    subgraph Op[Operator surface]
      OUser[Workspace owner / developer]
      Op_Auth[OAuth 2.1 + PKCE<br/>API keys]
    end
    subgraph EU[End-user surface — per workspace]
      EUser[Visitor / customer]
      EU_Auth[Passkey<br/>Email + password<br/>Federated SSO]
    end
    OUser --> Op_Auth --> ConsoleAPI[Console + Public API]
    EUser --> EU_Auth --> Runtime[Workspace runtime<br/>at the published app URL]
```

The pools are independent. A user with a `customer@example.com` account on Acme's app does not have a gavAI account. An operator who can patch Acme's pages does not, by virtue of that, have an end-user record inside Acme's app. The two never share a session, a credential, or a token.

The control-plane operator surface is documented under [Authentication](/api-reference/authentication). This page documents the end-user surface, which is what your visitors see when they sign in to the app you built.

## What the runtime supports

Each workspace's identity service supports the patterns end users actually expect from a modern app. You enable the ones you want on the workspace's auth configuration — you do not build them.

<FeatureTable
  columns={[
{ key: "method", label: "Method" },
{ key: "what", label: "What it gives the end user" },
{ key: "use_when", label: "Use it when" }
]}
  rows={[
{
  method: "Passkey",
  what: "Public-key credentials registered against the user's device or password manager. No password to forget; phishing-resistant by design.",
  use_when: "You want the strongest default for consumer apps. Pair with a fallback for users on devices without passkey support."
},
{
  method: "Email + password",
  what: "Classic flow. Password is checked against the workspace's password policy on every change.",
  use_when: "You need a baseline that works on every device, every browser, every recovery scenario."
},
{
  method: "Federated SSO",
  what: "OAuth/OIDC sign-in delegated to Google, Apple, Microsoft, or a workspace-configured custom identity provider.",
  use_when: "Your users already have accounts elsewhere and you want zero-friction sign-up."
}
]}
/>

A single workspace can offer all three at once. The end user picks the method on the sign-in page; the platform unifies them under one identity record once they have signed in.

## Password reset, MFA, and lockout

All three are platform-owned. You configure them, you do not write them.

<ArrowList>
  <ArrowItem>**Password reset.** A reset is initiated from the sign-in page. The runtime emails a one-time link through the workspace's email gateway, validates the link on click, and enforces the password policy on the new password.</ArrowItem>
  <ArrowItem>**MFA.** When required by workspace configuration, the runtime steps a sign-in through an MFA challenge after the primary credential check. One-time codes (TOTP) are the default; passkey as a second factor is supported on workspaces with passkey enabled.</ArrowItem>
  <ArrowItem>**OTP lockout.** Repeated incorrect MFA codes lock the account for a configurable cool-down. The lockout is per-account and the response shape is identical whether the account exists or not — neither attackers nor users can use the response to enumerate accounts.</ArrowItem>
</ArrowList>

## Session lifetime and refresh

End-user sessions are short-lived access tokens paired with a refresh token. The defaults — access TTL, refresh TTL, idle timeout — are workspace-configurable and applied consistently across passkey, password, and federated sign-in.

<DefList>
  <Def term="Access TTL">
    How long an access token is accepted before the runtime asks for a refresh. Short by default — minutes, not hours.
  </Def>

  <Def term="Refresh TTL">
    How long a refresh token is valid. Days or weeks, depending on workspace policy. Refresh tokens are single-use and rotate on every renewal.
  </Def>

  <Def term="Idle timeout">
    How long a session can sit without activity before the runtime invalidates it server-side. Independent of TTL — a user with a valid refresh token can still be logged out for inactivity.
  </Def>

  <Def term="Absolute lifetime">
    A cap on the total wall-clock age of a session regardless of refreshes. After this, the user must re-authenticate. Useful for high-stakes apps.
  </Def>
</DefList>

A workspace owner can revoke any end-user session immediately — the next refresh attempt fails, and any in-flight access token is rejected on next use.

## End users in capabilities

The `authUser` capability returns the currently signed-in end user's profile and roles. Capabilities that act on the end user's behalf — `runQuery` reading rows owned by them, `storeFile` writing to their bucket prefix — automatically pick up the session and resolve ownership against it.

Two consequences worth holding:

<ArrowList>
  <ArrowItem>**Ownership is checked at the capability layer.** A `runQuery` that filters by `customer_id = $current_user.id` is the standard pattern. The runtime supplies `$current_user` from the session; the schema enforces who can read what.</ArrowItem>
  <ArrowItem>**Anonymous traffic is its own principal.** Public pages serve without an end-user session. Capabilities that require one return `401 unauthorized` with `details.hint = "sign_in_required"`; pages can guard sections of UI with the same check.</ArrowItem>
</ArrowList>

## End users in audit

Audit records that are caused by an end-user action carry the end-user id, not the workspace owner's id. A capability dispatched on the end user's behalf is attributed to them in the chain. This matters for compliance: "did the customer authorize this?" has a verifiable answer.

The end-user id format is workspace-scoped. The same email registered on two workspaces produces two distinct ids — there is no cross-workspace end-user identity by design.

## Where end-user data lives

End-user accounts live in the workspace's per-tenant identity service. Profile fields, hashed credentials, and session state are stored alongside the workspace's other data — not in a shared identity pool. The same isolation guarantees as the [multi-tenant model](/security/multi-tenant-isolation) apply: an end user in workspace A cannot be resolved to an account in workspace B.

The trade-off is that end users do not have a portable identity across workspaces. A customer who uses Acme's app and also uses Globex's app holds two unrelated accounts — that is the right answer for security and the right answer for tenant isolation.

## When this surface is the right answer (and when not)

Use the platform's end-user identity when:

<ArrowList>
  <ArrowItem>**The app needs sign-in for end users.** Anything with personalization, ownership, or per-user data.</ArrowItem>
  <ArrowItem>**You want one identity service per workspace.** That's the default — each workspace gets its own pool with no special setup.</ArrowItem>
  <ArrowItem>**You want passkey, MFA, and federated SSO without building them.** All three ship; you enable what you want.</ArrowItem>
</ArrowList>

Don't use it when:

<ArrowList>
  <ArrowItem>**You already have a customer identity system** and you want gavAI to defer to it. Federated SSO with your own OIDC provider is the integration path — the workspace's identity service still issues the session, but the credential is yours.</ArrowItem>
  <ArrowItem>**You want a single end-user identity across multiple workspaces.** That is not the model. Workspace-scoped end users are the design, not a limitation we lift later.</ArrowItem>
</ArrowList>

## Related

<Columns cols={2}>
  <BrandCard eyebrow="GUIDE" title="End-user authentication" href="/guides/end-user-auth" icon="user">
    Step-by-step setup of sign-in for the people who use your published app.
  </BrandCard>

  <BrandCard eyebrow="CAPABILITY" title="authUser" href="/capabilities/auth-user" icon="id-card">
    Read the signed-in user's profile and roles from inside a page or workflow.
  </BrandCard>

  <BrandCard eyebrow="SECURITY" title="Multi-tenant isolation" href="/security/multi-tenant-isolation" icon="building-shield">
    Why end-user pools are per-workspace and what stops them from crossing.
  </BrandCard>

  <BrandCard eyebrow="REFERENCE" title="Operator authentication" href="/api-reference/authentication" icon="key">
    The separate surface for the people who build the workspace (operators).
  </BrandCard>
</Columns>
