skills/better-auth/SKILL.md
--- name: better-auth description: Wire Better Auth into a TanStack Start app as the optionality alt-path auth provider when you must own the user table, need EU data residency, or have fully custom auth flows. Default auth is /ro:clerk; alt-at-scale is /ro:workos. Use this skill when user explicitly wants Better Auth, owns-the-table semantics, EU-mandated user storage, or custom auth flows the vendored options cannot satisfy. category: auth argument-hint: [install | add-provider <github|google>
npx skillsauth add RonanCodes/ronan-skills skills/better-authInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Wire Better Auth into a TanStack Start + Drizzle + D1 app. Code-generates schema, server config, route handler, client, and optional OAuth providers and role helpers.
When to use this vs
/ro:clerk//ro:workos. Default auth is/ro:clerk(hosted UI components, free to 10K MAU, fastest first sign-in). Alt-at-scale is/ro:workos(1M MAU free, hosted Admin Portal, B2B SSO ready). Reach for Better Auth when one of these is true:
- You need to own the
userstable for native joins, FKs, and DB-enforced row-level security against merchant-scoped data.- EU data-residency mandate that neither Clerk nor vendored AuthKit can satisfy on their standard plans.
- Fully custom auth flows (unusual onboarding, custom session shape, exotic providers) that Clerk and AuthKit do not bend to.
- Zero vendor lock-in is a hard preference. The Auth.js consolidation under the Better Auth team in 2026 makes this the safest principled-OSS pick available.
If none apply, prefer
/ro:clerk.
/ro:better-auth install # initial wiring (schema + server + client + route)
/ro:better-auth install --email # + email/password with Resend for verification
/ro:better-auth add-provider github # add GitHub OAuth
/ro:better-auth add-provider google # add Google OAuth
/ro:better-auth add-roles # add roles plugin + helpers
/ro:better-auth generate-schema # regen Drizzle schema after config change
/ro:new-tanstack-app or /ro:migrate-to-tanstack)src/db/schema.ts, wrangler.toml with [[d1_databases]])RESEND_API_KEY in ~/.claude/.env if using --emailpnpm add better-auth
pnpm add -D @better-auth/cli
openssl rand -base64 32
Write it to the app's local env (NOT ~/.claude/.env — this is per-app):
# .dev.vars
BETTER_AUTH_SECRET=<generated>
BETTER_AUTH_URL=http://localhost:3000
Push to production as a wrangler secret:
wrangler secret put BETTER_AUTH_SECRET
wrangler secret put BETTER_AUTH_URL # = https://your-app.com
src/lib/auth.tsimport { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/db";
import * as schema from "@/db/schema";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "sqlite", schema }),
emailAndPassword: { enabled: true },
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
});
src/routes/api/auth/$.tsTanStack Start Server Route:
import { createServerFileRoute } from "@tanstack/react-start/server";
import { auth } from "@/lib/auth";
export const ServerRoute = createServerFileRoute("/api/auth/$").methods({
GET: ({ request }) => auth.handler(request),
POST: ({ request }) => auth.handler(request),
});
src/lib/auth-client.tsimport { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_BETTER_AUTH_URL ?? window.location.origin,
});
export const { signIn, signUp, signOut, useSession } = authClient;
pnpx @better-auth/cli generate --config src/lib/auth.ts --output src/db/auth-schema.ts
Re-export from src/db/schema.ts:
export * from "./auth-schema";
pnpm drizzle-kit generate
wrangler d1 migrations apply <db-name> --local
wrangler d1 migrations apply <db-name> --remote
http://localhost:3000 (dev) or your domain<baseURL>/api/auth/callback/github# .dev.vars
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
# production
wrangler secret put GITHUB_CLIENT_ID
wrangler secret put GITHUB_CLIENT_SECRET
src/lib/auth.ts:
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
Same pattern. Console: https://console.cloud.google.com/apis/credentials. Callback: <baseURL>/api/auth/callback/google.
pnpm add better-auth # plugin included
Patch src/lib/auth.ts:
import { admin } from "better-auth/plugins";
export const auth = betterAuth({
// ...existing...
plugins: [admin({ defaultRole: "user", adminRoles: ["admin"] })],
});
Re-generate schema (/ro:better-auth generate-schema) to add role column on user.
Session check helper in Server Functions:
// src/lib/auth-server.ts
import { createServerFn } from "@tanstack/react-start";
import { auth } from "@/lib/auth";
export const requireSession = createServerFn({ method: "GET" }).handler(async ({ request }) => {
const session = await auth.api.getSession({ headers: request.headers });
if (!session) throw new Response("Unauthorized", { status: 401 });
return session;
});
export const requireAdmin = createServerFn({ method: "GET" }).handler(async ({ request }) => {
const session = await auth.api.getSession({ headers: request.headers });
if (session?.user.role !== "admin") throw new Response("Forbidden", { status: 403 });
return session;
});
--email flag)Requires RESEND_API_KEY (global, ~/.claude/.env).
Patch src/lib/auth.ts:
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
export const auth = betterAuth({
// ...existing...
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendResetPassword: async ({ user, url }) => {
await resend.emails.send({
from: "[email protected]",
to: user.email,
subject: "Reset your password",
html: `<a href="${url}">Reset password</a>`,
});
},
},
emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
await resend.emails.send({
from: "[email protected]",
to: user.email,
subject: "Verify your email",
html: `<a href="${url}">Verify email</a>`,
});
},
},
});
| Var | Where | How to generate |
|---|---|---|
| BETTER_AUTH_SECRET | .dev.vars + wrangler secret | openssl rand -base64 32 |
| BETTER_AUTH_URL | .dev.vars + wrangler secret | dev: http://localhost:3000; prod: app URL |
| GITHUB_CLIENT_ID/SECRET | .dev.vars + wrangler secret | GitHub OAuth app |
| GOOGLE_CLIENT_ID/SECRET | .dev.vars + wrangler secret | Google Cloud Console OAuth credentials |
RESEND_API_KEY is the exception — it's global (~/.claude/.env), shared across all apps.
Per the authentication-hardening playbook (llm-wiki-security/wiki/playbooks/authentication-hardening.md), enable a phishing-resistant factor by default. Better Auth ships a passkey (WebAuthn) plugin, add it rather than relying on password + email/SMS OTP. Passkeys/FIDO2 meet NIST 800-63B AAL2+ and are the CISA gold standard; SMS/TOTP are phishable. Also: short sessions + step-up re-auth before sensitive actions, and (for single-user/internal apps) consider gating at the edge with Cloudflare Access + WARP instead of a public login.
BETTER_AUTH_SECRET in ~/.claude/.env — it MUST be per-app so compromise of one app doesn't forge sessions for all apps..dev.vars. Verify .gitignore includes it before /ro:better-auth install exits.user/session/account/verification tables without an explicit migration plan — this skill only adds, never drops./ro:clerk is the default for small SaaS (hosted UI components, free to 10K MAU, fastest first sign-in). Start there unless one of the four Better-Auth triggers above applies./ro:workos for the alt-at-scale case (vendored auth, hosted Admin Portal, B2B SSO ready, 1M MAU free, when you do not need to own the user table)/ro:new-tanstack-app --auth=better-auth to scaffold a new app with Better Auth pre-wired (default is --auth=clerk)/ro:cf-ship to ship after wiringllm-wiki-research/wiki/comparisons/auth-clerk-vs-better-auth.md, auth-three-way-deep-dive.mddevelopment
--- name: worktree description: Coordinate multiple agents on one repo via a worktree-lock pool, so two agents never clobber each other's working tree. Acquire the first free slot (main, then beta/gamma… worktrees, created on demand), work there on your own branch, release when you've pushed. Use before modifying any repo that might be in use by another agent (factory, dataforce, etc.), or whenever you're told a repo is being worked on. Backed by `ro worktree`. category: development argument-hin
testing
--- name: ship description: Ship a feature branch the local-CI-first way — run the full local gate, push, open a PR, squash-merge, then deploy, without waiting on GitHub Actions. Use when a branch is ready for main and you want it merged and deployed now. Reads CI policy from `ro ci` (default skips remote CI because GitHub Actions billing keeps hitting limits). Sibling to /ro:gh-ship (waits on GitHub checks) and /ro:cf-ship (the deploy half). Triggers on "ship it", "ship this", "merge and deploy
testing
--- name: setup-logging description: Set up (or audit) the observability stack in a TanStack Start + Cloudflare Workers app so it is "diagnosable by default" — structured logging (logtape) with a request context carrying trace_id + userId + tenant/orgId, a trace_id propagated FE→BE→logs→Sentry→PostHog, Cloudflare Workers observability enabled, and Sentry + PostHog wired. Two modes: `setup` (wire it into an app) and `audit` (check an existing app + report gaps). Use when scaffolding a new app, wh
development
Manage credentials INSIDE the active ~/.claude/.env file — read which token/account to use for a given app (Simplicity vs Dataforce vs Ronan-personal), add or update a secret WITHOUT it passing through the chat (an interactive Terminal window prompts for it), and track secrets that were exposed in a transcript so they get rotated. Sibling to /ro:context (which switches WHICH env file is active). Use when the user wants to add an API key/token/secret, asks "which credential do I use for X", needs the env organized/labelled, or a secret was pasted into the chat and should be rotated.