AuthTemplate
A Next.js + Supabase auth template with email/password sign-up, login, and sessions so you can skip the boilerplate and get straight to building. To start, just create a copy of the template, use the prompt to set up the code, and start building your app.
What's included
Free vs. Production โ No, I'm not charging anything for either. The issue is Supabase limits their free tier and prevents sending emails, which blocks email confirmation & forgot/reset password from working. This template therefore supports two paths:
- Free runs entirely on Supabase's free tier with zero email config setup โ sign-up, login, sessions, and settings all work end to end, you just don't get working forgot/reset password.
- Production adds a custom SMTP provider (eg. Resend) and a domain you own, which unlocks that last piece. That's what this website uses.
Both work end to end: start free, add Production whenever you actually need it โ see the Setup guide's fork or the AI prompt below.
Why I built free
I don't want to be blocked on setting up a whole SMTP server and domain just to get an app running. It costs money and takes time. Use the free version to get started quickly. It works, just don't forget your password :)
| Free | Production | |
|---|---|---|
| Setup cost | Supabase free tier โ nothing else | + SMTP provider + a domain you own |
| Email/password login | โ | โ |
| Email confirmation | off (recommended) | โ opt-in |
| Sessions & protected routes | โ | โ |
| Row Level Security | โ | โ |
| Settings page | โ | โ |
| Forgot/reset password | Not included | โ |
| Dark mode | โ | โ |
| CI-deployed migrations | โ | โ |
| SMTP_CONFIGURED flag | false (ships as-is) | true (you flip it) |
Repo file structure
To make the set up easy, I put everything that is deletable in the "delete when ready" directory. The auth template doesn't need anything in there and is only used to render this site :) Click a file or folder with a ๐ก to learn more about it.
Set up with your AI coding assistant
Before starting, make sure you've made a copy of the template on GitHub here.
To get started quickly, paste either the free or production prompt into your preferred agent (ex. Claude Code). Your agent will be given instructions to complete as much of the flow as possible for you โ like running the local setup commands and cleaning up the onboarding folder. It will ask you to intervene when necessary โ when creating a Supabase project or handling secrets.
Both prompts are identical through step 7 โ they only diverge at step 8. Free skips email setup entirely (forgot/reset password won't work); Production adds SMTP so it does. Switch anytime by running the other prompt.
You're in control the whole way through โ your agent will ask before committing or pushing anything, and you can always ask it what step it's on or what's left to do.
Setup guide
Prerequisites
node -vInstall Docker Desktop and make sure it's running
Supabase's local stack (Postgres, auth, storage) runs inside Docker containers on your machine.
New to Docker?
Think of containers as lightweight, self-contained environments. For this guide, just install Docker Desktop and leave it running.
docker infoWhat you'll need to deploy (not local dev)
Node and Docker above are all local dev needs. Deploying needs two accounts, both free โ that's it, regardless of which path below you pick:
- A Supabase account, for your hosted project
- A Vercel account (or any Next.js host)
SMTP and a domain โ only if you want working password reset
Free and Production genuinely support different things, not "same app, one path is just unfinished." Free deploys and runs for real with just the two accounts above โ sign-up, login, sessions, settings all work. The only thing it can't do is forgot/reset password, since that's the one flow that can't skip email. If you want that working, that's when you need a custom SMTP provider (e.g. Resend) and a domain you own โ see the fork at the bottom of this guide for exactly what that adds and why.
Get the code
Use this template
Click "Use this template" on GitHub to get your own copy, then clone it.
github.com/rl3020/AuthTemplategit clone <your-repo-url>Local setup
Install dependencies and start local Supabase
npm install pulls down (see package.json for the full list):
- Next.js and React โ the framework
- The Supabase client libraries
- The Supabase CLI itself โ a project dependency, not a global install, so every supabase command here runs through npx
npm installnpx supabase startSet up your env file and run the app
- cp copies .env.example to .env.local โ paste in the Publishable key that supabase start just printed
- npm run dev starts the Next.js dev server, which reads that file and talks to your local Supabase stack, not a production one
cp .env.example .env.localnpm run devRename it from the template's identity to yours
Do this now, not just at final cleanup โ otherwise every "supabase start" print, container name, and browser tab keeps saying "auth-template" instead of your app while you work.
- supabase/config.toml, line 5 โ top-level project_id: "auth-template" โ your project name (purely a local dev label, cosmetic, but confusing to leave)
- src/app/layout.tsx โ the title/description in its metadata export
Leave the [remotes.production] block alone here
Further down in config.toml there's a separate [remotes.production] block โ that one still holds the template author's own Supabase project ref and domain. It's a placeholder that only matters if you pick the Production track below; editing it is covered there, not here.
Create a Supabase project
Create the project
Click "New project", pick an organization and region, and set a database password when prompted โ write it down now, Supabase won't show it to you again. Once it finishes provisioning (a minute or two), note the Project URL and Publishable key (Project Settings โ API).
- Your project ref โ the string in the dashboard URL (supabase.com/dashboard/project/<ref>), also shown under Project Settings โ General โ gets reused as SUPABASE_PROJECT_REF in the next section and again in supabase/config.toml if you set up Production later
Generate an access token
Create a personal access token
Scope it to this project โ it's used by GitHub Actions to push migrations.
Account โ Access TokensWire up GitHub Actions
Add three repository secrets
The migration workflow below needs these to authenticate as you and reach your database โ without them it fails with no way to connect.
- Add them at: your GitHub repo โ Settings โ Secrets and variables โ Actions โ New repository secret (your repo, not this template's)
- Names must match exactly โ see the panel
- SUPABASE_PROJECT_REF is the ref from your project's dashboard URL; SUPABASE_ACCESS_TOKEN is the one you just generated above; SUPABASE_DB_PASSWORD is the one you set when creating the project
SUPABASE_ACCESS_TOKENSUPABASE_PROJECT_REFSUPABASE_DB_PASSWORDYour first migration is already here
This template ships two migrations โ you don't need to create them
The first created the profiles table you saw in "What's included": Row Level Security enabled, policies so each user can only read/update their own row, and a trigger that auto-inserts a profile row the moment someone signs up.
create table public.profiles ( id uuid primary key references auth.users(id) on delete cascade, display_name text);alter table public.profiles enable row level security;create policy "Users can view their own profile" on public.profiles for select using (auth.uid() = id);create policy "Users can update their own profile" on public.profiles for update using (auth.uid() = id);create function public.handle_new_user()returns trigger as $$begin insert into public.profiles (id) values (new.id); return new;end;$$ language plpgsql security definer;create trigger on_auth_user_created after insert on auth.users for each row execute function public.handle_new_user();The second grants access to that table
RLS only filters rows once a role already has table-level privilege โ it doesn't grant that privilege itself. Local dev's supabase start/db reset quietly bootstraps default grants for you (Supabase's local template does this for convenience), which is why this works without thinking about it locally. A hosted project doesn't get the same default grants for tables created by a migration (only ones created through Studio's Table Editor do) โ without this second migration, every fresh hosted project hits "permission denied for table profiles" the first time anything touches it, even though local dev looks completely fine.
grant select, insert, update, delete on public.profiles to authenticated;It goes live the first time the workflow runs
The GitHub Action above applies every file in supabase/migrations/ with supabase db push whenever main gets a commit touching that folder. This one was already committed before you added the secrets, so:
- Trigger it now: your repo's Actions tab โ "Deploy Supabase Migrations" (left sidebar) โ "Run workflow" button โ Run โ this workflow ships with a manual trigger built in, so you don't need a new commit
- Or just wait โ your next real migration will bring this one along too, since db push applies everything not yet applied, not just what changed
Double-check the SMTP_CONFIGURED repo variable while you're in here
Repo โ Settings โ Secrets and variables โ Actions โ Variables tab โ there shouldn't be an SMTP_CONFIGURED variable there yet, which is what makes .github/workflows/config.yml skip the email-config push by default. Some copies of this template were generated from an earlier version of it that hardcoded "true" straight into the file, which makes that workflow try to push email config on a Free-tier project with no SMTP set up, and Supabase rejects it with a 403.
Note
If you find SMTP_CONFIGURED already set to "true" and you haven't set up SMTP yet, delete it โ see the Production track below for when you actually want it on.
Adding your own migration later looks like this
New table? Don't forget the grant.
db reset above will look like it works โ local dev's default grants mask this every time. Add an explicit grant to the same migration (or a follow-up one) for any new table, or it'll pass locally and then throw "permission denied" the moment it hits your hosted project.
- grant select, insert, update, delete on public.posts to authenticated;
npx supabase migration new add_posts_tablenpx supabase db resetgit add supabase/migrationsgit commit -m "Add posts table"git pushDeploy to Vercel
Import the repo
vercel.com/newAdd environment variables
- NEXT_PUBLIC_SUPABASE_URL โ your hosted project's API URL
- NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY โ the public key from Project Settings โ API (safe to expose client-side โ it's not a secret)
- NEXT_PUBLIC_SITE_URL โ optional. src/lib/site.ts falls back to Vercel's own VERCEL_PROJECT_PRODUCTION_URL/VERCEL_URL when this isn't set, so on Vercel you only need it to override with a custom domain
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYAdd a custom domain (optional)
The *.vercel.app URL Vercel gives you by default works fine on its own โ this is only if you want your own domain instead:
- Vercel project โ Settings โ Domains โ add your domain
- Vercel shows you the DNS records to add (usually an A record or CNAME) โ add them at your domain registrar, then wait for Vercel to verify it
- Once verified and set as the Production domain, VERCEL_PROJECT_PRODUCTION_URL (which src/lib/site.ts already falls back to) reflects it automatically โ no NEXT_PUBLIC_SITE_URL override needed unless you want to force a specific value
Choose your path
Both are finished states. Add Production whenever you want.
Zero email setup.
You're live
Sign-up, login, sessions, and settings all work right now โ nothing to configure.
Forgot/reset password needs SMTP โ see Production, any time.
Adds working password reset.
1Forgot/reset password needs a real SMTP provider
This is the one flow that can't skip email, and free-tier Supabase won't accept this template's custom recovery template without one โ config push fails with:
unexpected status 400: {"message":"Email template modification isnot available for free tier projects using the default emailprovider. Please upgrade your plan or configure a custom SMTP provider."}2Set up a custom SMTP provider
Resend's free tier works fine for this (3,000 emails/month):
- Buy a domain if you don't already have one (GoDaddy, Namecheap, Cloudflare, etc.) โ it doesn't need to match wherever the app itself is hosted. If you do want the same domain hosting the app instead of the default *.vercel.app URL, that's the custom-domain step back under Deploy to Vercel, independent of everything below
- Create a Resend account, then Domains โ Add Domain for that domain
- Resend shows you TXT/CNAME records to add โ go to your registrar's DNS management and add them there
- Wait for Resend to show the domain as "Verified" โ usually minutes, occasionally longer while DNS propagates. Sending fails until it flips
- API Keys โ Create API Key โ that's your SMTP password
3Point Supabase at it
Authentication โ Emails โ SMTP Settings โ enable custom SMTP and fill in:
- Host: smtp.resend.com โ Port: 587 (not 465 โ Supabase's mailer can hang and time out on 465 with Resend; 587 is the one that actually works)
- Username: resend (literally that word) โ Password: the API key from the previous step
- Sender email: any address on your verified domain, e.g. noreply@yourdomain.com โ this doesn't need to be a real inbox you can log into, it's just the From address
- Sender name: whatever you want recipients to see, e.g. your app's name โ both this and Sender email are required, the form won't save without them
- Save, then Authentication โ Rate Limits โ raise the email limit off Supabase's shared-mailer default
4Fix site_url so emails link to production, not localhost
site_url isn't just a redirect allow-list entry โ it's the literal value Supabase substitutes into the email templates. Leave it as localhost in the base [auth] block (local dev needs that). A [remotes.production] block already exists near the bottom of supabase/config.toml, but it still holds the template author's own project ref and domain โ edit it in place (don't add a second one) to swap in yours:
[remotes.production]project_id = "<your project ref, from the dashboard URL>"[remotes.production.auth]site_url = "<your production URL>"additional_redirect_urls = ["<your production URL>", "<your production URL>/auth/confirm"]A project ref isn't a secret โ it's already public in your NEXT_PUBLIC_SUPABASE_URL โ so this is safe to commit. Left unedited, config push either no-ops (the ref won't match yours) or, if it somehow did, would aim at the template author's own project. supabase config push merges this override automatically when pushing to that project ref; local dev keeps using localhost.
5Flip the flag and ship it
SMTP_CONFIGURED=unset (defaults to false)
Skipped by default โ migrations still deploy either way. To enable:
- Add SMTP_CONFIGURED = true as a repo variable (not a file edit โ so it stays yours and never leaks into a fresh fork of this template): gh variable set SMTP_CONFIGURED --body true --repo <owner>/<repo> if you have the gh CLI, or repo โ Settings โ Secrets and variables โ Actions โ Variables tab otherwise
- Actions tab โ "Deploy Supabase Config" โ "Run workflow" โ config push should now succeed (not skip) and deploy your custom templates
- Test against your deployed URL, not localhost โ request a password reset for an email that's actually registered on the hosted project (Authentication โ Users)
- The UI always shows "check your email" whether or not an account exists โ that's deliberate, to stop attackers probing which emails are registered. If nothing arrives, check Resend's own dashboard (Emails โ Sending) to see whether it even received a send request โ that tells you which side the problem is on
6Email confirmation is optional too
SMTP being set up doesn't turn this on by itself โ enable_confirmations stays false in supabase/config.toml's [auth.email] block until you flip it. Worth enabling now that delivery actually works: it verifies someone owns the email before their account is fully usable, and the click-gated /auth/confirm page plus your custom template only actually close the mail-scanner vulnerability once real delivery exists โ leaving it off doesn't add safety, it just skips verification.
- Set enable_confirmations = true, commit, push โ applies to local dev too, there's no per-environment override for it like site_url has
- Leave it false if you'd rather not require verification at all โ sign-up already works fine either way, this is a judgment call, not a requirement