Vercel and Supabase, in that order of convenience and the reverse order of work. The application deploys in about four minutes; the database is the part that needs care, because a migration applied out of order to a project that already has people in it is not something you undo.
Read the whole of 1 before pressing anything in 2.
The deployed build talks to the same Supabase project the local one does. There is no separate production database until you make one, and nothing below creates it for you.
Supabase dashboard → SQL Editor → paste each file in
supabase/migrations/ in order, one at a time, waiting for each to succeed.
Or supabase db push with the CLI pointed at the project.
The live project's migration history is empty (everything was applied by
SQL), so Supabase cannot tell you what is applied. Ask the database instead:
check that the newest migration's functions and policies exist
(to_regprocedure('…'), pg_policies) before assuming it is current. Ours was
once five migrations behind with nothing visibly broken.
Reconcile by fingerprint, not by ledger. Build a database from the
repository (supabase/tests/00_supabase_shim.sql, then every migration) and
run the same query on both: a count and an md5 over every public function's
definition (blank lines stripped, names ordered collate "C"), every column,
trigger and policy, and the list of functions authenticated cannot execute.
Equal fingerprints mean production is what the repository says; a difference
tells you which bucket to look in. The query is in
docs/alignment-audit-2026-10-25.md.
Two things that bite when applying by hand:
- Supabase grants new functions to
anonby default. Every migration that adds a function should revoke it fromanonexplicitly; check it did. - A new function is invisible to the API until PostgREST reloads. The
error is
Could not find the function public.x in the schema cache. Migrations end withnotify pgrst, 'reload schema'; if you see that error, run that line on its own.
If you want a separate production project, make one, apply every migration to
it, and point Vercel at it while .env.local keeps the old one. Do not load
supabase/seed.sql into a real instance.
scope_rules ships with local set to one voice and no waiting period, so a new
install can get through a decision on its first day. On an instance with people
on it that means one person can decide alone:
update scope_rules set min_voices = 3, deliberation_days = 2 where scope = 'local';supabase/raise-the-floor.sql does every scale with its reasoning. Home now
states the rule in force under the scale selector, including the sentence that
matters on a fresh install: one voice means one person can decide alone.
-
vercel.com → Add New → Project → Import Git Repository. Authorise the GitHub app against
m4ck3nz13ideas-source/Sovereignif it asks. Vercel detects Next.js; the framework preset, build command and output directory are all correct as detected. Change nothing there. -
Environment variables, before the first deploy. Add these to Production, Preview and Development unless a line says otherwise:
Variable Value Required NEXT_PUBLIC_SUPABASE_URLSupabase → Project Settings → API yes NEXT_PUBLIC_SUPABASE_ANON_KEYsame page yes NEXT_PUBLIC_SITE_URLthe deployed origin, no trailing slash — Production only see below ANTHROPIC_API_KEYsk-ant-…no ANTHROPIC_MODELclaude-sonnet-5no NEXT_PUBLIC_WORLD_ID_APP_IDapp_…no NEXT_PUBLIC_WORLD_ID_ACTIONsovereign-personhoodno AI_SIGNING_SECRETopenssl rand -hex 32— the same value goes inprivate.ai_signing_keyyes, for AI results and law audits COMPANIES_HOUSE_API_KEYfree, from Companies House no The anon key is meant to be public — it is in the browser bundle by design, and RLS is what protects the data. The Anthropic key is server-side only and must never be given a
NEXT_PUBLIC_prefix.Without
ANTHROPIC_API_KEYthe whole loop still runs on the offline reviewer, and every review it writes says plainly that no model read it. That is a defensible thing to deploy; a fabricated reading is not.NEXT_PUBLIC_SITE_URLis required on a deployed install — see section 5. Set it on Production only, or preview deployments will send their magic links to the production site.AI_SIGNING_SECRETsigns every AI result the server writes (rule 38). Set it in Vercel and redeploy before inserting the same value intoprivate.ai_signing_key; with no key, law audits cannot be recorded at all (rule 47). -
Deploy. First build takes two to four minutes.
-
If the deployed site shows the setup page rather than the sign-in screen, the two Supabase variables did not reach the build.
src/proxy.tsredirects everything to/setupwhen either is missing. Add them and redeploy — changing an environment variable does not rebuild on its own.
This is the step that gets skipped, and the failure is silent: the email arrives, the link opens, and it lands back on the sign-in screen with nothing said.
LoginForm.tsx passes emailRedirectTo, and Supabase refuses any redirect
target that is not on its allow-list — quietly, by falling back.
Supabase dashboard → Authentication → URL Configuration:
- Site URL:
https://<your-domain>— the production origin, nothing else. This is the default used when noredirectTois given. - Redirect URLs, added one per line:
http://localhost:3000/**
https://<your-domain>/**
https://*-<your-vercel-team-slug>.vercel.app/**
The third line is only needed if you want magic links to work on preview deployments. It is a wildcard over every preview URL your account generates, which is the pattern Supabase documents for Vercel — and it is also a wider door than a production instance needs. If you are not testing auth on previews, leave it out.
Keep http://localhost:3000/** — removing it breaks local development, and
it is only reachable from the machine it names.
Supabase's built-in email service sends two messages per hour. Not two per user — two, for the whole project. Supabase are explicit that it exists for development and is not meant for production, and it is best-effort even within that.
Since the only way into this application is a magic link, that number is the
number of people who can reach it per hour, and you will hit it while testing
your own sign-in. The error is email rate limit exceeded, it is returned at
the send step rather than the link step, and it resets on a rolling hour.
So a custom SMTP provider is not a later refinement here. It is the thing standing between the deploy and anybody using it.
Project Settings → Authentication → SMTP Settings. Resend, Postmark,
SendGrid, AWS SES, Brevo and ZeptoMail all work; Resend is the least
ceremony and its free tier is far beyond anything this needs. You will need
a sender address on a domain you control, which means DKIM and SPF records
at the registrar — the same GoDaddy DNS panel as the www record, and the
provider gives you the exact values to paste.
Sending as something like sovereign@mackiavelli.co.uk also means the link
arrives from the same name as the site it points at, which matters more than
it sounds when you are asking three people to trust a governance application
they have never heard of.
Once SMTP is configured, Supabase applies a fresh limit of 30 messages per hour, adjustable under Authentication → Rate Limits. Raise it to something sane before an invite goes out, not after.
This install runs at www.mackiavelli.co.uk. Two records' worth of work and
three ways to be misled about the result.
- Vercel → project → Settings → Domains → add
www.mackiavelli.co.uk - GoDaddy → DNS → CNAME, host
www, valuecname.vercel-dns.com - Set
NEXT_PUBLIC_SITE_URL=https://www.mackiavelli.co.ukon Production - Change Site URL in Supabase to the same, and add
https://www.mackiavelli.co.uk/**to the redirect list - Redeploy
Adding the domain does not attach it to a build. A domain added after the most recent production deployment sits on the project verified and unaliased, and serves a 404 to everybody. Vercel's own domains page shows it as fine, because from Vercel's point of view it is. Either redeploy after adding the domain, or assign it to the current deployment explicitly. This cost an hour, misdiagnosed as DNS the whole time.
NEXT_PUBLIC_SITE_URL is not optional on a deployed install, whatever it
looks like. Sign-in redirects are built from it, and the fallback — the origin
the request arrived on — can resolve to the per-deployment *.vercel.app host
behind a proxy. On a project with deployment protection enabled that host is
behind Vercel's own login, so somebody who clicked a perfectly valid magic link
lands on a sign-in page belonging to another company while holding a working
session they cannot see. VERCEL_URL names exactly that host, which is why
nothing here falls back to it.
Environment variables that begin NEXT_PUBLIC_ are compiled into the
bundle. Changing one in the dashboard does nothing to the build already
serving. Redeploy, every time. Related: check the spelling of what is
already there. Ours had NEXT_PUB_SUPABASE_URL sitting alongside the real
one, which is not a variable, does nothing, and looks entirely correct in a
list.
A stale DNS answer looks exactly like a broken deploy. Check what the
record resolves to from outside your own machine
(dns.google/resolve?name=…&type=CNAME) before touching anything, and turn
GitHub Pages off under the repository's Settings → Pages so it stops
answering for the domain.
The apex mackiavelli.co.uk is still unclaimed here. Pointing it at Vercel
too, with an A record to 76.76.21.21 and a redirect to www, is a separate
five minutes whenever it matters.
The World ID browser step. The server half is finished and tested; the client widget needs an app registered at the World Developer Portal, and IDKit v4 has a different request shape from the v2 endpoint the adapter targets. Until it is connected, nobody on the instance can prove personhood, so resonance at national scale and above is closed to everyone equally. Local and regional are unaffected, as are reading, writing, questions and concerns at every scale. That is the correct behaviour rather than a workaround, and the personhood screen says so rather than showing a button that does nothing.
The readiness score is still author-written. The database checks that a
reading exists for this exact body and clears the bar; it cannot check that a
model produced it. That is fine while everybody using the instance knows each
other and is worth fixing before it is not. See docs/roadmap.md.
[ ] asked the database what it actually has, rather than assuming
[ ] every migration applied, in order, no errors
[ ] AI_SIGNING_SECRET in Vercel, redeployed, then the same value in private.ai_signing_key
[ ] founders: insert into founders (profile_id) select id from auth.users
where email = '<founder email>' on conflict do nothing; (0058)
[ ] scope_rules local floor raised
[ ] NEXT_PUBLIC_SUPABASE_URL and _ANON_KEY set in Vercel, spelled right
[ ] redeployed AFTER the last environment variable change
[ ] domain added, and aliased to a deployment rather than merely verified
[ ] the sign-in screen loads rather than /setup, from a machine that has
never visited the domain before
[ ] Supabase Site URL set to the deployed origin
[ ] Redirect URLs include the deployed origin and localhost
[ ] custom SMTP configured — the built-in sender does two an hour, which is
two people an hour, for an application you can only enter by email
[ ] NEXT_PUBLIC_SITE_URL set on Production, and redeployed since
[ ] signed in with a magic link on the deployed site, end to end
[ ] signed out, closed the browser, and signed back in — a second sign-in is
the one that proves the account persisted rather than the session
[ ] written one proposal on it that you would have written anyway
The last line is the only one that tells you anything.