This repo has a two-layer auth system. Learn the layers and you will never accidentally expose a protected route or block a public one. The rule: layout guards pages, getUserId() guards mutations.
This 19-line file protects the entire (app) route group:
// app/(app)/layout.tsx - line 7-8
const session = await getSession()
if (!session?.user) redirect("/sign-in")
Every page under app/(app)/* is behind this gate. If not logged in, you get bounced to /sign-in before any page renders.
/dashboard, /templates, /assessments, /gallery, /reports, /settings, /usage, /admin - everything in the (app) group.
/sign-in, /sign-up, /respond/[token] - these live outside app/(app)/ and have no layout guard. Respondents never log in.
Every server action starts with this call:
// lib/auth-helpers.ts
export async function getUserId(): Promise<string> {
const session = await auth.api.getSession({ headers: await headers() })
if (!session?.user) throw new Error("Unauthorized")
return session.user.id
}
Even if someone bypasses the layout by calling a server action directly from curl, getUserId() checks the session cookie. If invalid, it throws Unauthorized.
// app/actions/templates.ts - first line of every action
const userId = await getUserId()
// app/actions/assessments.ts - same pattern
const userId = await getUserId()
// app/actions/llm-keys.ts - same pattern
const userId = await getUserId()
This route lives outside app/(app)/ and does two checks that are NOT auth:
// app/respond/[token]/page.tsx - lines 16-38
const assessment = await db.select().from(assessments)
.where(eq(assessments.inviteToken, token)).limit(1)
if (!assessment) notFound() // Check 1: token exists
if (assessment.status === 'closed') // Check 2: assessment is active
return "Assessment Closed"
The inviteToken is a 32-character nanoid (unguessable). No login required. The assessment owner controls access by toggling status between draft, active, and closed.
A user visits /templates/new without being logged in. What happens?
Someone calls saveTemplate server action via curl without a session cookie. What happens?
Read lib/auth-helpers.ts (14 lines) and app/(app)/layout.tsx (19 lines). Together they are the entire auth system for pages and mutations.
Citations: layout guard from app/(app)/layout.tsx:7-8, action guard from lib/auth-helpers.ts:6-9, public route from app/respond/[token]/page.tsx:16-38.
Follow a template from creation through AI generation, assessment setup, invite link, response submission, to report aggregation. Every file touched, in order.