The Sanctuary — every door

You are in The Forge.

Unnamed ground — waiting, not missing.Unnamed ground — waiting, not missing.Unnamed ground — waiting, not missing.Unnamed ground — waiting, not missing.Unnamed ground — waiting, not missing.Unnamed ground — waiting, not missing.Unnamed ground — waiting, not missing.Unnamed ground — waiting, not missing.Unnamed ground — waiting, not missing.Unnamed ground — waiting, not missing.

Ground you have walked is named. The rest is drawn but unnamed — it is waiting, not missing, and nothing is required of you to find it.

Every door stays where you left it. Esc folds the map.

Technical Documentation

Authentication Flow

A password, or a link if a password is too much today. Two doors, one home.

Last Updated: August 24, 2026Supabase AuthPassword & Magic Link

Overview

We use Supabase Auth, and the door has two ways through it. A password is the first — the form asks for it, and a leaked-password check runs before it is ever used. Beside it, on the same page, stands a magic link: an email that opens the door once, for a day when a password is more than a nervous system has to spare. Neither is a lesser door. Both land in the same place.

The Journey

Sign Up
Name, email, password
Password Checked
Never leaves the device
Acid Test Offered
"Not now" is an answer
Home
Landing at /vessel

Step-by-Step Process

1

A Vessel Signs Up

Visits /signup and gives four things: a username, an email, a password, and the same password again. The Terms and the Privacy Policy are accepted here, by a checkbox that starts unticked.

📍 Code location: components/asgard/auth/SignupForm.tsx

2

The Password Is Checked Before It Is Used

A k-anonymous HaveIBeenPwned range check runs in the browser: the password is hashed there, only the first five characters of the hash are sent, and the comparison happens locally. The password never leaves the device. If HIBP is unreachable the check fails open — the Sanctuary does not lock its own door because a third party is napping.

📍 Code location: lib/auth/pwned.ts

3

Supabase Creates the Vessel

An auth record is created in auth.users. The house's birth chain, handle_new_user(), fills the three-table identity beside it — community_profiles (the public face), user_private (the sovereign shell, own-only by RLS), and vessel_config (presentation defaults, every ceremony off).

📍 Code location: docs/sql/007-the-vessel-arrives.sql

4

The Acid Test Is Offered, Not Imposed

Signup ends with an offer, and 'Not now' is a real answer that leads straight to /vessel. Taking it leads to /questionaire. It is offered again whenever it is wanted — nothing is lost by waiting, and nothing anywhere is gated behind it.

5

The Door Lands at /vessel

Whichever way through — a password, a magic link, or a returning login — the callback exchanges the code for a session and redirects to /vessel, the one room where the Velkomin greeting fires, so no arrival crosses in silence. A failed or spent link returns to /login and says so plainly.

📍 Code location: app/(auth)/callback/route.ts

Client-Side Auth

Browser client for user-facing components. A singleton, so React Strict Mode cannot build two.

lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
import type { Database } from '@/lib/generated/supabase/database.types'

let clientInstance: ReturnType<typeof createBrowserClient<Database>> | null = null

export function createClient() {
  if (clientInstance) return clientInstance

  clientInstance = createBrowserClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )

  return clientInstance
}

Server-Side Auth

Server client for route handlers and server components. It reads and writes the cookie store, which is how a session survives a redirect.

lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { Database } from '@/lib/generated/supabase/database.types'

export async function createServerSupabase() {
  const cookieStore = await cookies()

  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) { return cookieStore.get(name)?.value },
        set(name: string, value: string, options: any) {
          cookieStore.set({ name, value, ...options })
        },
        remove(name: string, options: any) {
          cookieStore.set({ name, value: '', ...options })
        },
      },
    }
  )
}

How Routes Are Guarded

There is no middleware.ts at the repo root — nothing intercepts a request before it reaches a page. Guarding is a component: AuthGuard wraps the pages that need it and decides once, when auth first settles.

components/asgard/auth/AuthGuard.tsx
export default function AuthGuard({
  children,
  requireAuth = true,
  redirectTo = AUTH_ROUTES.LOGIN,
}: AuthGuardProps) {
  const { user, loading } = useAuth()
  const signedInOnArrival = useRef<boolean | null>(null)

  useEffect(() => {
    if (loading) return

    // The bounce is an ENTRY check, not a live leash. Read once, when auth
    // first settles — signing in ON this page must leave the door's own
    // landing in charge.
    if (signedInOnArrival.current === null) signedInOnArrival.current = !!user

    if (requireAuth && !user) {
      // A spent recovery link says so, rather than failing in silence.
      router.push(buildRedirectUrl(redirectTo, pathname))
    } else if (!requireAuth && user && signedInOnArrival.current) {
      router.push(AUTH_ROUTES.DASHBOARD)   // '/vessel'
    }
  }, [user, loading, requireAuth, redirectTo, router, pathname])

  if (loading) return <LoadingSanctuary />
  if (requireAuth && !user) return null

  return <>{children}</>
}

Protected Routes

Every route below exists under src/app. These are all of them — the Sanctuary gates very little.

Protected Routes
/reset-password

AuthGuard requires a live recovery session. Without one it returns you to /login and says the link has been spent — no one transitions unaccompanied.

/login · /signup · /forgot-password

AuthGuard, inverted: a vessel already signed in on arrival is sent to /vessel rather than shown the door twice.

/council/applications/creator · /council/applications/vendor

The form renders only for a signed-in vessel; a visitor is asked to sign in first, and is turned away from nothing else.

✨ Why Both?

  • A password is the fastest way back in for anyone who has one to hand
  • Some days a vessel has not the capacity to reset a password — the link is for those days
  • The password is checked against known breaches without ever leaving the device
  • Neither door tells a stranger whether an address has a home here