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

# Authentication

> Public, gated, and mixed auth configurations for DeepSpace apps.

Authentication runs on the platform's [auth worker](/concepts/architecture#platform-workers), so you don't run an OAuth flow, mint JWTs, or manage sessions. The SDK ships React providers and components that wrap [Better Auth](https://www.better-auth.com/), plus a [`verifyJwt`](/sdk-reference/worker/auth#verifyjwt-config-token) helper for your worker.

## Auth models

DeepSpace apps usually fall into one of three shapes. The scaffold ships the mixed model - the `(protected)/_layout.tsx` route group applies [`<AuthGate>`](/sdk-reference/client/auth#%3Cauthgate-fallback=--redirectonsignout=--/%3E) to everything inside it.

<Tabs>
  <Tab title="Mixed (default)">
    Public pages live at `src/pages/<name>.tsx`. Gated pages go inside the `(protected)/` route group, which applies `<AuthGate>` via its layout.

    ```
    src/pages/
      home.tsx                  public (/home)
      (protected)/
        _layout.tsx             <AuthGate><Outlet /></AuthGate>
        settings.tsx            gated (/settings)
        // add gated pages here
    ```

    The folder name `(protected)/` is wrapped in literal parentheses - generouted treats it as a route group that doesn't appear in the URL. Adding a new gated page is a one-file change: drop it inside `(protected)/`.

    **Best for:** consumer apps with a public landing or marketing surface plus an authenticated app behind sign-in.
  </Tab>

  <Tab title="Fully gated">
    Wrap the entire tree with [`<AuthGate>`](/sdk-reference/client/auth#%3Cauthgate-fallback=--redirectonsignout=--/%3E) in `_app.tsx`, and drop `allowAnonymous` from [`<RecordProvider>`](/sdk-reference/client/records#%3Crecordprovider%3E):

    ```tsx theme={null}
    // src/pages/_app.tsx
    import { Outlet } from 'react-router-dom'
    import { DeepSpaceAuthProvider, AuthGate, RecordProvider, RecordScope } from 'deepspace'
    import Navigation from '../components/Navigation'
    import { APP_NAME, SCOPE_ID } from '../constants'
    import { schemas } from '../schemas'

    export default function App() {
      return (
        <DeepSpaceAuthProvider>
          <AuthGate>
            <RecordProvider>
              <RecordScope roomId={SCOPE_ID} schemas={schemas} appId={APP_NAME}>
                <Navigation />
                <Outlet />
              </RecordScope>
            </RecordProvider>
          </AuthGate>
        </DeepSpaceAuthProvider>
      )
    }
    ```

    Inside an `<AuthGate>` subtree the user is always signed in, so you don't need the anonymous WebSocket path.

    **Best for:** internal tools, paid SaaS, anything where every route requires sign-in.
  </Tab>

  <Tab title="Fully public">
    Delete the scaffold's `src/pages/(protected)/` folder (or remove the `<AuthGate>` from its `_layout.tsx`) so no route is gated. Keep `<RecordProvider allowAnonymous>` - it's already on in the scaffold's `_app.tsx`. Signed-out visitors get the records that the `'*'` role rule in [permissions](/concepts/permissions) grants read access to.

    **Best for:** marketing sites, public catalogs, read-only directories.
  </Tab>
</Tabs>

## Auth-state checks in components

Use [`useAuth().isSignedIn`](/sdk-reference/client/auth#useauth--authstate) for the "is the user signed in?" check. It updates immediately on sign-in and sign-out:

```tsx theme={null}
import { useAuth } from 'deepspace'

function MyComponent() {
  const { isLoaded, isSignedIn, userId } = useAuth()

  if (!isLoaded) return <Skeleton />
  if (!isSignedIn) return <SignInPrompt />
  return <SignedInView userId={userId} />
}
```

<Warning>
  Don't gate on [`useUser().user`](/sdk-reference/client/auth#useuser---user-isloading-refetch-) truthiness alone. `useUser` returns the storage-layer profile (karma, credits, room role) and loads async, so you'll get a flash of "not signed in" while the profile fetch resolves. Use `useAuth().isSignedIn` for state checks and reach for `useUser()` only when you need profile fields.
</Warning>

## `<AuthGate>` props

| Prop                | Type        | Description                                                                                                                                                                                                 |
| ------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fallback`          | `ReactNode` | UI shown to first-visit signed-out users. Defaults to `<AuthOverlay />` rendered without `onClose`, which makes it non-dismissible. Not used when the user signs out mid-session - see `redirectOnSignOut`. |
| `redirectOnSignOut` | `string`    | Where the user lands when they sign out from inside the gate. Defaults to `'/'`. Triggers a full-page reload so cached state can't leak.                                                                    |

Pass a custom fallback to render something other than the default overlay:

```tsx theme={null}
import { AuthGate } from 'deepspace'

<AuthGate fallback={<TeaserPage />}>
  <Dashboard />
</AuthGate>
```

## Sign-in UI

`<AuthOverlay />` is a styled modal sign-in component:

```tsx theme={null}
import { AuthOverlay, useAuth } from 'deepspace'

function App() {
  const { isSignedIn } = useAuth()
  return (
    <>
      <MainContent />
      {!isSignedIn && <AuthOverlay providers={['google', 'github']} />}
    </>
  )
}
```

Render `<AuthOverlay />` without an `onClose` prop and gate on `!isSignedIn`. It auto-hides when the user signs in.

### Providers

By default `<AuthOverlay />` shows GitHub, Google, and email/password. The `providers` prop controls only the OAuth buttons - its type is `Array<'github' | 'google'>`. Email/password sign-in is always rendered:

```tsx theme={null}
<AuthOverlay providers={['google']} />              // Google + email/password
<AuthOverlay providers={['google', 'github']} />    // Google + GitHub + email/password
```

## Conditional rendering

For small one-off bits of UI, [`<SignedIn>`](/sdk-reference/client/auth#%3Csignedin%3E-and-%3Csignedout%3E) and `<SignedOut>` are shorthand for the `isSignedIn` branch:

```tsx theme={null}
import { SignedIn, SignedOut } from 'deepspace'

<SignedIn>
  <UserMenu />
</SignedIn>
<SignedOut>
  <SignInButton />
</SignedOut>
```

## Signing out

Call [`signOut`](/sdk-reference/client/auth#signin-/-signout) - a thin re-export of Better Auth's client method:

```tsx theme={null}
import { signOut } from 'deepspace'

<button onClick={() => signOut()}>Sign out</button>
```

The scaffolded `Navigation.tsx` already calls `signOut()` from the avatar dropdown. Extend the existing one rather than adding a second sign-out control.

<Tip>
  If your app requires sign-in, keep a sign-out control reachable in the signed-in UI. If you replace `Navigation.tsx`, wire `signOut()` into the new shell.
</Tip>

## Server-side verification

For custom API routes that aren't auto-protected, verify the JWT yourself. Add the handler to the existing Hono `app` in the scaffold's `worker.ts` - `verifyJwt` is already imported there, so the `import` line below is redundant if you're extending `worker.ts` in place:

```ts theme={null}
// worker.ts
import { verifyJwt } from 'deepspace/worker'

app.get('/api/me', async (c) => {
  const auth = c.req.header('Authorization') ?? ''
  const token = auth.replace(/^Bearer\s+/i, '')

  const outcome = await verifyJwt({
    publicKey: c.env.AUTH_JWT_PUBLIC_KEY,
    issuer: c.env.AUTH_JWT_ISSUER,
  }, token)

  if (!outcome.result) return c.json({ error: 'unauthorized' }, 401)
  return c.json({ userId: outcome.result.userId, claims: outcome.result.claims })
})
```

`verifyJwt` never throws. It returns `{ result, error?, debug? }`: `result` is `{ userId, claims }` on success or `null` on failure, `error` is the underlying jose error, and `debug` is the decoded `iss`/`aud`/`azp`/`exp` for log lines. Always check `result` before reading the subject.

The scaffold's `wsRoute` handler already calls `verifyJwt` for every WebSocket upgrade, so you only need this pattern for custom HTTP routes.

## Common patterns

### Landing page with an "open app" CTA

```tsx theme={null}
import { Link } from 'react-router-dom'
import { useAuth } from 'deepspace'

function Landing() {
  const { isSignedIn } = useAuth()
  return (
    <>
      <Hero />
      <Pricing />
      {isSignedIn
        ? <Link to="/dashboard">Open the app</Link>
        : <SignInButton />}
    </>
  )
}
```

### Conditional nav links

The scaffold's `Navigation.tsx` filters `src/nav.ts` by the user's room role. Omit `roles` to show an item to everyone; admins see everything regardless.

```ts theme={null}
// src/nav.ts
import type { Role } from './constants'

export interface NavItem {
  path: string
  label: string
  roles?: Role[]
}

export const nav: NavItem[] = [
  { path: '/home', label: 'Home' },                    // visible to everyone
  { path: '/settings', label: 'Settings' },            // visible to everyone
  { path: '/admin', label: 'Admin', roles: ['admin'] },// admin-only
]
```

`Role` is defined in `src/constants.ts` - extend it there to add new roles, then use them in [permissions](/concepts/permissions).

### Hiding nav on the landing route

If your landing page has its own header, gate the global `<Navigation />` on `useLocation()` inside the scaffold's existing `_app.tsx`. Keep the surrounding providers - only the `<Navigation />` line changes:

```tsx theme={null}
// src/pages/_app.tsx - inside the existing App() return
import { useLocation } from 'react-router-dom'

const isLanding = useLocation().pathname === '/'

// ...replace <Navigation /> with:
{!isLanding && <Navigation />}
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="`useToast must be used within ToastProvider` on import">
    The scaffold's `_app.tsx` wraps the tree in the **local** `ToastProvider` (from `src/components/ui`), not the SDK's. Import `useToast` from `../components/ui`, not from `deepspace`. Mixing the two contexts produces this error.
  </Accordion>

  <Accordion title="Safari shows the user as signed-out even after sign-in">
    Safari refuses to set `__Secure-` cookies on `localhost` because the attribute requires HTTPS; Chrome is more lenient. Test against `https://` URLs (or deploy) when verifying Safari behavior.
  </Accordion>

  <Accordion title="Sign-in popup opens but never returns to the app">
    The OAuth flow opens the auth worker at the platform domain, completes sign-in, then redirects back to your app. If the redirect doesn't fire, it's almost always a cookie or HTTPS issue (see above) - open the auth worker's tab and check the browser console.
  </Accordion>
</AccordionGroup>

## Next steps

* [Permissions](/concepts/permissions) - how role-based access control works on collections.
* [Server actions](/guides/server-actions) - privileged worker code that bypasses user RBAC.
* [Auth reference](/sdk-reference/client/auth) - all auth hooks, providers, and components.
* [Worker auth reference](/sdk-reference/worker/auth) - `verifyJwt` and HMAC primitives.
