Supakit + Supabase setup
A short, copy-paste-friendly walkthrough to connect Supabase to your app: where to find each key, which ones are safe to ship to the browser, and which must stay on the server.
Prerequisites
- Node.js 18+ (or Bun 1.0+) and a package manager (
npm,pnpm, orbun). - A Supabase account (free tier is fine) — supabase.com.
- A React/TypeScript app (Vite, Next.js, TanStack Start, etc.) where you can add environment variables.
- Ability to set environment variables in your hosting provider (for production secrets).
1. Create a Supabase project
- Go to supabase.com/dashboard and click New project.
- Pick an organization, name the project, and set a strong database password.
- Choose the region closest to your users and wait for provisioning (~1 min).
2. Copy the API keys
In the Supabase dashboard, open Project Settings → API. You will need three values:
Project URL
Safe in browserBase URL of your Supabase project. Shipped to the browser.
- Env var
- SUPABASE_URL / VITE_SUPABASE_URL
- Where to find it
- Project Settings → API → Project URL
Publishable (anon) key
Safe in browserUsed by the browser client. Combined with Row Level Security, it is safe to ship publicly.
- Env var
- SUPABASE_PUBLISHABLE_KEY / VITE_SUPABASE_PUBLISHABLE_KEY
- Where to find it
- Project Settings → API → Project API keys → anon / public
Service role key
Server onlyBypasses RLS. Never expose to the browser. Server-side admin work only.
- Env var
- SUPABASE_SERVICE_ROLE_KEY
- Where to find it
- Project Settings → API → Project API keys → service_role (reveal)
3. Store the secrets
Add each value as a secret in your hosting provider — never commit them to git.
- Project URL and publishable key can also be exposed to the browser as
VITE_*variables. - Service role key must only exist on the server. Read it inside server functions via
process.env.SUPABASE_SERVICE_ROLE_KEY. - Rotate keys from the same API settings page if a secret ever leaks.
4. End-to-end example
Install the client, configure env vars, create a table, then insert and read a row.
a. Install
npm install @supabase/supabase-js # or: bun add @supabase/supabase-js
b. .env.local
VITE_SUPABASE_URL=https://<project-ref>.supabase.co VITE_SUPABASE_PUBLISHABLE_KEY=<your-anon-key> # Server only — never prefix with VITE_ SUPABASE_SERVICE_ROLE_KEY=<your-service-role-key>
c. Create a table (SQL)
create table public.notes ( id uuid primary key default gen_random_uuid(), body text not null, created_at timestamptz not null default now() ); grant select, insert on public.notes to anon, authenticated; alter table public.notes enable row level security; create policy "anyone can read notes" on public.notes for select to anon, authenticated using (true); create policy "anyone can insert notes" on public.notes for insert to anon, authenticated with check (true);
d. Client + read & write
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY,
);
// Insert
const { data: inserted, error: insertError } = await supabase
.from("notes")
.insert({ body: "hello from supakit" })
.select()
.single();
// Read
const { data: notes, error: readError } = await supabase
.from("notes")
.select("id, body, created_at")
.order("created_at", { ascending: false })
.limit(10);
console.log({ inserted, insertError, notes, readError });A populated inserted row plus a non-empty notes array confirms the URL, key, table grants, and RLS policies are all wired correctly. A 401/403 usually means a missing policy for the anon role.
Cloudflare Turnstile troubleshooting
NSO protects its sign-up and sign-in forms with a Cloudflare Turnstile challenge. When the widget is misconfigured, the most common symptom is an 110200 (invalid domain) error on submit, or a form that silently never completes sign-in. Here is how to check and fix it.
Hostname management
Turnstile widgets only render for hostnames that are allowlisted on the widget in the Cloudflare dashboard. Add every host your app runs on:
| Environment | Hostname to add | Added | Verified |
|---|---|---|---|
| Custom domain (root)Always required | nso.jackiepoole.com | ||
| Custom domain (www)Only if you serve it | www.nso.jackiepoole.com | ||
| Published Lovable URLPublic site | supakit-guide-space.lovable.app | ||
| Preview URLCurrent preview | id-preview--4104f88b-583c-4730-a530-650dcf175a90.lovable.app |
The allowlist does not auto-include subdomains or www, so add each hostname above explicitly. Copy each value straight into Cloudflare Hostname Management for the widget. Keep the widget mode as-is (Managed) — only the hostname list needs to change. Since the site key itself is unchanged, no redeploy is required after saving.
How to confirm sign-up and sign-in work
- Open the sign-in page in an incognito window (avoid cached scripts).
- Check the widget: you should see the Turnstile challenge (“Verify you are human”) rather than a blank area or an error message.
- Open the browser console — there should be no
110200 (invalid domain)error. - Tick the checkbox, complete the form, and confirm you land on your dashboard.
- Repeat once on the published URL and once on the custom domain to confirm the hostname list covers all environments.
Confirm the token & endpoints in DevTools
Beyond ticking the box, you can confirm in the browser DevTools that a Turnstile token was actually generated and that the challenge endpoints respond 200 for the hostname you are on. Follow these steps on each environment.
- Open the page, press F12 (or Cmd/Ctrl + Shift + I), then switch to the Network tab.
- Reload the page and tick the Turnstile checkbox if one is shown. In the Network tab, filter by
challenges.cloudflare.com. Every request in that list should show a 200 status — a 403 or110200 (invalid domain)response here means the hostname is missing from the widget’s Hostname Management list. - Next, switch to the Console tab and run:
window.__NSO_TURNSTILE_TOKEN
The page stores the freshly minted token onwindow.__NSO_TURNSTILE_TOKENafter a successful challenge. You should see a long base64-encoded string (notundefinedand not thedev-bypasssentinel that appears when the site key is missing). - Repeat this on the custom domain, the published Lovable URL, and the preview URL. A valid token on one host and none on another points at the hostname allowlist, not the site key or secret.
Copy-paste curl checks
From a terminal you can quickly confirm two things: that the Turnstile challenge endpoint is reachable, and that every hostname you use is serving the widget-enabled auth page. Run these against each environment.
1. Is the challenge endpoint up? The widget pulls its script from Cloudflare’s challenge host. It responds with a redirect to a CDN asset, so use -L to follow it — you should end on a 200:
curl -sL -o /dev/null -w "%{http_code}\n" \
https://challenges.cloudflare.com/turnstile/v0/api.js2. Does each hostname serve the auth page? Loop over every host the app runs on. Some hosts (the published Lovable URL and the preview URL) redirect to a canonical origin first, so follow redirects with -L and you should land on a final 200:
for host in nso.jackiepoole.com \
supakit-guide-space.lovable.app \
id-preview--4104f88b-583c-4730-a530-650dcf175a90.lovable.app
do
code=$(curl -sL -o /dev/null -w "%{http_code}" "https://$host/auth")
printf "%-45s HTTP %s\n" "$host" "$code"
doneA final 200 for every host confirms the auth page (and the widget that renders on it) is being served there. A host you are not actually serving — like www.nso.jackiepoole.com unless you route it — will report 000 and is expected to fail. Because this app renders the Turnstile widget client-side, the site key lives in the JavaScript bundle rather than the page HTML — curl cannot see it, so use the DevTools steps above to confirm the token is minted. A 403 110200 error during the in-browser challenge still points at the hostname allowlist, which curl cannot fully reproduce — that last step always needs a real browser.
Need help?
Check the Supabase docs for API keys and Row Level Security. For the NSO-specific setup, see the Supabase setup guide.