# Auth

fuast signs people in for you. You never configure OAuth, store passwords, or verify tokens. Handlers read the verified caller from `ctx.auth`, and clients read it from `useAuth()`.

## Accounts

A fuast account is a person with a verified email address. They sign in on the fuast platform with email and password or Google. An app never sees passwords or provider tokens, only the identity:

| Field | Value for an account |
| --- | --- |
| `userId` | Stable, opaque id. The same in every app. |
| `provider` | `"fuast"` |
| `isSignedIn` | `true` |
| `displayName` | The account's name. |
| `email`, `emailVerified`, `picture` | Profile data. |
| `workspace` | `{ id, name }`, the account's workspace. |

`userId` is the authorization key. Email, name, and picture are profile data: they can change, and they are not proof of access. Never key ownership on email.

## Workspaces

Every account belongs to one workspace. An account with a company email joins the workspace for that email domain after verifying the address. Personal email domains (gmail.com and friends) get a workspace of one. Every app belongs to the workspace of the account that first deployed it. See [teams](https://fuast.com/docs/teams.md).

## Who can open an app

The app's sharing mode decides who reaches the UI and can call its queries, mutations, and actions:

| Sharing | Who gets in | Guests |
| --- | --- | --- |
| `private` (default) | The owner, workspace admins, and people on the app's allow-list. | No |
| `workspace` | Every member of the app's workspace, plus the allow-list. | No |
| `public` | Anyone with the link. | Yes, unless `requireSignIn` |

Someone without access sees a fuast page that lets them sign in or request access. Requests show up in `fuast users list`. Change sharing with `fuast share --visibility ...`.

Private and workspace apps only ever see signed-in accounts from the browser, so `ctx.auth.isSignedIn` is `true` for every UI call. Write handlers with `requireIdentity()` or `requireSignedIn()` anyway, so the app stays correct if you make it public later.

## Guests on public apps

On a public app without `requireSignIn`, a visitor with no session gets a guest identity automatically. The browser holds it in an HttpOnly cookie for that app only, valid for 30 days.

An [unclaimed preview](https://fuast.com/docs/reference.md#deploy-without-an-account) works the same way, except that everyone is a guest: an app nobody owns yet has no sign-in, so `requireSignedIn()` always fails there and a `requireSignIn` app cannot be used until it is claimed.

| State | `userId` | `provider` | `isGuest` | `isSignedIn` |
| --- | --- | --- | --- | --- |
| No session | `null` | `null` | `false` | `false` |
| Guest | string | `"guest"` | `true` | `false` |
| Account | string | `"fuast"` | `false` | `true` |

Check `isGuest` and `isSignedIn` separately. Neither is true without a session. A user id is not a credential: never create your own guest ids, accept one as an argument, or infer anything from its shape.

Clearing browser data, another browser, or another device means a new guest. Signing in before that keeps the data.

## Require an identity

```ts
const { userId } = ctx.auth.requireIdentity();   // guest or account
const me = ctx.auth.requireSignedIn();           // account only
```

`requireIdentity()` returns the identity with a non-null `userId`, or throws an error with code `unauthenticated` and the message `Sign in or start a guest session first.`. `requireSignedIn()` also rejects guests, with code `sign_in_required`.

The guard identifies the caller. It does not authorize access to rows. Filter user-owned reads by `userId` and re-check ownership before updates and deletes.

## Require sign-in for the whole app

```ts
export default capsule({
  auth: { requireSignIn: true },
  // ...
});
```

With `requireSignIn: true`, guests and callers without a session cannot run any query, mutation, action, or endpoint, or use the app's storage routes. The app shell and the sign-in page still load. The default is `false`.

Use `requireSignedIn()` inside specific handlers when only some operations need an account. On a public app that accepts webhooks, leave `requireSignIn` off and check the webhook secret in the endpoint.

## Auth in the client

```tsx
import { canAccessApp, retryAuth, SignInButton, useAuth } from "fuast/client";
import type { ComponentChildren } from "preact";

function Gate({ children }: { children: ComponentChildren }) {
  const auth = useAuth();
  if (auth.isLoading) return null;
  if (canAccessApp()) return <>{children}</>;
  return (
    <section>
      {auth.error ? <p role="alert">{auth.error}</p> : <p>Sign in to use this app.</p>}
      {auth.error ? <button type="button" onClick={() => void retryAuth()}>Retry</button> : null}
      <SignInButton />
    </section>
  );
}
```

- `useAuth()` returns `{ isLoading, error, requireSignIn, userId, isGuest, isSignedIn, displayName, email?, picture?, workspace? }`. The page ships with its session, so `isLoading` is normally `false` on the first render.
- `canAccessApp()` is `true` when session setup finished without an error, a guest or account exists, and the app's sign-in policy is met. Mount data components only when it is true.
- `<SignInButton />` or `signIn()` sends the user to fuast sign-in and back to the same page. `SignInWithGoogle` is an alias, kept for code written against Lakebed.
- `signOut()` ends the account session in this app. On a public app the user continues as a fresh guest.
- `retryAuth()` retries a failed session setup or guest upgrade. Show it next to `auth.error`, with `<SignInButton />`.

Client checks only choose what to render. The server enforces access.

## Guest upgrade

When a guest signs in on a public app in the same browser, fuast upgrades them:

1. fuast verifies both the guest session and the account.
2. Your `auth.onGuestUpgrade` hook runs, if you declared one.
3. Every `userId()` field in every table that equals the guest's id is rewritten to the account's id.

Steps 2 and 3 are one transaction. If anything fails, nothing changes and `auth.error` lets the user retry. A guest is upgraded at most once. Rows the account already had stay. Only `userId()` fields move: plain strings, `json()` values, and snapshots like a stored author name do not.

Use the hook for app-specific merge rules, for example keeping the account's settings when both have a row:

```ts
import { capsule, string, table, userId } from "fuast/server";

export default capsule({
  schema: {
    settings: table({ ownerId: userId(), theme: string() }).index("by_owner", ["ownerId"])
  },
  auth: {
    onGuestUpgrade: async (ctx, { guestUserId, userId }) => {
      const mine = await ctx.db.settings.withIndex("by_owner", (q) => q.eq("ownerId", userId)).first();
      if (!mine) return;
      const guest = await ctx.db.settings.withIndex("by_owner", (q) => q.eq("ownerId", guestUserId)).collect();
      for (const row of guest) await ctx.db.settings.delete(row.id);
    }
  }
});
```

The hook can read and write `ctx.db` and nothing else that leaves the database: no `fetch`, email, storage writes, or team writes. Its transaction can roll back and retry, and outside effects cannot.

## Endpoint credentials

Two headers, two owners. Keep them straight:

| Header | Belongs to | On endpoints | On `/api/call`, `/__fuast/*`, storage |
| --- | --- | --- | --- |
| `X-Fuast-Token` | fuast | The caller's fuast identity. Verified, then removed before your handler runs. | Also accepted. |
| `Authorization` | Your app | Passed through untouched. Never creates a fuast identity. Use it for webhook secrets, Bearer, or Basic auth you check yourself. | `Bearer <fuast token>` is the fuast credential. |

A fuast identity reaches an endpoint in one of two ways:

- **Same-origin browser requests** carry the app's session or guest cookie automatically. `fetch(appUrl("/api/status"))` from your client is identified with no extra header.
- **`X-Fuast-Token`**, sent raw without a `Bearer` prefix. It accepts exactly these credentials:

| Credential | Accepted in `X-Fuast-Token` | Caller becomes |
| --- | --- | --- |
| Identity token from `getIdentity().token` | Yes, for the app it was issued for only | That person, or that guest |
| Personal token from `fuast token create` (no `--app`) | Yes | The token's account |
| The token `fuast login` saved (`FUAST_TOKEN` from a login) | Yes | The logged-in account |
| App-scoped deploy token (`fuast token create --app`) | Never | No identity |
| Anything else, expired, or forged | No | No identity |

Use the identity token for cross-origin browser callers, for example a browser extension or another tab you control. Use a personal token for headless agents and scripts that should act as you, for example `curl -H "X-Fuast-Token: $FUAST_TOKEN" https://my-app.fuast.com/api/status`. The account still needs access to the app for `ctx.auth` to be set, exactly as in the browser.

```ts
import { appUrl, getIdentity } from "fuast/client";

const { token } = getIdentity();
const res = await fetch(appUrl("/api/status"), { headers: token ? { "X-Fuast-Token": token } : {} });
```

`getIdentity()` returns the auth state with `token` and `expired`. The identity token works only for this app and expires after 12 hours; the client refreshes it from `GET <app>/__fuast/auth/token` (same-origin cookie only, returns `{ token, expiresAt }`, 401 without a session, 403 without access), so read it right before each request. Send it only to this app's origin, never to a third party. A personal token can do anything you can on the platform: send it only to apps you trust.

Both headers can appear together:

```http
X-Fuast-Token: <fuast identity token or personal token>
Authorization: Bearer <your webhook secret>
```

Callers without either have `ctx.auth.userId === null`. Webhooks and other machines usually have neither, so protect machine-called endpoints with your own secret from `ctx.env`. Any fuast token in the `Authorization` header is not a fuast identity on an endpoint: it reaches your handler like any other header value. `X-Fuast-Token` is always removed before your handler runs.

## The HTTP call API

Scripts and tests can call queries, mutations, and actions without a browser:

```sh
curl -X POST https://my-app.fuast.com/api/call \
  -H "Authorization: Bearer $FUAST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"kind":"query","name":"todos","args":[]}'
```

This route accepts the app session cookie or `Authorization: Bearer <fuast token>` and applies the same sharing rules as the browser. `GET /api/me` returns the identity the app sees. See [reference](https://fuast.com/docs/reference.md#http-call-api).

## App origins

Each hosted app runs on its own origin, `https://<slug>.<apps domain>/` (on fuast.com, `https://<slug>.fuast.com/`). That origin gets its own session cookie through a one-time code redirect from the platform at sign-in. Platform cookies are host-only and never reach an app origin, so one app's JavaScript cannot act as you on the platform or read another app's pages.

All apps share the apps domain, and until that domain is on the [Public Suffix List](https://publicsuffix.org/), browsers treat every app on it as the **same site**:

- `SameSite` cookies do not separate your app from other apps. If a writable endpoint relies on cookies, check the `Origin` header and accept only your app's origin.
- Another app can set a cookie for the parent domain. If you set your own cookie, use a `__Host-` name with `Secure; Path=/` and no `Domain`, so no other subdomain can overwrite it.
- Treat every other app as untrusted, including your own.

`fuast domains add <name>` adds `<name>.<apps domain>` as an extra hostname for the app. It gets the same origin rules.

Self-hosted platforms without a wildcard domain serve apps under `/a/<slug>/` on the platform origin instead. There every app shares one origin, so path mode suits only platforms where every app is trusted. Client routes from `fuast/client` work in both modes. Build endpoint URLs with `appUrl("/api/...")` from `fuast/client` so they work in both modes; a root-relative `fetch("/api/...")` targets the app only on an app origin. See [security](https://fuast.com/docs/security.md#app-origins-and-cookies).

## Local identities

`fuast dev` needs no auth setup:

- By default you are the signed-in account `dev`.
- `?fuast_user=alice` in the URL makes that tab the signed-in account `alice`. Use two tabs to test two people.
- `?fuast_guest=1` makes the tab a guest, so you can test public-app flows and guest upgrade.
- `fuast auth as alice` changes the default identity for the browser and for CLI commands that call `fuast dev`. `fuast auth reset` restores `dev`.

All local accounts share one local workspace, so `ctx.workspace.members()` lists every name you have used.
