golemkit

Core

Authentication

Authentication is Better Auth configured in src/modules/auth/auth.ts — the single source of truth for providers, sessions, and hooks. Three sign-in methods ship enabled:

  • Email + password — with requireEmailVerification enabled: sign-in is blocked until the address is verified, so disposable-email accounts can’t farm the signup credit bonus. Flip it off in auth.ts if you don’t need that guard.
  • Google OAuth — registers itself only when GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are set; the UI hides the button otherwise.
  • Magic links — “Email me a sign-in link” on the sign-in page.

Reading the session

In server code (pages, layouts, route handlers):

import { getSession, requireSession } from "@/modules/auth";

const session = await getSession();        // Session | null
const session = await requireSession();    // redirects to /sign-in if absent

requireSession() in a layout protects an entire subtree — that’s how /dashboard is guarded. In client components, use the hooks from @/modules/auth/auth-client (useSession, signIn, signOut).

Two-layer protection

src/proxy.ts (Next 16’s middleware) does a fast, optimistic cookie check and redirects obvious anonymous visitors. The real validation — hitting the database — runs in the layout. Don’t move authorization into the proxy; Next.js explicitly recommends against it.

Signup side effects

New users get a credit bonus (SIGNUP_BONUS_CREDITS, 15 by default) and a welcome email through Better Auth’s databaseHooks.user.create.after — see it in auth.ts. That hook is the right place for any “on signup” behavior (analytics, default workspace rows), keeping it out of the UI flow. The email is scheduled with after() from next/server: it goes out once the response is sent, and delivery failures are logged, never surfaced as signup errors.

Abuse guards

Signup and magic-link sends are rate limited per client IP (10 per hour, in hooks.before in auth.ts) using the same Postgres rate limiter as the AI routes. Per-user limits can’t help here — the attacker isn’t a user yet, and every minted account would burn a signup bonus of real AI Gateway spend.

Account settings

/dashboard/settings ships the screens every SaaS needs: rename, change password (revokes other sessions), change email (confirmed by email), and GDPR-grade account deletion gated by password plus a typed confirmation. Deletion is safe because every user-owned table references user.id with onDelete: “cascade” — credits, transactions, and documents go with the account. The forms live in src/modules/auth/components/settings-forms.tsx; the server side is just user.changeEmail and user.deleteUser enabled in auth.ts.

Auth emails

Magic-link and verification emails go through src/modules/email (Resend). When RESEND_API_KEY is unset they’re logged to the server console instead, so the flows stay testable locally without an account. In production a missing key fails loudly: sending throws rather than letting sign-in links end up in your server logs.

The admin area

/dashboard/admin is gated by a deliberately simple allow-list: emails in the ADMIN_EMAILS env var (comma separated). Everyone else gets a 404, so the page’s existence stays private. Inside: system stats (users, credits in circulation, purchased, spent), a searchable user table, recent credit activity, and a grant/deduct form whose adjustments go through the same ledger as every other transaction. The queries live in src/modules/admin; full RBAC is intentionally out of scope until you need teams.

The user table can also deactivate an account: it sets a banned flag and revokes every session, so the lockout is immediate. A single Better Auth hook (databaseHooks.session.create.before in auth.ts) rejects sign-ins for banned users across all methods — password, Google, and magic link — with a clear “account disabled” message. Admins cannot deactivate themselves.