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.

Forgot password?

Don't have an account? Sign up

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 :)

FreeProduction
Setup costSupabase free tier โ€” nothing else+ SMTP provider + a domain you own
Email/password loginโœ“โœ“
Email confirmationoff (recommended)โœ“ opt-in
Sessions & protected routesโœ“โœ“
Row Level Securityโœ“โœ“
Settings pageโœ“โœ“
Forgot/reset passwordNot includedโœ“
Dark modeโœ“โœ“
CI-deployed migrationsโœ“โœ“
SMTP_CONFIGURED flagfalse (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.

src/
app/

Onboarding content only โ€” the landing page you're looking at right now, this file-tree section, the setup guide below, and the /dashboard example. It's a route group: the parentheses keep it out of the URL, so deleting the whole folder won't break any other route. Delete it once you've replaced the home page with your own. ยท delete when ready

Every auth route and its shared UI, all in one place โ€” not to be confused with lib/auth/ further down, which holds the actual signUp/signIn/signOut logic this folder calls into.

A real, non-deletable protected page โ†’ "/settings" โ€” not inside (deleteWhenReady) since account settings is a feature most apps actually keep. Reads the profile via lib/profile/queries.ts; DisplayNameForm.tsx writes it back through lib/settings/actions.ts.

lib/

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.

You're helping me take my copy of the AuthTemplate repo (Next.js + Supabase auth starter) from a fresh clone to a deployed app. Work through these in order. Before moving from one numbered section to the next, check that section off in SETUP_CHECKLIST.md (edit the actual checkbox, don't just tell me it's done) and check in with me โ€” do this at every single section, not just some of them, it's the same step as moving on, not a separate thing to remember. Rule for the whole thing: never ask me to paste an API key, access token, or password directly into this chat. When a step needs a real secret, either have me paste it straight into a dashboard myself, or run the tool's own secure prompt (`gh secret set`, `vercel env add`, etc.) โ€” those ask for the value directly in the terminal, hidden, without it ever passing through you. Another rule for the whole thing: never run `git commit` or `git push` on your own. Stage changes and show me what's about to be committed (`git status` / `git diff`), then wait for me to say go ahead โ€” every time, not just once at the start. This applies to SETUP_CHECKLIST.md same as any other file. Whenever you're about to share more than one link with me in a message, put them in a small markdown table (columns: Link, What it's for) instead of a bare list โ€” easier to scan. Before doing anything else, read SETUP_CHECKLIST.md at the repo root โ€” it ships with every checkbox unchecked and mirrors every step below. If any boxes are already checked, resume from the first unchecked one instead of starting over or re-asking me things it already answered (check its Notes section too โ€” that's where earlier facts like my project ref or deployed URL should already be). Whenever you check something off, also add anything later steps will need to the Notes section, and stage that file's changes along with whatever else you're about to commit at each step, same as any other file โ€” it's meant to be real project history, not scratch state. ## 1. Get the code Check whether the current directory is already this repo (look for "auth-template" in package.json, or check `git remote -v`). If not, ask me for the repo URL โ€” the copy I created by clicking "Use this template" on https://github.com/rl3020/AuthTemplate, not the template itself โ€” then clone it and cd in. ## 2. Local setup 1. Run `npm install`. 2. Confirm Docker is installed and running (`docker info`). If not, tell me to install/start Docker Desktop and wait for me. 3. Run `npx supabase start` and read the local API URL and Publishable key it prints. 4. Copy `.env.example` to `.env.local`, fill in NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY with those local values, and leave NEXT_PUBLIC_SITE_URL as http://localhost:3000. 5. Run `npm run dev` and confirm http://localhost:3000 loads. Ask me to confirm it looks right before continuing. 6. Ask me for a short project name (e.g. "my-app"), then rename it from the template's own identity to mine, now rather than at final cleanup: in supabase/config.toml, change the top-level `project_id` on line 5 from "auth-template" to that name (purely a local dev label โ€” it's why `supabase start` printed "auth-template" before this, not a bug, just confusing to leave). Then update the title and description in src/app/layout.tsx's metadata to match. Leave the `[remotes.production]` block further down in config.toml alone for now โ€” that's a separate placeholder (the template author's own Supabase ref and domain) that only matters if I pick the Production path in step 8. ## 3. Create a hosted Supabase project This part is manual on my end โ€” walk me through it, don't try to do it yourself: - Tell me to go to https://supabase.com/dashboard, click "New project", pick an org/region, and set a database password when prompted โ€” tell me to write that password down myself, Supabase won't show it again, and not to paste it here. - Ask me for the Project URL and Publishable key (Project Settings โ†’ API) once it finishes provisioning โ€” those aren't secret, safe to paste here. - Ask me for the project ref too (the string in the dashboard URL, supabase.com/dashboard/project/<ref>, also under Project Settings โ†’ General) โ€” I'll reuse this in steps 5 and (if I go with Production) later. Not secret, safe to paste here. ## 4. Generate a Supabase access token Manual: tell me to create one at https://supabase.com/dashboard/account/tokens, scoped to this project. This is a real secret โ€” tell me to hold onto it for step 5, not paste it here. ## 5. Add the GitHub repository secrets Three are needed: SUPABASE_ACCESS_TOKEN (from step 4), SUPABASE_PROJECT_REF (from step 3), and SUPABASE_DB_PASSWORD (from step 3). If the `gh` CLI is installed and authenticated against my repo, run `gh secret set SUPABASE_ACCESS_TOKEN --repo <owner>/<repo>` for each one โ€” it prompts for the value directly in the terminal. If `gh` isn't available, tell me to add them manually at my repo โ†’ Settings โ†’ Secrets and variables โ†’ Actions โ†’ New repository secret, and wait for me to confirm. ## 6. First migrations supabase/migrations/ already has two โ€” nothing to create. One creates the profiles table with RLS policies; the other grants the authenticated role actual table-level access to it (RLS alone doesn't grant that โ€” without it, every fresh hosted project hits "permission denied for table profiles" the moment anything touches it, even though local dev looks completely fine, since supabase start/db reset bootstraps default grants locally that a hosted project doesn't get). Both apply automatically the first time the "Deploy Supabase Migrations" GitHub Actions workflow runs after step 5. Tell me to trigger it: repo's Actions tab โ†’ "Deploy Supabase Migrations" โ†’ "Run workflow" button (it has a manual trigger built in, no new commit needed). This workflow only handles migrations โ€” auth/email config is a separate "Deploy Supabase Config" workflow, covered in the Production path below. Before moving on, check my repo's Settings โ†’ Secrets and variables โ†’ Actions โ†’ Variables tab for an `SMTP_CONFIGURED` variable โ€” there shouldn't be one yet (some copies of this template were generated from an earlier version that hardcoded "true" straight into .github/workflows/config.yml, 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). If you find one already set to "true" and I haven't set up SMTP yet, delete it before continuing. If I ever ask you to add a new table later: enable RLS, add policies, *and* explicitly grant select/insert/update/delete on it to authenticated in the same migration โ€” db reset won't catch a missing grant, only a real deploy will, so this is easy to miss. ## 7. Deploy to Vercel Prefer the Vercel CLI so this stays scriptable: 1. `npx vercel login` if I'm not already logged in. 2. `npx vercel link` to connect this project (create a new Vercel project if prompted). 3. `npx vercel env add NEXT_PUBLIC_SUPABASE_URL production` and the same for NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY โ€” each prompts for the value in the terminal; use the hosted values from step 3. Don't bother with NEXT_PUBLIC_SITE_URL โ€” src/lib/site.ts already falls back to Vercel's own VERCEL_PROJECT_PRODUCTION_URL/VERCEL_URL when it's unset, so it's only needed if I'm using a custom domain. 4. `npx vercel --prod` to deploy. 5. Ask if I want a custom domain instead of the default *.vercel.app one โ€” optional, skip if not. If yes: manual on my end, tell me to add it under the Vercel project โ†’ Settings โ†’ Domains, add the DNS records Vercel shows at my registrar, and wait for it to verify. Once it's set as the Production domain, VERCEL_PROJECT_PRODUCTION_URL picks it up automatically โ€” no code change needed. ## 8. You're done โ€” nothing else to set up There's no SMTP_CONFIGURED repo variable set, and that's correct as-is โ€” don't add one. Confirm with me that: - Sign-up, login, sessions, protected routes, and the settings page all work end to end. - Forgot/reset password does *not* work yet โ€” that's expected on this path, not a bug. It only needs Supabase's free tier and no domain purchase, which is the whole point of this path. There's nothing to touch in supabase/config.toml on this path either โ€” site_url and the redirect-URL list only matter for email flows (confirmation/recovery), and this path doesn't send any, so the localhost defaults there are harmless. Tell me this is revisitable any time โ€” if I ever want working forgot/reset password, the fix is running through the Production version of this prompt, not redoing anything above. ## 9. Clean up Once everything above is confirmed working: delete the entire src/app/(deleteWhenReady)/ folder โ€” onboarding content only (this landing page, the setup guide, an example dashboard), safe to remove since it's an isolated route group and won't break /auth/*, /settings, or src/lib/. (Project naming and layout.tsx's metadata were already handled back in step 2 โ€” nothing left to rename here.) Also delete SETUP_CHECKLIST.md โ€” it was only ever meant to track this setup, not stick around afterward. Then clean up README.md, which still describes this as the template rather than my app: rewrite the title and the "Quickstart" intro section (tech stack / "What's already wired" / "What's not" โ€” that's template-pitch copy, not documentation of my app) to describe what I'm actually building. Also update the "Routing" table โ€” drop the /dashboard row once that example page is deleted, and adjust anything else there that no longer matches reality. Throughout: ask before anything destructive, and stop and tell me plainly if a command fails instead of guessing around it.

Setup guide

Prerequisites

01

Install Node.js

Version 20.19 or newer (22 or 24 LTS recommended).

nodejs.org
Terminal
node -v
02

Install 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.

Get Docker
Terminal
docker info
03

What 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

04

Use this template

Click "Use this template" on GitHub to get your own copy, then clone it.

github.com/rl3020/AuthTemplate
Terminal
git clone <your-repo-url>

Local setup

05

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
package.json
Terminal
npm install
npx supabase start
06

Set 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
Terminal
cp .env.example .env.local
npm run dev
07

Rename 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

08

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
supabase.com/dashboard

Generate an access token

09

Create a personal access token

Scope it to this project โ€” it's used by GitHub Actions to push migrations.

Account โ†’ Access Tokens

Wire up GitHub Actions

10

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
GitHub docs: creating repository secrets
GitHub Secrets
SUPABASE_ACCESS_TOKEN
SUPABASE_PROJECT_REF
SUPABASE_DB_PASSWORD

Your first migration is already here

11

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.

supabase/migrations/20260830005405_init.sql
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();
12

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.

supabase/migrations/20260904034640_grant_profiles_access.sql
grant select, insert, update, delete on public.profiles to authenticated;
13

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
GitHub docs: manually running a workflow
14

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.

15

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;
Example: your next migration
npx supabase migration new add_posts_table
npx supabase db reset
git add supabase/migrations
git commit -m "Add posts table"
git push

Deploy to Vercel

16

Import the repo

vercel.com/new
17

Add 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
Vercel Env Vars
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
18

Add 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.

free

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.

production

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:

supabase config push
unexpected status 400: {"message":"Email template modification is
not available for free tier projects using the default email
provider. 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:

supabase/config.toml
[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
Supabase SMTP docs

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