A reusable signup/login module for Next.js — email/password + Google OAuth, powered by Supabase Auth and styled with shadcn/ui.
AuthForm is a drop-in authentication module built for reuse across future SaaS projects. It ships with working signup, login, and Google OAuth flows on top of Supabase Auth, styled with shadcn/ui — so a new project gets a secure, accessible auth screen without re-solving the same problem from scratch.
- Email/password auth — signup and login with server-side validation, duplicate-email handling, and email confirmation
- Password reset — forgot-password request and set-new-password flow, with generic responses that avoid confirming which emails have accounts
- Google OAuth — one-click sign-in via Supabase's OAuth provider, full redirect flow handled end-to-end
- Session management — middleware-based session refresh so users stay logged in across requests
- Protected routes — example
/dashboardroute showing server-side auth checks and redirects - Accessible by default — semantic markup, keyboard-operable password toggle, ARIA live regions for form errors
- Tested — unit tests (Vitest) for validation logic, e2e tests (Playwright) for signup/login/reset flows
- Built for reuse — no hardcoded project values; drop into a new Next.js + Supabase project by swapping env vars and OAuth credentials
| Layer | Choice |
|---|---|
| Framework | Next.js 14 (App Router, Server Actions) |
| Auth & DB | Supabase Auth |
| UI | shadcn/ui + Tailwind CSS |
| Language | TypeScript |
app/
(auth)/
login/page.tsx Login route
signup/page.tsx Signup route
forgot-password/page.tsx Request a password reset link
actions.ts Server actions: signUpWithEmail, signInWithEmail,
signInWithGoogle, logout, requestPasswordReset,
updatePassword
auth/callback/route.ts OAuth + email-confirmation + reset-link callback handler
reset-password/page.tsx Set a new password (requires active recovery session)
dashboard/page.tsx Example protected route
layout.tsx / page.tsx Root layout + redirect-based landing page
components/
auth/ Form components (signup, login, password field,
status messages, Google button, card shell, logout)
ui/ shadcn/ui primitives (button, input, label, card, separator)
lib/
supabase/
client.ts Browser Supabase client
server.ts Server Component / Server Action client
middleware.ts Session refresh logic used by middleware.ts
middleware.ts Runs on every request to keep the session cookie fresh
git clone https://github.com/justworkIT/authForm.git
cd authForm
npm install- supabase.com/dashboard → New project
- Copy the Project URL and anon public key from Settings → API
- Authentication → Providers → Email — on by default; confirm "Confirm email" matches whether you want email verification before login
- Authentication → Providers → Google — toggle on, then follow Supabase's prompt for the Google Client ID/Secret (see step 4)
- Authentication → URL Configuration — set Site URL to your deployed URL (or
http://localhost:3000during dev), and add{your-url}/auth/callbackto Redirect URLs
- Google Cloud Console → APIs & Services → Credentials → Create OAuth client ID (Web application)
- Authorized redirect URI: the callback URL Supabase's Google provider screen shows you (a
supabase.coURL, not your app's/auth/callback) - Paste the resulting Client ID/Secret into Supabase's Google provider settings
- This step is tied to each Google Cloud project/domain — it does not carry over between apps
cp .env.local.example .env.localFill in NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY from step 2.
npm run devVisit /signup or /login. After auth, users land on /dashboard (replace this with your app's real landing page — it's a working example here).
| Copy as-is | Needs redoing per project |
|---|---|
All files in components/, lib/, middleware.ts, app/(auth)/, app/auth/callback/ |
New Supabase project + credentials |
| Form validation, error handling, accessibility behavior | Google OAuth consent screen / client ID |
| Session refresh logic | .env.local values |
| — | Redirect targets in actions.ts / callback/route.ts if /dashboard isn't your landing page |
- Generic error on bad login ("Invalid email or password") is intentional — it avoids confirming whether an email is registered.
- Server Actions validate independently of the client. Client-side
required/minLengthattributes are UX only;actions.tsre-validates everything, since client checks can be bypassed. middleware.tsmatcher excludes static assets so it runs only on real navigations, not every image/font request.
Unit tests (Vitest) cover the pure validation logic in lib/validation.ts:
npm testE2E tests (Playwright) cover signup, login, password reset, protected-route redirects, and the password visibility toggle, run against a real dev server and real Supabase project:
npm run test:e2e
⚠️ Run e2e tests against a dev/test Supabase project only — the signup tests create real (throwaway) user accounts. Flows requiring email inbox access (confirming a signup, clicking a reset link) aren't covered by this starter suite; wiring up an email-testing service (e.g. Mailosaur, Ethereal) is a good next step if you need full coverage.
Deployed easily on Vercel — import the repo, add the two Supabase env vars, and deploy. See Vercel deployment docs for details.
Supabase free-tier projects pause after 7 days without database activity. This repo includes a GitHub Action (.github/workflows/supabase-keepalive.yml) that pings the database twice a week to prevent that. To enable it:
- Run the migration in
supabase/migrations/0001_keepalive.sqlagainst your Supabase project (paste it into the SQL Editor in the Supabase dashboard, or apply via the Supabase CLI) - In your GitHub repo, go to Settings → Secrets and variables → Actions and add:
SUPABASE_URL— your project URLSUPABASE_SERVICE_ROLE_KEY— from Settings → API in Supabase (this key bypasses RLS, so it's only ever used server-side in the Action, never in the app itself)
- The workflow runs automatically on its schedule, or trigger it manually from the Actions tab (
Supabase Keep-Alive→ Run workflow) to test it immediately
Real issues hit while building and deploying this project, kept here so they don't get hit twice.
Cause: NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY weren't set in Vercel's project settings — .env.local only exists on your machine and is never read by Vercel.
Fix: Add both vars in Vercel → Settings → Environment Variables (all three environments: Production, Preview, Development), then redeploy — Vercel does not retroactively apply new env vars to existing deployments; you must trigger a new one.
Cause: The redirectTo URL used in resetPasswordForEmail (/auth/callback?next=/reset-password) wasn't in Supabase's allow-listed Redirect URLs, so Supabase silently fell back to the Site URL and appended the error as a hash fragment.
Fix: Supabase dashboard → Authentication → URL Configuration → Redirect URLs, add a wildcard: https://your-domain.com/**. This covers /auth/callback and any future paths, so you won't need to update it again per route.
Secondary cause to rule out: email security scanners in some corporate/email-client environments "prefetch" links inside emails, consuming the single-use reset token before the real user clicks it. If the allow-list is correct and this still happens, test with a personal (non-corporate) email address.
Cause: Supabase's built-in email service caps out at 2 emails/hour, project-wide (not per-user) — it's dev/testing-only, not for production. Confirmed by checking Supabase → Logs → Auth Logs for repeated 429 responses on POST /auth/v1/recover.
Fix: Set up custom SMTP (see below). There is no way to raise this limit on the default service.
- Verify a sender domain in Brevo (Settings → Senders, Domains & Dedicated IPs → Domains → Add a domain) — requires DNS access, so it must be a domain you actually own (a
vercel.appsubdomain won't work, since you don't control its DNS) - Add the TXT/DKIM records Brevo gives you at your DNS provider; wait for propagation (minutes to ~24h)
- Generate an SMTP key at Settings → Senders, Domains & Dedicated IPs → SMTP & API → SMTP tab —
⚠️ this is a different credential from the API key on the adjacent API Keys tab; using the API key as the SMTP password will fail auth - In Supabase → Authentication → Emails → SMTP Settings, enter host
smtp-relay.brevo.com, port587, the SMTP login (looks likexxxxx@smtp-brevo.com, shown on the same SMTP tab), and the SMTP key as the password ⚠️ Supabase's host field doesn't trim whitespace — a stray leading/trailing space pasted into the host field produces a "no such host" error in Auth Logs that has nothing to do with your actual credentials
Contributions are welcome — see CONTRIBUTING.md for branch naming, commit conventions, and the PR checklist.
Distributed under the MIT License. See LICENSE for details.
Built by @justworkIT

