Setting up a project: the full reference

Every provider the builder offers, in the order to set them up. Untick what you don't use to get a shorter guide.

Build this guide for your project

Providers
Next.js

1. Run the project

  1. 1Unzip the download and open a terminal in the folder. Run npm install.
  2. 2Copy .env.example to .env.local and fill in the 6 required values: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_SITE_URL, RESEND_API_KEY, EMAIL_FROM, CONTACT_TO_EMAIL. Each provider section below says where to get them.
  3. 3Run npm run dev and open http://localhost:3000. This is a Next.js 16 project (Turbopack by default); it needs Node 20 or newer.
  4. 4The README.md inside the project repeats this guide under Go live, so you can hand the folder to someone else.

Pages in the Business website template, plus the sign-in pages Supabase adds

//products/pricing/about/support/contact/terms/privacy/login/signup/forgot-password/account

With Cloudflare on, this project deploys as a Worker (it has server code for Supabase and Resend). The Cloudflare section below covers that mode.

2. Supabase (auth and database)

Your project ships with sign in, sign up, a protected account page and a Postgres profiles table. It needs two values from your Supabase project. Nothing else changes.

Values you will paste into .env.local

  • NEXT_PUBLIC_SUPABASE_URLProject URL from Project settings → API.
  • NEXT_PUBLIC_SUPABASE_ANON_KEYThe anon public key (newer projects call it the publishable key, sb_publishable_…). Never the service role or secret key: this key is the only Supabase credential the site holds.
  • NEXT_PUBLIC_AUTH_GOOGLESet to 1 to show a “Continue with Google” button on the sign-in and sign-up pages, after enabling the Google provider in Supabase (see the guide).
  • NEXT_PUBLIC_SITE_URLThe public URL of your site, without a trailing slash. Links in emails (sign-up confirmation, password reset, newsletter confirmation) are built from it, never from the incoming request. http://localhost:3000 while developing; your real domain, e.g. https://www.yourdomain.com, when you deploy.

Get your keys

  1. 1Go to supabase.com/dashboard and create a project. The free tier is enough. Pick a region close to your users and save the database password somewhere safe (you won't need it for this guide).
  2. 2In the left sidebar open Project settings (the gear icon), then API.
  3. 3Copy the Project URL. It looks like https://abcdefghijkl.supabase.co.
  4. 4Copy the anon public key. Newer projects call this the publishable key and it starts with sb_publishable_; either works. Do not copy the service role or secret key, it must never be in a browser app.

Paste them into the env file

  1. 1In the project folder, copy .env.example to .env.local. On macOS or Linux: cp .env.example .env.local. On Windows: copy .env.example .env.local.
  2. 2Open .env.local and fill in both values:
NEXT_PUBLIC_SUPABASE_URL=https://abcdefghijkl.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_xxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_SITE_URL=http://localhost:3000

.env.local is ignored by git, so your keys stay out of version control. When you deploy, set the same variables in the host's environment settings, with NEXT_PUBLIC_SITE_URL changed to your real domain: the confirmation and reset emails link back to it.

Create the database table

  1. 1In the Supabase dashboard open SQL editor and click New query.
  2. 2Paste the contents of supabase/migrations/0001_profiles.sql from your project and click Run. This creates a profiles table with row-level security and a trigger that fills it in when someone signs up.

Allow sign-in redirects

  1. 1Open Authentication → URL configuration.
  2. 2Set Site URL to http://localhost:3000. When you deploy, change it to your domain, the same value as NEXT_PUBLIC_SITE_URL.
  3. 3Under Redirect URLs add http://localhost:3000/auth/callback**. Add your production domain the same way later, for example https://example.com/auth/callback**. The two stars let the link carry a next= parameter, which the password reset uses.
  4. 4The site asks Supabase to send people to NEXT_PUBLIC_SITE_URL + /auth/callback; a destination that is not on the Site URL's domain or in the Redirect URLs is ignored and the email links to the Site URL instead.
  5. 5Optional: under Authentication → Providers → Email, turn off Confirm email while developing so you can sign in immediately after signing up.

Try it

  1. 1Restart npm run dev so the new env file is read.
  2. 2Open http://localhost:3000/signup, create an account, confirm the email if required, and sign in. You land on /account, where you can edit your name; it reads and writes your row in the profiles table.
  3. 3Try Forgot password? on the sign-in page: the email link opens /reset-password with a recovery session so you can choose a new password.

Google sign-in (optional)

  1. 1In Google Cloud Console create an OAuth client ID of type Web application. Add https://YOUR-PROJECT-REF.supabase.co/auth/v1/callback as an authorised redirect URI (the exact value is shown in Supabase under Authentication → Providers → Google).
  2. 2In Supabase open Authentication → Providers → Google, enable it, and paste the client ID and secret.
  3. 3Set NEXT_PUBLIC_AUTH_GOOGLE=1 in .env.local and restart. A Continue with Google button appears on the sign-in and sign-up pages.

Prefer the CLI to the SQL editor?

  1. 1Run npx supabase@latest login, then npx supabase@latest link --project-ref YOUR-PROJECT-REF (the ref is the first part of your Project URL).
  2. 2Run npm run db:push to apply every file in supabase/migrations/. Re-run it whenever you add a migration.

What was added to your project

  • lib/supabase/client.ts, server.ts, session.ts, env.ts
  • proxy.ts (Next 16) or middleware.ts (Next 15)
  • app/login, app/signup, app/forgot-password, app/reset-password, app/account
  • app/auth/actions.ts (sign in/up/out, password reset, profile update, Google), app/auth/callback
  • lib/safe-next.ts (keeps post-sign-in redirects on this site)
  • lib/site-url.ts (the origin for links in emails, from NEXT_PUBLIC_SITE_URL)
  • components/auth/auth-card.tsx
  • supabase/migrations/0001_profiles.sql, supabase/config.toml, npm run db:push

If something goes wrong

  • “Supabase is not configured yet”: The env file is missing or the dev server was started before you saved it. Check the file name is exactly .env.local and restart.
  • Sign-up email never arrives: Check spam, or turn off Confirm email under Authentication → Providers → Email while developing. Supabase's built-in mailer is rate-limited; for production, configure SMTP under Authentication → SMTP settings.
  • Redirected to an error after clicking the confirmation link: The callback URL is not in Redirect URLs. Add it exactly as shown.
  • “The site's public URL is not set”: On a deployed site NEXT_PUBLIC_SITE_URL is missing or still http://localhost:3000. Set it to your domain in the host's environment (with Cloudflare: in .env.local, then npm run cf:secrets).
  • Account page shows no name: The SQL migration has not been run, so there is no profiles row. Run it and sign up again.
  • Reset link opens /login instead of the password form: The link expired (they last one hour) or was opened in a different browser. Request a new one.
  • Google button does nothing or errors: Check the redirect URI in Google Cloud matches the one Supabase shows exactly, and that the provider is enabled with the client secret saved.

3. Resend (email)

Your project's contact form and newsletter signup send real email through Resend. You need an API key and two addresses. Until they are set, the forms show a friendly "not configured" message instead of failing.

Values you will paste into .env.local

  • RESEND_API_KEYAPI key from the Resend dashboard → API Keys → Create. Permission: Sending access; you can also restrict it to your sending domain. That is all the site needs.Secret.
  • EMAIL_FROMThe address emails are sent from. Must be on a domain you verified in Resend, e.g. hello@yourdomain.com. While testing you can use onboarding@resend.dev.
  • CONTACT_TO_EMAILWhere contact form messages are delivered. Any inbox you read.
  • RESEND_AUDIENCE_IDResend audience to add newsletter subscribers to (Audiences → your audience → the id in the URL). Only used when Supabase is off; with Supabase on, subscribers are stored in your database.
  • FORMS_SECRETUsed when Supabase is on. A password only your server knows: the database functions that store subscribers and contact messages refuse any call without it, so the public Supabase key alone can never write to your lists. Running supabase/migrations/0002_forms.sql prints the value; paste it here.Secret.

Get an API key

  1. 1Create an account at resend.com. The free tier covers a small site.
  2. 2Open API Keys in the left sidebar and click Create API Key. Name it after your site, give it Sending access, and copy the key. It is shown once.

Verify your domain

  1. 1Open Domains and click Add Domain. Enter the domain your site will live on, for example yourdomain.com.
  2. 2Resend shows DNS records to add (an MX, a TXT for SPF, and a TXT for DKIM). Add them at your DNS provider. If your DNS is on Cloudflare: DNS → Records → Add record, one per row, exactly as shown.
  3. 3Click Verify. It usually takes a few minutes, occasionally up to an hour.
  4. 4Until the domain verifies, you can test with EMAIL_FROM=onboarding@resend.dev, which can only send to the email address of your Resend account.

Paste the values into the env file

  1. 1If you haven't yet, copy .env.example to .env.local.
  2. 2Fill in the Resend section:
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
EMAIL_FROM=hello@yourdomain.com
CONTACT_TO_EMAIL=you@yourdomain.com
# With Supabase on: the value supabase/migrations/0002_forms.sql prints
FORMS_SECRET=
# Without Supabase: newsletter storage in a Resend audience
RESEND_AUDIENCE_ID=

RESEND_API_KEY is a secret: keep it out of git and out of the browser. It is only read on the server.

Newsletter storage

  1. 1With Supabase enabled, subscribers are stored in your database with double opt-in: run supabase/migrations/0002_forms.sql in the SQL editor (after the profiles migration). Contact messages are kept there too.
  2. 2The file's last line prints a forms secret. Copy it into .env.local as FORMS_SECRET. Your site's Supabase key is public (every visitor's browser has it), so the tables are closed to it entirely; the three functions the file creates (subscribe, confirm, store a message) answer only when the server also sends this secret. Nobody can read, edit or add to your lists without it. Running the file again keeps the same secret; to read it later, run select secret from private.forms_secret;.
  3. 3The confirmation link is built from NEXT_PUBLIC_SITE_URL (see the Supabase section), so set that to your domain when you deploy.
  4. 4Unsubscribing: every subscriber row has an unsubscribe_token, and /newsletter/unsubscribe?token=… with it opens a page with an Unsubscribe button (the welcome email already carries the link). When you send a newsletter, put each subscriber's link at the bottom: https://yourdomain.com/newsletter/unsubscribe?token= followed by their token. Your list to send to is select email, unsubscribe_token from subscribers where confirmed;, and unsubscribing clears confirmed, so they drop out of it.
  5. 5Without Supabase, create an Audience in Resend and paste its id into RESEND_AUDIENCE_ID. Subscribers are added directly and receive a welcome email.

Try it

  1. 1Restart npm run dev, open /contact, and send yourself a message. It arrives at CONTACT_TO_EMAIL, and the sender gets an auto-reply.
  2. 2Enter an email in the footer newsletter box. With Supabase on, click the confirmation link; otherwise the welcome email arrives straight away.
  3. 3Use Resend's Emails page to see every message and its delivery status.

Use Resend for Supabase's sign-up emails too

  1. 1Supabase's built-in mailer allows only a few emails per hour and is meant for testing.
  2. 2In Supabase open Authentication → SMTP Settings, enable Custom SMTP, and enter host smtp.resend.com, port 465, username resend, and your Resend API key as the password. Sender email is your EMAIL_FROM.

What was added to your project

  • lib/email/resend.ts, lib/email/templates.ts
  • lib/forms/guard.ts (honeypot + rate limit)
  • lib/safe-next.ts (keeps the redirect back to the form on this site)
  • components/forms/form-status.tsx
  • app/contact/actions.ts, app/newsletter/actions.ts
  • app/newsletter/confirm/route.ts, app/newsletter/unsubscribe/page.tsx and supabase/migrations/0002_forms.sql (when Supabase is on)
  • lib/forms/messages.ts (the copy behind every ?sent= and ?error= code)

If something goes wrong

  • “Email is not configured yet”: One of the three required values is missing from .env.local, or the dev server was started before you saved it. Restart it.
  • Resend returns 403 or “domain is not verified”: EMAIL_FROM must be on a verified domain. Check the DNS records in Resend → Domains, or use onboarding@resend.dev while testing.
  • The message says sent but nothing arrives: Check Resend → Emails for the delivery status, then the spam folder of CONTACT_TO_EMAIL.
  • “Too many messages from this connection”: The rate limit is 5 submissions per 10 minutes per IP. Wait, or raise MAX_PER_WINDOW in lib/forms/guard.ts.
  • “Form storage is not configured yet”: FORMS_SECRET is missing from .env.local, or still the placeholder. Run supabase/migrations/0002_forms.sql in the SQL editor and paste the 64-character value it prints; on a deployed site push it with npm run cf:secrets.
  • “The site's public URL is not set”: With Supabase on, the newsletter confirmation link is built from NEXT_PUBLIC_SITE_URL. On a deployed site it is missing or still http://localhost:3000; set it to your domain in the host's environment (with Cloudflare: in .env.local, then npm run cf:secrets).

4. Cloudflare (hosting, DNS and domain)

Your project deploys to Cloudflare with npm run deploy. If it uses Supabase or Resend it runs as a Cloudflare Worker (full Next.js, via OpenNext); otherwise it is exported as static files and served from Cloudflare's edge. Either way the free tier is enough to start.

Deploy for the first time

  1. 1Cloudflare's tool (Wrangler) needs Node.js 22 or newer. Check with node -v; if you are on an older version, install the current LTS from nodejs.org or with nvm install 22.
  2. 2Create an account at dash.cloudflare.com. No card needed for the free plan.
  3. 3In the project folder run npx wrangler login. A browser window asks you to allow Wrangler; approve it and come back to the terminal.
  4. 4Optional but recommended: run npm run preview to build and run the site locally exactly as it will run on Cloudflare. Open the printed URL, click around, then stop it with Ctrl+C.
  5. 5Run npm run deploy. The first time, Wrangler asks to create the Worker. When it finishes it prints a URL ending in .workers.dev. Open it: your site is live.

Every later deploy is the same single command. Deploys take about a minute and are atomic: visitors never see a half-updated site.

Set your production keys

  1. 1Run npm run cf:secrets. It reads .env.local and pushes every value to Cloudflare as a Worker secret, the public NEXT_PUBLIC_ ones included: your deployed site reads them at request time, and Cloudflare only knows what you have pushed. Re-run it whenever you change a value.
  2. 2Public values also need to be in .env.local when you run npm run deploy, because the browser side of the site gets them inlined at build time. Keep the file filled in and both sides agree.
  3. 3You can also see and edit them in the dashboard: Workers & Pages → your Worker → Settings → Variables and Secrets.

Static sites have no server-side values, so this step does not apply to them.

Deploy from CI (optional)

  1. 1Create an API token at My Profile → API Tokens → Create Token → Create Custom Token with exactly these permissions, each an Account row, scoped to your account: Workers Scripts · Edit, Account Settings · Read. Nothing else is needed for npm run deploy.
  2. 2In your CI, set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID (the account id is on the dashboard's overview page), then run npm ci and npm run deploy. Wrangler uses the token instead of a browser login.

These are the same permissions BytesJoy's own weekly check deploys with, so they are known to be sufficient.

Use your own domain

  1. 1Buy a domain in Cloudflare → Domain Registration (at-cost pricing), or add one you already own with Add a domain and switch its nameservers to the two Cloudflare gives you at your current registrar. Nameserver changes take minutes to a day.
  2. 2Open Workers & Pages → your Worker → Settings → Domains & Routes → Add → Custom domain, and enter yourdomain.com. Cloudflare creates the DNS record and certificate for you. Add www.yourdomain.com the same way if you want both.
  3. 3Visit the domain. HTTPS works automatically.

Update the other providers for production

  1. 1Supabase: under Authentication → URL configuration, set the Site URL to https://yourdomain.com and add https://yourdomain.com/auth/callback to Redirect URLs.
  2. 2Resend: the sending domain you verified should be this domain or a subdomain of it. If your DNS is on Cloudflare now, add the Resend DNS records under DNS → Records.
  3. 3If you changed any NEXT_PUBLIC_ value, run npm run deploy again so it is rebuilt in.

What was added to your project

  • wrangler.jsonc
  • open-next.config.ts and scripts/cf-secrets.mjs (Worker mode)
  • next.config.ts: images.unoptimized, plus output: "export" in static mode
  • npm scripts: preview, deploy, cf:secrets, cf:typegen

If something goes wrong

  • “Wrangler requires at least Node.js v22”: Upgrade Node: nvm install 22 && nvm use 22, or download it from nodejs.org. The site itself runs on Node 20+, only the deploy tool needs 22.
  • “You are not authenticated” or “not logged in”: Run npx wrangler login again. On a server or CI, set CLOUDFLARE_API_TOKEN instead.
  • Deploy fails with a size limit error: The free plan allows a 3 MB compressed Worker, paid plans 10 MB. Remove unused dependencies, or upgrade the plan.
  • Forms or sign-in work locally but not on the deployed site (a setup notice, or “not configured”): The site's values are missing on Cloudflare. Fill in .env.local, run npm run cf:secrets, then npm run deploy again.
  • Changed a NEXT_PUBLIC_ value but the site still shows the old one: Those are compiled in. Run npm run deploy again after changing .env.local.
  • Custom domain shows an error page or old DNS: Make sure the domain's nameservers point at Cloudflare and that you added it under the Worker's Domains & Routes, not just as a DNS record.

5. Code graph (for coding agents)

Your project can describe itself as a graph: pages, the components on them, routes, server actions, database tables, env vars and dependencies, with the relationships between them. Coding agents query it instead of reading every file. Nothing about the running site changes.

Values you will paste into .env.local

  • NEO4J_URIBolt URI of your Neo4j instance, e.g. neo4j://localhost:7687 or the neo4j+s:// URI from Aura. Only needed for npm run graph:push.
  • NEO4J_USERDatabase user. Defaults to neo4j.
  • NEO4J_PASSWORDDatabase password.Secret.

Build the graph

  1. 1Run npm run graph. It writes graph/nodes.jsonl, graph/edges.jsonl and graph/import.cypher in about a second, and also runs automatically before npm run dev.
  2. 2Open graph/SCHEMA.md for the node labels, relationships and ready-made queries.
  3. 3While working, npm run graph:watch in a second terminal keeps the graph current as files change.

Load it into Neo4j

  1. 1Get a database: Neo4j Desktop (local app), Docker (docker run -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH=neo4j/your-password neo4j), or a free Aura instance.
  2. 2Add the connection to .env.local:
  3. 3Run npm run graph:push. It merges nodes and edges, so re-running after changes updates in place.
  4. 4Open Neo4j Browser (http://localhost:7474 locally) and try a query from graph/SCHEMA.md, for example MATCH (p:Page)-[:CONTAINS*]->(c:Component) RETURN p.title, count(c).
NEO4J_URI=neo4j://localhost:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=your-neo4j-password

No Neo4j? The JSONL files are plain data; any agent can read them directly, and import.cypher pastes into any Cypher console.

Point a coding agent at it

  1. 1AGENTS.md in the project root explains the layout, conventions and how to use the graph. Claude Code, Cursor and Codex read it automatically.
  2. 2For live queries, run the official Neo4j MCP server (mcp-neo4j-cypher) against your database and add it to the agent's MCP config. The agent can then ask questions like “which modules write to the subscribers table?” in Cypher.

What was added to your project

  • scripts/extract_graph.mjs (+ extract_graph.js alias), scripts/graph_push.mjs
  • graph/SCHEMA.md, graph/spec-graph.json
  • the graph section of AGENTS.md
  • npm scripts: graph, graph:watch, graph:push, predev

If something goes wrong

  • npm run graph:push says NEO4J_URI is not set: Add the three NEO4J_ values to .env.local. Pushing is optional; npm run graph alone writes the files.
  • Connection refused on push: Neo4j isn't running, or the URI scheme is wrong. Local: neo4j://localhost:7687. Aura: use the neo4j+s:// URI from the console.
  • The graph is missing something I just wrote: Run npm run graph again (or keep npm run graph:watch running). The extractor scans app/, components/, lib/ and emails/.

This is everything. Back to the builder