Homeguides / supakit
Integration guide

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, or bun).
  • 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

  1. Go to supabase.com/dashboard and click New project.
  2. Pick an organization, name the project, and set a strong database password.
  3. 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 browser

Base 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 browser

Used 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 only

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

EnvironmentHostname to addAddedVerified
Custom domain (root)Always requirednso.jackiepoole.com
Custom domain (www)Only if you serve itwww.nso.jackiepoole.com
Published Lovable URLPublic sitesupakit-guide-space.lovable.app
Preview URLCurrent previewid-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

  1. Open the sign-in page in an incognito window (avoid cached scripts).
  2. Check the widget: you should see the Turnstile challenge (“Verify you are human”) rather than a blank area or an error message.
  3. Open the browser console — there should be no 110200 (invalid domain) error.
  4. Tick the checkbox, complete the form, and confirm you land on your dashboard.
  5. 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.

  1. Open the page, press F12 (or Cmd/Ctrl + Shift + I), then switch to the Network tab.
  2. 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 or 110200 (invalid domain) response here means the hostname is missing from the widget’s Hostname Management list.
  3. Next, switch to the Console tab and run:
    window.__NSO_TURNSTILE_TOKEN
    The page stores the freshly minted token on window.__NSO_TURNSTILE_TOKEN after a successful challenge. You should see a long base64-encoded string (not undefined and not the dev-bypass sentinel that appears when the site key is missing).
  4. 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.js

2. 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"
done

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

Still failing? Verify the site key in your environment matches the widget, and confirm every host in Hostname Management. A headless bot can never tick the interactive checkbox — that renders fine but produces an empty token and is expected, not a domain rejection.

Need help?

Check the Supabase docs for API keys and Row Level Security. For the NSO-specific setup, see the Supabase setup guide.