Skip to main content

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 runs on the platform’s auth worker, so you don’t run an OAuth flow, mint JWTs, or manage sessions. The SDK ships React providers and components that wrap Better Auth, plus a verifyJwt 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> to everything inside it.
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.

Auth-state checks in components

Use useAuth().isSignedIn for the “is the user signed in?” check. It updates immediately on sign-in and sign-out:
import { useAuth } from 'deepspace'

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

  if (!isLoaded) return <Skeleton />
  if (!isSignedIn) return <SignInPrompt />
  return <SignedInView userId={userId} />
}
Don’t gate on useUser().user 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.

<AuthGate> props

PropTypeDescription
fallbackReactNodeUI 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.
redirectOnSignOutstringWhere 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:
import { AuthGate } from 'deepspace'

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

Sign-in UI

<AuthOverlay /> is a styled modal sign-in component:
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:
<AuthOverlay providers={['google']} />              // Google + email/password
<AuthOverlay providers={['google', 'github']} />    // Google + GitHub + email/password

Conditional rendering

For small one-off bits of UI, <SignedIn> and <SignedOut> are shorthand for the isSignedIn branch:
import { SignedIn, SignedOut } from 'deepspace'

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

Signing out

Call signOut - a thin re-export of Better Auth’s client method:
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.
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.

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:
// 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

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 />}
    </>
  )
}
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.
// 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.

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:
// 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

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.
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.
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.

Next steps