feat: migrate auth + Kraal off D1 to Supabase identity.person#34
Draft
bryanfawcett wants to merge 6 commits intoclaude/finish-everythingfrom
Draft
feat: migrate auth + Kraal off D1 to Supabase identity.person#34bryanfawcett wants to merge 6 commits intoclaude/finish-everythingfrom
bryanfawcett wants to merge 6 commits intoclaude/finish-everythingfrom
Conversation
First slice of the D1 → Supabase migration. The auth flow no longer makes the /api/auth/sync round-trip — frontend talks directly to identity.person. ### Why Per direction "if we have Supabase we don't need D1". Supabase MCP audit confirmed identity.person already has a workos_user_id column, so the auth migration is straightforward: upsert by workos_user_id, fall back to email for legacy rows. RLS resolves auth.jwt()->>sub to person_id because PR-33 wired the WorkOS access token into the Supabase client. ### Bugs found while auditing the live DB - circles.post_reaction column is reaction_type, not reaction. Every Kraal reaction toggle was silently 400-ing in prod against the live schema. Fixed in togglePostReaction. - PersonRow type used display_name/given_name/family_name, but the live identity.person has name/givenname/familyname. Every Kraal author label was rendering "Member" because the SELECT returned nulls. Fixed in api.ts SELECT lists, types.ts PersonRow shape, and the authorLabel helper in kraal-detail-client. ### Frontend changes - src/lib/supabase/types.ts: PersonRow rebuilt against the actual DB schema (workos_user_id, name, givenname, familyname, alternatename, address jsonb, knowsabout array, role, etc.). New PersonAddress type for the address jsonb shape. - src/lib/supabase/api.ts: adds getPersonByWorkosId, getPersonByEmail, upsertPersonFromWorkos, updatePersonProfile. togglePostReaction switched to reaction_type. getEventHostInfo's person SELECT updated. Kraal post / member SELECTs updated. - src/components/auth/auth-context.tsx: drops the fetch(/api/auth/sync) call; uses upsertPersonFromWorkos directly. personRowToNhimbeUser helper maps the Supabase row to the existing NhimbeUser surface so consumers don't change. - src/lib/api.ts: updateProfile is now a thin wrapper around updatePersonProfile (Supabase-direct). Signature changed from (sessionJwt, fields) to (personId, fields). - 4 callers of updateProfile updated: name-prompt, location-prompt, interests-prompt, /profile/edit. They now pass user.personId from auth-context instead of fetching the access token. - src/app/kraal/[id]/kraal-detail-client.tsx: authorLabel + optimistic post backfill use the new PersonRow shape. ### Frontend tests - auth-context.test.tsx: replaces global.fetch mocks with vi.mock of @/lib/supabase/api → upsertPersonFromWorkos. 160 / 160 pass. ### Worker Untouched. /api/auth/sync, /me, PATCH /profile remain so old client deploys keep working until this PR has shipped, after which they can be deleted in a follow-up. wrangler.toml DB binding removal also deferred to a follow-up. ### Verified npm run lint # 0 errors npm run test:run # 160 / 160 cd worker && npx vitest run # 283 / 283 cd worker && npx tsc --noEmit # clean npm run build # clean ### Deferred to follow-up PRs - Delete worker /api/auth/sync, /me, /profile routes - Migrate events/registrations/reviews/waitlists/checkin worker writes to Supabase events.* schemas - Migrate orphans (audit_logs, kiosk_pairings, payments, event_series) into new Supabase schemas - Drop the DB binding from wrangler.toml + delete worker D1 schema/migrations https://claude.ai/code/session_01Dp6YFZCHz1HjL9svPWmso2
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
mukoko-nhimbe-api | bf24dc3 | Commit Preview URL Branch Preview URL |
May 08 2026, 01:15 PM |
Per direction "don't use the worker for auth — reduce the lift the worker
is doing". The frontend stopped calling /api/auth/sync, /me, /profile
in the previous commit, so these routes are dead code.
Removes:
- worker/src/routes/auth.ts (sync, me, PATCH profile route handlers)
- worker/src/__tests__/auth-profile.test.ts (4 tests, all on the
deleted PATCH /api/auth/profile route)
- The auth import + app.route("/api/auth", auth) + rateLimit on
/api/auth/* in worker/src/index.ts
Worker keeps:
- getAuthenticatedUser (still validates WorkOS JWT for write-protected
routes that haven't been migrated yet — events, registrations, etc.)
- getAdminUser (still reads users.role from D1 for admin/* routes; the
D1 → Supabase migration of admin role checks is a follow-up)
After this commit:
- 9 test files / 279 worker tests still pass
- worker tsc clean
- frontend build clean
Per direction "no merge until Supabase is fully adopted". The worker
admin role check no longer reads D1's `users` table; it reads
`identity.person.role` in nyuchi_platform_db via PostgREST with the
service-role key.
Changes:
- worker/src/db/supabase.ts (new): tiny PostgREST fetch helper. No
@supabase/supabase-js dep — keeps the worker bundle lean and avoids
the SDK's auth/realtime overhead. Sets Accept-Profile / Content-Profile
headers for non-public schemas.
- worker/src/middleware/auth.ts: getAdminUser swapped from
`env.DB.prepare("SELECT … FROM users WHERE stytch_user_id = ?")` to
`supabaseFetch({ schema: "identity", path: "person",
query: "workos_user_id=eq.<sub>&select=id,email,name,role&limit=1" })`.
Added mapPlatformRole() boundary mapper:
platform_admin → super_admin
admin → admin
moderator → moderator
* → user
…so callers keep using hasPermission() against the worker's UserRole
hierarchy without caring that platform_db uses different role names.
- worker/src/types.ts: SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY added to
Env (URL is a public var, key is a secret).
- worker/wrangler.toml: SUPABASE_URL set on dev/staging/production envs
pointing at tdcpuzqyoodrdsxldgsh.supabase.co.
- worker/.dev.vars.example: documents SUPABASE_SERVICE_ROLE_KEY secret.
- worker/src/__tests__/mocks.ts: createMockEnv now includes test values
for both Supabase env vars.
- worker/src/__tests__/routes-coverage.test.ts: setupAdminAuth() now
stubs globalThis.fetch with vi.stubGlobal so the identity.person REST
lookup returns the admin row. afterEach() cleans the stub. The 5 admin
tests had their D1 prepare callIndex switch decremented by one (the
previous "Call 1: getAdminUser role lookup" branch is gone).
After this commit:
- 279/279 worker tests pass
- worker tsc clean
- 160/160 frontend tests pass
- frontend build clean
- worker no longer touches D1 for the admin role check
Next slice (later commit): apply the device.* + engagement.referral
migrations to nyuchi_platform_db (SQL drafted in PR #34 description,
awaiting approval).
Per direction "nyuchi_pay_db is API-only" — the worker no longer queries
the pay DB directly via PostgREST. It calls a new payments-api Edge
Function deployed on naqcjejomizwthgdzomp.supabase.co. The pay-db's
service-role key never leaves the Edge Function runtime.
Edge function (deployed via Supabase MCP, version 1, status ACTIVE):
- Auth: timing-safe-compares Authorization: Bearer <PAY_API_KEY> AND
x-supabase-pay-publishable-key header. Both must match. Lets us roll
caller identities independently of the shared secret.
- Routes:
- GET / — public health probe (no auth)
- GET /v1/health — auth-required, samples config.currency to
confirm DB reach with service-role
- Follow-up commits add: POST /v1/intents, GET /v1/intents/:id,
POST /v1/webhooks/paynow (HMAC-SHA512 verified before any DB write).
Worker side:
- worker/src/payments/pay_api.ts (new): payApiFetch helper that sends
the two auth headers and propagates errors. Replaces the future need
for direct PostgREST against pay-db.
- worker/src/types.ts: SUPABASE_PAY_URL, SUPABASE_PAY_PUBLISHABLE_KEY,
PAY_API_KEY added to Env.
- worker/wrangler.toml: SUPABASE_PAY_URL + SUPABASE_PAY_PUBLISHABLE_KEY
set on dev/staging/production. PAY_API_KEY is a wrangler secret.
- worker/.dev.vars.example: documents PAY_API_KEY (must match the
value set as a Supabase secret on the pay-db project).
- worker/src/__tests__/pay-api.test.ts (new, 3 tests): config error
path, header propagation, error propagation.
- worker/src/__tests__/mocks.ts: createMockEnv populates the new vars.
Platform_db migrations applied (all via Supabase MCP, with user approval):
- device.* schema: device.device, device.pairing, device.session +
indexes + RLS. Knowledge-graph-aligned via context_schema +
context_entity_type + context_entity_id. Used by nhimbe (kiosk +
signage), mukoko bushtrade (POS), mukoko lingo (classroom display),
mukoko news (newsroom screens). Replaces D1 kiosk_pairings.
- engagement.referral: cross-schema referrals with
target_schema + target_entity_type + target_entity_id. Replaces D1
referrals + user_referral_codes.
- RLS enabled on the 4 sync.* infrastructure tables.
Pay_db migrations applied:
- RLS enabled on the 8 previously-open tables (5 gateway.usage_log
partitions + system.verification_tier + system.verification_subject_type
+ system.trust_score_weights).
After this commit:
- 10 test files / 282 worker tests pass (3 new pay-api tests)
- worker tsc clean
- 160 frontend tests pass
- frontend build clean
- Pay-db RLS: defence-in-depth, anon key can no longer reach
internal tables via PostgREST
- Edge function reachable; needs PAY_API_KEY + PAY_PUBLISHABLE_KEY +
SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY set as Supabase secrets on
the pay-db project before /v1/health works in prod.
Per direction "WorkOS access token only" + "one function per concern".
The previous payments-api scaffolding used dual-credential auth
(PAY_API_KEY + publishable key), which is now wrong. Replaced with:
Edge functions on nyuchi_pay_db (deployed via Supabase MCP):
- payments-api (v2): now returns 410 Gone with the new endpoint URLs.
No MCP delete tool — the function will be removed via the Supabase
Dashboard once all callers have moved.
- payments-intents (v1): user-context payment intents. verify_jwt:false,
so the function does its own auth — port of worker/src/auth/workos.ts
to Deno (RS256 JWT against WorkOS JWKS, 1h cache, full claims check
for iss / aud / exp / nbf). Routes:
GET / — public health probe
GET /v1/health — auth-required, samples config.currency to
confirm WorkOS validation + DB reach
POST /v1/intents — 501 stub (follow-up commit)
GET /v1/intents/:id — 501 stub (follow-up commit)
- payments-webhooks-paynow: deferred to the commit that moves the real
Paynow handler. Webhooks use HMAC-SHA512 (no user context).
Worker side:
- worker/src/payments/pay_api.ts: now takes `accessToken` (the user's
WorkOS JWT from the incoming request) and forwards it as Authorization
Bearer. Drops PAY_API_KEY and the publishable-key header entirely.
Points at /functions/v1/payments-intents.
- worker/src/types.ts: SUPABASE_PAY_PUBLISHABLE_KEY and PAY_API_KEY
removed from Env. Only SUPABASE_PAY_URL remains.
- worker/wrangler.toml: dropped publishable key from dev/staging/prod.
- worker/.dev.vars.example: PAY_API_KEY block replaced with a note
pointing at the pay-db Supabase secrets that need to be set
(WORKOS_CLIENT_ID, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY).
- worker/src/__tests__/mocks.ts: only SUPABASE_PAY_URL remains in the
mock env.
- worker/src/__tests__/pay-api.test.ts: rewritten to verify the WorkOS
token forwarding path; explicitly checks no machine-to-machine secret
is sent.
Required pay-db Supabase secrets (Dashboard → Edge Functions → Secrets):
WORKOS_CLIENT_ID — same value as the worker's
SUPABASE_URL — https://naqcjejomizwthgdzomp.supabase.co
SUPABASE_SERVICE_ROLE_KEY — pay-db service-role key
After this commit:
- 10 test files / 282 worker tests pass
- worker tsc clean
- 160 frontend tests pass
- frontend build clean
…db edge fns
api.mukoko.com (FastAPI on fly.io) is the public façade that owns API-key
management and brokers access to private back-end stores (pay-db,
platform-db). Pay-db has no public edge functions — it's reached only by
api.mukoko.com via service role. The worker is one of many consumers of
api.mukoko.com (third-party builders are others).
Pay-db side
- payments-intents (v2) replaced with 410 Gone — pay-db has no public
edge functions. Delete via Supabase Dashboard once safe.
- payments-api (already 410 from previous commit) unchanged.
Worker side
- Renamed worker/src/payments/pay_api.ts → mukoko_api.ts. The client now
targets api.mukoko.com with dual-credential auth:
• X-Api-Key — always sent. Identifies the worker as a
machine-context caller.
• Authorization Bearer — only when userAccessToken is provided.
Forwarded WorkOS access token for user-context
calls; api.mukoko.com validates against JWKS.
- worker/src/types.ts: SUPABASE_PAY_URL removed; MUKOKO_API_URL +
MUKOKO_API_KEY added.
- worker/wrangler.toml: dropped SUPABASE_PAY_URL across dev / staging /
prod, added MUKOKO_API_URL = https://api.mukoko.com.
- worker/.dev.vars.example: MUKOKO_API_KEY placeholder, with note pointing
at api.mukoko.com's API-key management for value generation.
- worker/src/__tests__/mocks.ts: env mock updated.
- worker/src/__tests__/mukoko-api.test.ts: rewritten — covers config errors,
X-Api-Key always present, Authorization Bearer only with userAccessToken,
body/method forwarding, non-2xx propagation.
Required new wrangler secret (per environment):
wrangler secret put MUKOKO_API_KEY # dev
wrangler secret put MUKOKO_API_KEY --env staging
wrangler secret put MUKOKO_API_KEY --env production
Verification
- 10 test files / 284 worker tests pass
- worker tsc clean
- 160 frontend tests pass
- frontend build clean
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Per direction "if we have Supabase we don't need D1" + "don't use the worker for auth — reduce the lift the worker is doing" + "build for the platform, not in isolation". First slices of a multi-stage D1 → Supabase migration. PRs in this stack are kept open until Supabase is fully adopted — nothing merges until D1 is gone.
Stacks on top of #33 (Supabase ↔ WorkOS access-token wiring + vitest
@/alias).Design philosophy:
nyuchi_platform_dbis a knowledge graphThe audit revealed the platform DB is structured as a typed graph, not a relational DB:
events,places,identity,circles,engagement,system, etc. — each is a node-type clustercontext_schema+context_entity_type+context_entity_id— generic typed cross-schema references. Examples already in the wild:identity.person_role.context_*(a role within an entity, circle, place, etc.)engagement.review.item_reviewed_schema+item_reviewed_id(review on any entity)places.place_relations(typed graph edges between places)events.check_in→ CheckInAction,events.rsvp_action→ RsvpAction,engagement.follow_action→ FollowAction, etc.Therefore: all new tables in this stack must use the same patterns — schema.org-aligned column names, knowledge-graph context edges, no nhimbe-specific schemas.
Corrected Supabase inventory (full audit)
I undercounted heavily before. The platform DB has 31 user schemas, not 2. Most of nhimbe's domain model already exists in Postgres:
events.*— full nhimbe events schema already existseventsevents.event(52 cols, schema.org Event, RRULE/series/check-in/waitlist/visibility/owner_type already on the row)registrationsevents.rsvp_action(RsvpAction)events.ticket+events.ticket_tier(Offer)attendance(QR)events.check_in(CheckInAction)waitlistsevents.waitlist_entry(WaitAction)event_reviewsengagement.review(cross-schema)event_seriesevents.eventalready hasrrule,series_parent_id,recurrence_end,series_occurrenceevents.organiser(Role wrapper,can_*permission columns)events.event_update(Message)events.commentevents.poll+events.poll_voteevents.programme_itemevents.save_actionidentity.*— full role/permission systemidentity.person(2 rows) withrole(text NOT NULL) on the row +active_rolesarray (denormalized)identity.role_type(40 rows):admin,moderator,platform_admin,circle_admin,circle_moderator,compliance_officer,creator,developer,employee,finance_officer,regulator,support,support_finance,user,data_researcher, …identity.role_permission(98),identity.platform_permission(43)identity.person_role— context-scoped role assignmentsidentity.entity(97 rows, schema.org Organization umbrella, hasworkos_org_id)entity.membership(2 rows) — universalperson ↔ entityjoin→ Replaces the D1
users.rolecolumn.getAdminUserreadsidentity.person.role.places.*— geography with real dataplaces.places_geo(93 rows: cities/towns/districts) ← nhimbe cities source, not "derived from published events"places.places(2079 rows, schema.org Place — venues)places.place_category(129),places.countries(54),places.provinces(73)engagement.*engagement.interest_category(40 rows) ← replaces D1categoriesengagement.review← replaces D1event_reviewsengagement.follow_action← replaces D1followssystem.*system.activity_logs+system.change_history← replaces D1audit_logssystem.theme(5 rows, mineral palette) ← replaces D1themessystem.notifications,system.feature_flags,system.unified_submissionsservice_bus.*(170 subs, 169 event types, 822 events logged)service_bus.events(append-only log),event_types,subscriptions,dead_letter_queue,event_processing,service_health← replaces CloudflareANALYTICS_QUEUE+EMAIL_QUEUE.nyuchi_pay_db— separate Supabase projectProduction-grade payment system (
config,ledgerw/ double-entry bookkeeping,provider,gateway,streamoutbox,audit).worker/src/payments/paynow.tsand the D1paymentstable both get deleted in a later slice.Other social schemas already present
circles.*(Kraal),campfire.*,pulse.*,shamwari.*(AI),ubuntu.*(badges/missions/leaderboards),wallet.*(mit_token, NFTs, swaps, balance, payment_intents).Genuinely missing — needs new platform-wide schemas
New schema:
device.*— generic device pairing/sessions for the whole platformUsed by nhimbe (kiosk + signage), mukoko bushtrade (POS), mukoko lingo (classroom display), mukoko news (newsroom screens) — anyone needing a paired display/terminal.
This deletes the D1
kiosk_pairingstable. Nhimbe kiosk/signage become consumers ofdevice.*withcontext_schema='events',context_entity_type='events.event'.New table:
engagement.referral— generic cross-schema referralsUsed by events (referrer→event), circles (referrer→circle), commerce (referrer→outlet), etc.
This deletes the D1
referralsanduser_referral_codestables.Analytics → emit as
service_bus.eventsD1
event_views,search_queriesbecome events on the bus. No new tables.RLS advisories the platform_db owners should fix
What this PR does (incremental commits — no merge until D1 is gone)
1. Frontend Supabase auth migration (commits a-c, landed)
src/lib/supabase/api.ts:getPersonByWorkosId,getPersonByEmail,upsertPersonFromWorkos,updatePersonProfileauth-context.tsx: removedfetch(/api/auth/sync)— now upsertsidentity.persondirectlyupdateProfile()rewritten to callupdatePersonProfile()againstidentity.person. Signature changed:(sessionJwt, fields)→(personId, fields). All 4 callers updated.2. Worker auth routes deleted (commit d, landed)
worker/src/routes/auth.tsremoved entirely (POST/sync, GET/me, PATCH/profile)3. Two prod Kraal bugs fixed
circles.post_reaction.reaction_type(was wronglyreaction)identity.person.name/givenname/familynamecolumns (was wronglydisplay_name/given_name/family_name) — every Kraal author label was rendering "Member" in prodComing next in this PR
getAdminUser→identity.person.role(no more D1usersread)worker/src/db/supabase.tsREST helper using SERVICE_ROLE_KEYdevice.*(3 tables) +engagement.referralComing in follow-up PRs
events.*directly from frontend (RLS-protected)engagement.review; cities →places.places_geo; categories →engagement.interest_categorysystem.theme; audit logs →system.activity_logsworker/src/routes/kiosk.ts) →device.*REST/edge functionnyuchi_pay_db(deleteworker/src/payments/paynow.ts)service_bus.events(delete Cloudflare Queue bindings)DBbinding fromwrangler.toml+ delete D1 schema/migrations + deleteworker/src/db/Worker eventually keeps only: AI inference (Workers AI), R2 image uploads, Vectorize search.
Verified so far
https://claude.ai/code/session_01Dp6YFZCHz1HjL9svPWmso2