From shipwithai-auth
Set up authentication for web apps. Supports Better Auth and Firebase Auth. Handles email/password, Google OAuth, sessions, middleware, protected routes. Includes UI components. Auto-detects existing project themes. (GitHub & Apple OAuth coming soon)
How this skill is triggered — by the user, by Claude, or both
Slash command
/shipwithai-auth:auth-setupThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Production-ready authentication for any web app in under 45 minutes.
assets/components/better-auth/auth-provider-buttons.tsxassets/components/better-auth/dashboard-client.tsxassets/components/better-auth/forgot-password.tsxassets/components/better-auth/login-page.tsxassets/components/better-auth/protected-layout.tsxassets/components/better-auth/register-page.tsxassets/components/better-auth/reset-password.tsxassets/components/better-auth/user-profile.tsxassets/components/firebase/auth-provider-buttons.tsxassets/components/firebase/dashboard-client.tsxassets/components/firebase/forgot-password.tsxassets/components/firebase/login-page.tsxassets/components/firebase/protected-layout.tsxassets/components/firebase/register-page.tsxassets/components/firebase/reset-password.tsxassets/components/firebase/user-profile.tsxassets/components/shared/dashboard-page.tsxassets/components/shared/globals.cssassets/components/shared/icons.tsxassets/config/authjs.config.tsProduction-ready authentication for any web app in under 45 minutes. 2 auth providers (Better Auth & Firebase), Google OAuth, copy-paste UI components, and a verification script. GitHub & Apple OAuth coming soon.
If scaffolding a new Next.js project, never run create-next-app . in the current directory.
Claude Code plugins (OMC, gstack) create dotfiles (.omc/, .claude/) that conflict with
create-next-app, which requires a completely empty directory. The dotfiles are recreated
by hooks between deletion and scaffolding, causing an infinite failure loop.
Correct pattern — scaffold in /tmp, pin to Next.js 14:
SCAFFOLD_DIR=/tmp/nextjs-scaffold-$RANDOM && \
npx create-next-app@14 "$SCAFFOLD_DIR" \
--typescript --tailwind --eslint --app --src-dir \
--import-alias "@/*" --use-npm --no-turbopack && \
cp -a "$SCAFFOLD_DIR"/. . && rm -rf "$SCAFFOLD_DIR"
Why @14 not @latest? Next.js 16 deprecated middleware.ts in favor of proxy.ts.
All templates, guides, and middleware examples in this plugin use the middleware.ts convention
(Next.js 14/15). Using @latest will install Next.js 16+ and the build will fail.
See pitfall #37 for details.
Or scaffold into a named subdirectory, then move files up.
shadcn/ui: Use npx [email protected] init -d (NOT shadcn@2 or shadcn@latest).
[email protected]+ expects Tailwind v4's CSS-based config and fails with Next.js 14's tailwind.config.ts.
After shadcn init, you MUST install tailwindcss-animate — shadcn adds it to tailwind.config.ts but does NOT auto-install it: {pm} install tailwindcss-animate (use package manager detected in Step 0).
See pitfall #44.
| Your situation | Best choice | Why |
|---|---|---|
| Full control, no vendor lock-in | Better Auth | Open-source, self-hosted, most flexible, zero cost |
| Google ecosystem or mobile + web | Firebase Auth | Best mobile/KMP support, free < 50K MAU |
Note: Clerk, Auth.js, and Supabase Auth support is planned for a future release. Currently only Better Auth and Firebase Auth are fully supported.
Full comparison with cost analysis: references/01-choosing-provider.md
Before asking ANY questions, scan the project silently:
0a. Framework & Router:
next.config.* (.js, .mjs, .ts) → Next.jsastro.config.* → Astroremix.config.* or @remix-run/* in deps → Remixvite.config.* without framework deps → Viteapp/ directory → App Router | pages/ directory → Pages Routerapp/) → warn: "Pages Router detected. This plugin requires Next.js App Router (app/ directory)."0b. Version Compatibility:
package.json and extract versions:
next — check major version (14, 15, 16+)react — must be ≥18 for Server Componentsnode — check engines field or .nvmrc if existsmiddleware.ts is deprecated — plugin will use proxy.ts pattern instead."experimental: { serverComponentsExternalPackages: [...] }serverExternalPackages: [...]0c. Database ORM:
drizzle.config.ts or drizzle.config.js → Drizzleprisma/schema.prisma → Prismapackage.json for drizzle-orm or @prisma/client0d. Existing Auth:
src/lib/auth.ts + better-auth in deps → Better Auth already installedsrc/lib/firebase.ts + firebase in deps → Firebase already installed@clerk/nextjs in deps → Clerk detectednext-auth in deps → Auth.js/NextAuth detected@supabase/ssr in deps → Supabase Auth detected/shipwithai-auth:doctor first to check health, or confirm you want to replace it."0e. Existing Auth Pages & Routes:
src/app/(auth)/login/, src/app/login/, app/login/src/app/api/auth/, app/api/auth/src/middleware.ts, middleware.ts0f. Package Manager:
pnpm-lock.yaml → pnpmyarn.lock → yarnbun.lockb → bunpackage-lock.json → npm0g. UI Library:
components.json → shadcn/ui (read style and baseColor fields)@mui/material in deps → MUI@chakra-ui/react in deps → Chakra UI@mantine/core in deps → Mantinetailwind.config.* or tailwindcss in depsshadcn init in setup, only add missing components0h. Existing Design System (Context Project):
globals.css already has shadcn CSS variables → Scenario A (skip theme generation)Show discovery summary to user:
🔍 Project scan complete
Framework: Next.js 14.2.18 (App Router)
Package mgr: npm
ORM: Drizzle ([email protected])
UI: shadcn/ui (new-york style)
Existing auth: None detected
Existing pages: /dashboard (protected)
Node.js: v20.11.0
React: 18.3.1
Ready to set up authentication.
If critical issues found, STOP and report:
⛔ Cannot proceed:
- Next.js 13 detected — requires Next.js 14+ with App Router
- No `app/` directory found — Pages Router not supported
Fix these issues first, then re-run /shipwithai-auth:setup
Ask user which provider using AskUserQuestion. Present options:
If user selects a "Coming Soon" provider: Do NOT proceed. Warn and re-ask with only supported options.
If unsure, ask:
Ask user which social login using AskUserQuestion. Present options:
If user selects an option with "Coming Soon" providers: Warn, then proceed with Google only. Mention /shipwithai-auth:add-oauth for future providers.
ALWAYS run this step — even for new projects. Use Step 0 discovery results to auto-classify where possible.
There are 4 scenarios:
| Scenario | Condition | globals.css Action |
|---|---|---|
| A | Existing project with shadcn/ui (detected in 0g) | SKIP — use existing CSS variables |
| B | Existing project with Tailwind but no shadcn/ui (detected in 0g) | MERGE — add shadcn/ui vars to existing globals.css |
| C | Standalone new project (no parent design system) | AUTO-SELECT — AI picks Ocean or Sunrise preset |
| D | New project inside/alongside existing project | INHERIT — map parent's design tokens to shadcn/ui vars |
Auto-classification from Step 0:
components.json exists) → Scenario A — skip to Step 2, no question neededOnly ask Question 5 if scenario is ambiguous:
globals.css) → ask: "Is this new app part of an existing project with its own design system?"npx ts-node scripts/detect-theme.ts /path/to/new-app --context /path/to/parent-projectScenario C (NEW PROJECT) — AI auto-selects theme:
Two curated theme presets are available in assets/themes/. Do NOT ask the user to choose — pick automatically based on project signals:
| Theme | File | Font | Best for |
|---|---|---|---|
| Ocean | assets/themes/ocean.css | DM Sans | SaaS, dashboard, dev tools, API, analytics, admin, B2B |
| Sunrise | assets/themes/sunrise.css | Outfit | Consumer, blog, shop, community, education, portfolio, e-commerce |
Auto-selection logic (SILENT — no user interaction):
package.json → name and description fieldsREADME.md first paragraph (if exists)After selecting theme:
npx [email protected] init -d first (creates default globals.css)globals.css with the selected theme preset fileglobals.css:
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap');layout.tsx to use the theme's font instead of default Geist:
import { DM_Sans } from "next/font/google" → const font = DM_Sans({ subsets: ["latin"] })import { Outfit } from "next/font/google" → const font = Outfit({ subsets: ["latin"] })<html className="dark"> in layout.tsxTheme: Ocean (dark, DM Sans) — auto-selected for SaaS project
Scenario D (INHERIT) — what to do:
npx [email protected] init -d FIRST, then apply the inherited theme. shadcn init overwrites globals.css with default variables — if you apply the inherited theme first, shadcn will destroy it.shadcnMapping with dark and light mode valuesglobals.css with the inherited theme mapping@import to globals.cssclassName="dark" on <html> if the parent is dark-firstlayout.tsx to use the parent's font-family instead of default GeistKey rule: Auth components use shadcn/ui CSS variables (--primary, --background, etc.). The theme detection ensures these variables match the surrounding project's design — whether it's the same project (A/B), a parent/sibling project (D), or an auto-selected preset (C).
See references/10-existing-project-integration.md for detailed adaptation steps.
| Provider | Guide |
|---|---|
| Better Auth | references/02-better-auth-guide.md |
| Firebase Auth | references/05-firebase-auth-guide.md |
Skip this step for Firebase — Firebase handles email delivery automatically.
For Better Auth — you MUST ask the user before proceeding. Do NOT skip this question. Do NOT assume console-only. STOP and ASK:
"How should auth emails be sent? (verification emails, password reset links)
A) Resend (Recommended) — free tier: 100 emails/day, simple API, works in 2 minutes B) Console only (Development) — emails log to terminal, no real delivery C) Other provider — you'll configure SendGrid/Nodemailer/etc. yourself"
WAIT for the user's answer before writing any auth config files.
If A (Resend):
{pm} install resend (use package manager detected in Step 0).env.local: RESEND_API_KEY=re_xxxxx and [email protected]src/lib/email.ts with the sendAuthEmail helper from the Better Auth guide → "Email Provider — Resend" sectionsendVerificationEmail and sendResetPassword callbacks in src/lib/auth.ts to use sendAuthEmail[email protected] as sender, emails can ONLY be delivered to the email address that owns your Resend account. To send to any email, verify your own domain at resend.com/domains and change EMAIL_FROM to [email protected]."If B (Console):
console.log stubs in auth config (already the default)If C (Other):
console.log stubs as placeholderCurrently supported: Google OAuth only. GitHub and Apple are coming soon.
See references/07-oauth-social-login.md for Google setup across all providers.
See references/08-database-auth-schema.md for Drizzle/Prisma/Supabase schemas.
Use Step 0 discovery results:
shadcn init, only run npx [email protected] add button input label card separator for missing componentsnpx [email protected] init -d first, then add components. Remember: for Scenario D, apply inherited theme AFTER shadcn init.Components are organized by provider. Copy from the correct folder:
assets/components/better-auth/assets/components/firebase/assets/components/shared/REQUIRED — Core auth pages (MUST copy all of these):
| File | Source folder | Place at | Purpose |
|---|---|---|---|
login-page.tsx | {provider}/ | app/(auth)/login/page.tsx | Login with email + social buttons |
register-page.tsx | {provider}/ | app/(auth)/register/page.tsx | Registration form |
forgot-password.tsx | {provider}/ | app/(auth)/forgot-password/page.tsx | Password reset request (sends email) |
reset-password.tsx | {provider}/ | app/(auth)/reset-password/page.tsx | Complete password reset (token from email → new password) |
protected-layout.tsx | {provider}/ | app/(protected)/layout.tsx | Server-side session verification |
dashboard-client.tsx | {provider}/ | app/(protected)/dashboard/dashboard-client.tsx | Sign-out button (client component) |
dashboard-page.tsx | shared/ | app/(protected)/dashboard/page.tsx | Protected dashboard page |
icons.tsx | shared/ | src/components/icons.tsx | OAuth provider SVG icons (Google, GitHub) — required by login/register pages |
globals.css | shared/ | app/globals.css | Tailwind CSS directives + shadcn/ui variables |
RECOMMENDED — User management (copy unless user explicitly declines):
| File | Source folder | Place at | Purpose |
|---|---|---|---|
user-profile.tsx | {provider}/ | app/(protected)/profile/page.tsx | Profile view + update name + sign out + delete account |
OPTIONAL:
auth-provider-buttons.tsx — Standalone OAuth buttons (already included in login/register pages)Copy from assets/config/:
next.config.ts — Security headers (X-Frame-Options, HSTS, etc.) — merge into existing configdrizzle.config.ts — Drizzle Kit config with dialect: "sqlite" (Better Auth only) — copy to project rootWhere {provider} = better-auth or firebase depending on the chosen provider.
CRITICAL for Firebase Auth — File creation checklist: You MUST create ALL of these files for Firebase to work. Do NOT skip any:
src/lib/firebase.ts — Client SDK init (from firebase.config.ts → "Client SDK Setup")src/lib/firebase-admin.ts — Admin SDK init (from firebase.config.ts → "Admin SDK Setup")src/lib/firebase-session.ts — createSessionCookie() + clearSessionAndSignOut() helpers (from firebase.config.ts → "Session Cookie Helper" + "Sign-Out Helper")src/lib/auth-server.ts — getServerUser() helper (from firebase.config.ts → "Server Component Auth Check"). Import this in BOTH (protected)/layout.tsx AND (protected)/dashboard/page.tsx — do NOT duplicate the function inlinesrc/app/api/auth/session/route.ts — Session API with rate limiting + CSRF (from firebase.config.ts → "Session Cookie API Route"). The POST handler MUST be wrapped in try/catch — return 401 on invalid token, never 500src/middleware.ts — Route protection (from assets/middleware/firebase/nextjs-middleware.ts)app/globals.css — Tailwind v3 directives (@tailwind base; @tailwind components; @tailwind utilities;) + shadcn/ui hsl CSS variables — layout.tsx imports this file. Copy from assets/components/shared/globals.cssCRITICAL for Firebase Auth — Google OAuth Setup:
If using Google sign-in with signInWithPopup, you MUST complete the 5-step OAuth setup checklist in references/05-firebase-auth-guide.md → "Google OAuth Setup Checklist". Without it, sign-in will fail with 400 Bad Request. Key points:
https://<PROJECT>.firebaseapp.com/__/auth/handler to Authorized redirect URIs (NOT /api/auth/callback/google — that's Auth.js)references/05-firebase-auth-guide.md → "Google OAuth Setup Checklist" for full stepsCRITICAL for Firebase Auth — Session flow:
createSessionCookie(result.user) and await it/dashboardclearSessionAndSignOut() FIRST (clears server cookie + Firebase), then redirectonAuthStateChanged for the initial login redirectCRITICAL for Firebase Auth Security:
NEXT_PUBLIC_APP_URL env var — required for CSRF protection on session endpointapp/(protected)/ and use protected-layout.tsx to verify sessions server-side (middleware only checks cookie existence, NOT validity)CRITICAL for Firebase Auth — Middleware routes:
Only include routes in protectedRoutes that have actual pages. Default: ["/dashboard"]. Add /profile only if user-profile.tsx was copied. Do NOT add /settings or /profile unless those pages exist.
CRITICAL for Firebase Auth — Middleware security:
The middleware runs on Edge Runtime and CANNOT use Firebase Admin SDK. It MUST only check cookie existence (request.cookies.get("__session")?.value). Do NOT attempt to decode, parse, or validate the JWT in middleware — Base64-decoding the payload without verifying the cryptographic signature gives a false sense of security. An attacker can craft a fake JWT that passes the decode check. Real verification happens in (protected)/layout.tsx via adminAuth.verifySessionCookie().
CRITICAL for Better Auth — File creation checklist: You MUST create ALL of these files for Better Auth to work. Do NOT skip any:
src/db/schema.ts — Drizzle schema with Better Auth tables (from assets/db/better-auth-schema.ts)src/lib/db.ts — Drizzle client (from assets/db/better-auth-db.ts). MUST import and pass schema to drizzle(client, { schema })src/lib/auth.ts — Server config (from assets/config/better-auth.config.ts). MUST pass schema to drizzleAdapter: drizzleAdapter(db, { provider: "sqlite", schema })src/lib/auth-client.ts — Client setup (from better-auth.config.ts → "Client Setup" section)src/app/api/auth/[...all]/route.ts — API route handlersrc/middleware.ts — Route protection using cookie-based pattern (from assets/middleware/better-auth/nextjs-middleware.ts). Do NOT use auth() wrapper — that's Auth.js, not Better Auth (see pitfall #43)src/app/(protected)/layout.tsx — Server-side session verification.env.local — Copy from assets/config/better-auth.env.example and fill in valuesdrizzle.config.ts — Copy from assets/config/drizzle.config.ts. MUST use dialect: "sqlite" (NOT driver: "better-sqlite" — removed in drizzle-kit 0.21+). See pitfall #48.npx drizzle-kit push (or npx @better-auth/cli generate && npx @better-auth/cli migrate)assets/components/better-auth/ (shared components from assets/components/shared/)reset-password.tsx already includes a <Suspense> wrapper — useSearchParams() requires this in Next.js 14+ App Router or the build fails. Do NOT remove the Suspense wrapper.npx [email protected] (NOT @latest or @2) to avoid Tailwind v4 conflicts with Next.js 14. See pitfall #44.CRITICAL for Better Auth — next.config.ts compatibility:
When adding better-sqlite3 or other native packages to next.config.ts:
experimental: { serverComponentsExternalPackages: ["better-sqlite3"] }serverExternalPackages: ["better-sqlite3"]
Check the project's Next.js version in package.json BEFORE writing the config. Using the wrong syntax causes: Unrecognized key(s) in object: 'serverExternalPackages'.CRITICAL for Better Auth — Session flow:
better-auth.session_token) — no manual cookie creation needed (unlike Firebase)cookieCache is enabled by default (5 min) — session changes (role updates, email verification) take up to 5 minutes to propagateauth.api.getSession({ headers: await headers() }) — this verifies the session cryptographicallybetter-auth.session_token cookie directly — do NOT use auth() wrapper (that's Auth.js, see pitfall #43). Do NOT use headers() from next/headers in middleware (Edge Runtime incompatible)NOTE for Better Auth: The forgot-password page sends a reset link via email. You MUST also copy reset-password.tsx to app/(auth)/reset-password/page.tsx to handle the link destination. Without it, users get a 404.
CRITICAL for shadcn/ui Button: If using buttonVariants in a Server Component (e.g., home page), extract buttonVariants into a separate file WITHOUT "use client" directive. The Button component needs "use client" but buttonVariants is a pure function that can run on the server.
Run scripts/verify-auth-setup.ts to confirm auth works end-to-end. The script now includes dangerous code pattern detection — it catches module-scope SDK instantiation, wrong middleware patterns, missing Suspense wrappers, and other pitfalls automatically.
Copy the provider's assets/templates/providers/{better-auth|firebase}/README.md.tmpl, substitute placeholders, write to end-user project root as README.md. Do NOT improvise — template is source of truth.
MANDATORY FLOW: Read → Ask → Write. Never call Write on README.md without first checking existence and asking the user. The Write tool fails with Error writing file when the target exists and has not been Read in this session — this is a guard against silent overwrites.
Step 8a — Existing README guard (MUST run before any Write):
Glob with pattern README.md (or Bash: ls README.md 2>/dev/null) to detect.Write directly.Read README.md) — required before any later Write to that path AND so you can summarize current content to the user.
b. Call AskUserQuestion with these 3 options (do NOT silently pick one):
Edit)README.md.bak via Bash: cp README.md README.md.bak, then Write new README)README.AUTH.md (Write to a new path; existing README untouched)
c. Wait for the user's answer. Do NOT proceed on assumption.
d. Execute the chosen branch:Edit to append the rendered template under a new ## Authentication section. Do NOT use Write (it would overwrite).Bash command first → verify .bak exists → then Write README.md with new content.Write to README.AUTH.md instead of README.md. Mention the new file in the final summary.Recovery — if Write README.md already failed with Error writing file: The file exists and was not Read. Do NOT retry the same Write. Instead: Read README.md → run Step 8a's AskUserQuestion → execute the chosen branch.
Step 8b — Substitute placeholders (double-brace {{NAME}}):
| Placeholder | Source | Fallback chain |
|---|---|---|
{{PROJECT_NAME}} | package.json name | 1) strip @scope/ → 2) basename(cwd) → 3) My App |
{{NEXT_VERSION}} | Step 0b | 14+ if unknown |
{{ORM}} | Step 0c | Drizzle (Better Auth default) |
Provider name is pre-resolved by selecting the correct template file (
firebase/vsbetter-auth/), not via placeholder.
Step 8c — Process conditional blocks <!-- IF key=value -->...<!-- /IF -->:
oauth=google → keep if Step 1a included Google, else delete (and remove markers).email=resend|console|other → keep matching Step 3 choice, delete the rest.Step 8d — Verify (mandatory): No raw {{ remains. No raw <!-- IF remains. Every env var in .env.example appears in the README env table. At least one link to provider's console (Firebase / Google Cloud / Resend). No orphan intra-doc anchor links. If any check fails → re-run substitution.
These rules are mandatory. They are extracted from 60 production pitfalls (references/09-common-pitfalls.md) and represent the most common causes of auth failures. Claude MUST follow these rules when generating ANY auth code:
R1 — NEVER instantiate SDK clients at module scope.
new Resend(process.env.X) or new Stripe(process.env.X) at the top of a file crashes during build/SSR because env vars aren't available at module evaluation time. Use lazy initialization (singleton getter function) or create instances inside handler functions. Copy assets/config/email.ts for the correct Resend pattern. (#50)
R2 — ALWAYS await session cookie BEFORE redirect.
router.push("/dashboard") without await createSessionCookie() first → middleware bounces user back to /login because cookie doesn't exist yet. Firebase: await createSessionCookie(user) then redirect. Better Auth: handled automatically via cookie. (#22)
R3 — Sign-out: clear server cookie FIRST, then client, then redirect.
Calling signOut() (client) before DELETE /api/auth/session (server) leaves an orphan server cookie. Order: server delete → client signOut → window.location.href = "/login". (#23)
R4 — NEVER use auth() wrapper in middleware for Better Auth.
auth() is Auth.js (NextAuth v5), NOT Better Auth. Better Auth middleware checks request.cookies.get("better-auth.session_token") directly. Using auth() gives "auth is not a function". (#43)
R5 — NEVER decode JWT in middleware for Firebase.
Middleware runs on Edge Runtime — no Firebase Admin SDK. Only check cookies.get("__session")?.value existence. Real verification happens in (protected)/layout.tsx via adminAuth.verifySessionCookie(). Base64-decoding without crypto verification gives false security. (#28)
R6 — ALWAYS copy email.ts from assets/config/email.ts template.
Do NOT write email helper code from scratch. The template has correct lazy-init, dev fallback (console.log), and production guard. Inline code examples in guides may be simplified — the template is the source of truth.
R7 — useSearchParams() requires <Suspense> wrapper in Next.js 14+ App Router.
reset-password.tsx uses useSearchParams() — without Suspense, next build fails. The template already includes it. Do NOT remove the Suspense wrapper. (#47)
R8 — Copy ALL files in the provider checklist.
Skipping any file (e.g., auth-server.ts for Firebase, auth-client.ts for Better Auth) causes cascading import errors. Use the numbered checklist in the CRITICAL section above — every item is required.
Full 60 pitfalls: references/09-common-pitfalls.md
next.config.ts (use assets/config/next.config.ts template)NEXT_PUBLIC_APP_URL for CSRF protection (Firebase)05-firebase-auth-guide.md Security Hardening section)Provides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub mangalahq/shipwithai-plugins --plugin shipwithai-auth