> ## 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 authentication

> Gate blocks by sign-in state and user role via authUser.

export const ErrorTable = ({errors}) => <div className="gv-error-table" role="table" aria-label="Errors and fixes">
    <div className="gv-error-table__head" role="row">
      <span className="gv-error-table__col-head" role="columnheader">Error</span>
      <span className="gv-error-table__col-head" role="columnheader">Cause</span>
      <span className="gv-error-table__col-head" role="columnheader">Fix</span>
    </div>
    {errors.map((err, i) => <div className="gv-error-table__row" role="row" key={i}>
        <code className="gv-error-table__code" role="cell">{err.code}</code>
        <div className="gv-error-table__cause" role="cell">{err.cause}</div>
        <div className="gv-error-table__fix" role="cell">{err.fix}</div>
      </div>)}
  </div>;

export const ExpectedOutput = ({caption = "Expected output", children}) => <div className="gv-output">
    <div className="gv-output__label">
      <svg className="gv-output__icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <polyline points="15 10 20 15 15 20" />
        <path d="M4 4v7a4 4 0 0 0 4 4h12" />
      </svg>
      <span>{caption}</span>
    </div>
    <div className="gv-output__body">{children}</div>
  </div>;

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 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 NumberedStep = ({n, title, children}) => <li className="gv-step">
    <span className="gv-step__index">{n}</span>
    <div className="gv-step__body">
      <h3 className="gv-step__title">{title}</h3>
      <div className="gv-step__content">{children}</div>
    </div>
  </li>;

export const NumberedSteps = ({children}) => <ol className="gv-steps">{children}</ol>;

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 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="GUIDE" />

<PageMeta audience="Developers" readingTime="10 min" level="Intermediate" prereqs={["gavAI workspace"]} />

By the end of this guide your page renders different content depending on whether the visitor is signed in and which roles they hold. The `authUser` capability returns the active end-user's profile during page rendering, so you gate any block with no backend code. You will add a sign-in entry point, read the active profile, and use `visible_if` expressions to gate by auth state and by role.

## Prerequisites

<ArrowList>
  <ArrowItem>A gavAI workspace.</ArrowItem>
  <ArrowItem>An end-user auth pool. Auth pools are provisioned automatically when you create a workspace — no extra setup.</ArrowItem>
</ArrowList>

## Steps

<NumberedSteps>
  <NumberedStep title="Add a sign-in entry point">
    The integration depends on which shell your page uses:

    <ArrowList>
      <ArrowItem>If your page uses a **NavShell** block, sign-in may be handled by the shell's built-in auth flow.</ArrowItem>
      <ArrowItem>For an inline sign-in, add a **Form** block and point it at the auth flow for your pool.</ArrowItem>
    </ArrowList>

    <Note>
      The public source does not specify whether sign-in is a dedicated block type, a `NavShell` prop, or a separate built-in. Confirm with the platform team before shipping. For now, open the Builder, drag a **Form** block onto the page, and configure it under **Form → Auth settings**.
    </Note>
  </NumberedStep>

  <NumberedStep title="Read the active user">
    Call `authUser` from any capability hook or expression on the page. It takes no arguments and operates on the active session.

    When a user is signed in, `authUser` returns a profile of this shape:

    <ExpectedOutput caption="authUser profile">
      ```json theme={null}
      {
        "user_id": "usr_01H8X3K2YQZ4V7N9P5R6S7T8U9",
        "email": "ada@example.com",
        "email_verified": true,
        "display_name": "Ada Lovelace",
        "roles": ["member", "admin"],
        "metadata": { "plan": "pro" }
      }
      ```
    </ExpectedOutput>

    When no user is signed in, `authUser` returns `null`. Always handle the `null` case — every public page receives anonymous visitors.

    `display_name` and `metadata` are optional fields and may be absent on some users.
  </NumberedStep>

  <NumberedStep title="Show a block only when signed in">
    Every block accepts a `visible_if` prop. Set it to an expression that evaluates to true for signed-in visitors:

    ```json theme={null}
    {
      "block": "DetailPanel",
      "props": {
        "schema_table": "customers",
        "record_id_param": "id",
        "fields": ["name", "email", "status", "notes"],
        "editable": true,
        "visible_if": "{{authUser != null}}"
      }
    }
    ```

    When `authUser` is `null`, the block is removed from the rendered output entirely — not just hidden with CSS. Anonymous visitors never receive the markup.

    <Note>
      The exact `visible_if` expression syntax — delimiters, operator support, and available globals — is not fully documented in the public source. The expression above is illustrative; confirm with the platform team before shipping.
    </Note>
  </NumberedStep>

  <NumberedStep title="Show admin-only content">
    Extend the `visible_if` expression to check a specific role:

    ```json theme={null}
    {
      "block": "Table",
      "props": {
        "schema_table": "customers",
        "columns": ["name", "email", "status", "created_at"],
        "sortable": true,
        "filterable": true,
        "page_size": 25,
        "visible_if": "{{authUser.roles contains 'admin'}}"
      }
    }
    ```

    Roles are workspace-defined strings. Set them in the Builder under **Workspace → Auth → Roles**. A user can hold multiple roles; the check is true if the user's `roles` array includes the named value.

    <Note>
      Whether `contains` is the correct operator for array-membership tests in `visible_if` expressions is not confirmed in the public source. Verify with the platform team before shipping.
    </Note>

    <Warning>
      `visible_if` controls rendering. It is **not a security boundary**. Row-level access is enforced server-side by `runQuery` using the `authUser` context. Never rely on `visible_if` alone to protect sensitive data.
    </Warning>
  </NumberedStep>

  <NumberedStep title="Sign out">
    Give users a way to end their session — a button that clears the active session and redirects to the home or login page.

    <Note>
      The exact sign-out mechanism (a capability call, a `NavShell` prop, a special Form action, or a platform-provided URL) is not specified in the public source. Confirm with the platform team. In the Builder, look for **Auth → Sign out** actions in the button and Form settings.
    </Note>
  </NumberedStep>
</NumberedSteps>

## Troubleshooting

<ErrorTable
  errors={[
{
  code: "Block visible to anonymous visitors",
  cause: "The visible_if expression evaluated to truthy.",
  fix: "Log the value of authUser at render time and confirm the operator is correct.",
},
{
  code: "Admin role check never matches",
  cause: "The role string is case-sensitive.",
  fix: "Match the role defined under Workspace → Auth → Roles exactly, including case.",
},
{
  code: "Sensitive data leaks via runQuery",
  cause: "visible_if only hides the block — the query still runs server-side with the active session.",
  fix: "Enforce row-level rules in your schema. Treat visible_if as UX, not security.",
},
]}
/>

## What you built

You wired a sign-in entry point, learned how `authUser` returns the active profile (or `null` for anonymous visitors), and used `visible_if` to gate blocks by sign-in state and by role. Your page now renders different content for anonymous users, signed-in members, and admins — with no custom backend.

## Next steps

<Columns cols={2}>
  <BrandCard title="authUser reference" href="/capabilities/auth-user" cta="Read reference">
    Full input/output schema, null handling, and session lifecycle for the authUser capability.
  </BrandCard>

  <BrandCard title="Multi-tenant isolation" href="/security/multi-tenant-isolation" cta="Read more">
    How gavAI enforces workspace and row-level boundaries so one tenant never sees another's data.
  </BrandCard>

  <BrandCard title="PageBlocks overview" href="/pageblocks/overview" cta="Read more">
    The full block catalogue — every block type, its props, and how the document model works.
  </BrandCard>
</Columns>
