Skip to content

feat: migrate auth + Kraal off D1 to Supabase identity.person#34

Draft
bryanfawcett wants to merge 6 commits intoclaude/finish-everythingfrom
claude/d1-to-supabase-migration
Draft

feat: migrate auth + Kraal off D1 to Supabase identity.person#34
bryanfawcett wants to merge 6 commits intoclaude/finish-everythingfrom
claude/d1-to-supabase-migration

Conversation

@bryanfawcett
Copy link
Copy Markdown
Contributor

@bryanfawcett bryanfawcett commented May 8, 2026

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_db is a knowledge graph

The audit revealed the platform DB is structured as a typed graph, not a relational DB:

  • Domains as schemasevents, places, identity, circles, engagement, system, etc. — each is a node-type cluster
  • Edges as context_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)
  • Schema.org alignment is the contract — almost every table maps to a schema.org @type via column comments. 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 exists

nhimbe D1 table Already in Supabase as
events events.event (52 cols, schema.org Event, RRULE/series/check-in/waitlist/visibility/owner_type already on the row)
registrations events.rsvp_action (RsvpAction)
ticketing events.ticket + events.ticket_tier (Offer)
attendance (QR) events.check_in (CheckInAction)
waitlists events.waitlist_entry (WaitAction)
event_reviews engagement.review (cross-schema)
event_series NOT a separate tableevents.event already has rrule, series_parent_id, recurrence_end, series_occurrence
host roles events.organiser (Role wrapper, can_* permission columns)
organiser→attendee events.event_update (Message)
event comments events.comment
event polls events.poll + events.poll_vote
programme/agenda events.programme_item
save event events.save_action

identity.* — full role/permission system

  • identity.person (2 rows) with role (text NOT NULL) on the row + active_roles array (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 assignments
  • identity.entity (97 rows, schema.org Organization umbrella, has workos_org_id)
  • entity.membership (2 rows) — universal person ↔ entity join

→ Replaces the D1 users.role column. getAdminUser reads identity.person.role.

places.* — geography with real data

  • places.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 D1 categories
  • engagement.review ← replaces D1 event_reviews
  • engagement.follow_action ← replaces D1 follows

system.*

  • system.activity_logs + system.change_history ← replaces D1 audit_logs
  • system.theme (5 rows, mineral palette) ← replaces D1 themes
  • system.notifications, system.feature_flags, system.unified_submissions

service_bus.* (170 subs, 169 event types, 822 events logged)

service_bus.events (append-only log), event_types, subscriptions, dead_letter_queue, event_processing, service_healthreplaces Cloudflare ANALYTICS_QUEUE + EMAIL_QUEUE.

nyuchi_pay_db — separate Supabase project

Production-grade payment system (config, ledger w/ double-entry bookkeeping, provider, gateway, stream outbox, audit). worker/src/payments/paynow.ts and the D1 payments table 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 platform

Used by nhimbe (kiosk + signage), mukoko bushtrade (POS), mukoko lingo (classroom display), mukoko news (newsroom screens) — anyone needing a paired display/terminal.

CREATE SCHEMA device;

-- Knowledge graph node — a registered device
-- schema.org: Product (additionalType = mukoko:Device, mukoko:Kiosk, mukoko:POS, etc.)
CREATE TABLE device.device (
  id uuid PRIMARY KEY DEFAULT system.uuidv7(),
  name text NOT NULL,                    -- schema:name
  device_type text NOT NULL,             -- schema:additionalType: kiosk | signage | pos | classroom_display | …
  model text,                            -- schema:model
  manufacturer text,                     -- schema:manufacturer
  serial_number text,                    -- schema:serialNumber

  -- Knowledge graph edge to whatever the device is bound to
  context_schema text,                   -- 'events' | 'commerce' | 'lingo' | 'news' | …
  context_entity_type text,              -- 'events.event' | 'commerce.outlet' | …
  context_entity_id uuid,

  owner_person_id uuid REFERENCES identity.person(id),
  owner_entity_id uuid REFERENCES identity.entity(id),

  status text NOT NULL DEFAULT 'active', -- active | idle | revoked | retired
  registered_at timestamptz DEFAULT now(),
  last_seen_at timestamptz,
  metadata jsonb DEFAULT '{}'::jsonb,
  created_at timestamptz DEFAULT now(),
  updated_at timestamptz DEFAULT now()
);

-- Pairing flow — schema.org RegisterAction
-- agent = the operator generating the code, instrument = the code,
-- result = the device that gets registered
CREATE TABLE device.pairing (
  id uuid PRIMARY KEY DEFAULT system.uuidv7(),
  code text NOT NULL UNIQUE,             -- schema:identifier (6-char short)

  -- Where the device is being paired (graph edge)
  context_schema text NOT NULL,          -- 'events'
  context_entity_type text NOT NULL,     -- 'events.event'
  context_entity_id uuid NOT NULL,
  intended_device_type text NOT NULL,    -- kiosk | signage | pos | …

  initiated_by_person_id uuid NOT NULL REFERENCES identity.person(id),

  claimed_at timestamptz,
  claimed_by_device_id uuid REFERENCES device.device(id),

  status text NOT NULL DEFAULT 'pending', -- pending | claimed | expired | revoked
  expires_at timestamptz NOT NULL,
  created_at timestamptz DEFAULT now()
);
CREATE INDEX device_pairing_code_pending_idx ON device.pairing(code) WHERE status = 'pending';
CREATE INDEX device_pairing_context_idx ON device.pairing(context_schema, context_entity_id);

-- Active sessions (post-pairing). Token hash, not raw token.
CREATE TABLE device.session (
  id uuid PRIMARY KEY DEFAULT system.uuidv7(),
  device_id uuid NOT NULL REFERENCES device.device(id) ON DELETE CASCADE,
  token_hash text NOT NULL UNIQUE,
  started_at timestamptz NOT NULL DEFAULT now(),
  expires_at timestamptz NOT NULL,
  last_activity_at timestamptz NOT NULL DEFAULT now(),
  revoked_at timestamptz,
  ip_address inet,
  user_agent text,
  created_at timestamptz DEFAULT now()
);
CREATE INDEX device_session_device_active_idx ON device.session(device_id) WHERE revoked_at IS NULL;

This deletes the D1 kiosk_pairings table. Nhimbe kiosk/signage become consumers of device.* with context_schema='events', context_entity_type='events.event'.

New table: engagement.referral — generic cross-schema referrals

Used by events (referrer→event), circles (referrer→circle), commerce (referrer→outlet), etc.

CREATE TABLE engagement.referral (
  id uuid PRIMARY KEY DEFAULT system.uuidv7(),

  referrer_person_id uuid NOT NULL REFERENCES identity.person(id),
  referred_person_id uuid REFERENCES identity.person(id),  -- null until conversion

  code text NOT NULL UNIQUE,             -- schema:identifier
  share_url text,                        -- schema:url

  -- Knowledge graph edge — what's being referred to
  target_schema text NOT NULL,           -- 'events' | 'circles' | 'commerce' | …
  target_entity_type text NOT NULL,      -- 'events.event' | 'circles.circle' | …
  target_entity_id uuid NOT NULL,

  status text NOT NULL DEFAULT 'pending', -- pending | converted | expired
  converted_at timestamptz,

  created_at timestamptz DEFAULT now()
);
CREATE INDEX referral_code_idx ON engagement.referral(code);
CREATE INDEX referral_referrer_idx ON engagement.referral(referrer_person_id);
CREATE INDEX referral_target_idx ON engagement.referral(target_schema, target_entity_id);

This deletes the D1 referrals and user_referral_codes tables.

Analytics → emit as service_bus.events

D1 event_views, search_queries become events on the bus. No new tables.

RLS advisories the platform_db owners should fix

-- nyuchi_platform_db: 4 sync.* tables have RLS disabled
ALTER TABLE sync.reference_data_version ENABLE ROW LEVEL SECURITY;
ALTER TABLE sync.outbound_log           ENABLE ROW LEVEL SECURITY;
ALTER TABLE sync.satellite_config       ENABLE ROW LEVEL SECURITY;
ALTER TABLE sync.inbound_log            ENABLE ROW LEVEL SECURITY;
-- (Followed by service-role-only policies — sync tables are infra.)
-- nyuchi_pay_db: 8 tables have RLS disabled
ALTER TABLE gateway.usage_log_2026_04        ENABLE ROW LEVEL SECURITY;
ALTER TABLE gateway.usage_log_2026_05        ENABLE ROW LEVEL SECURITY;
ALTER TABLE gateway.usage_log_2026_06        ENABLE ROW LEVEL SECURITY;
ALTER TABLE gateway.usage_log_2026_07        ENABLE ROW LEVEL SECURITY;
ALTER TABLE gateway.usage_log_2026_08        ENABLE ROW LEVEL SECURITY;
ALTER TABLE system.verification_tier         ENABLE ROW LEVEL SECURITY;
ALTER TABLE system.verification_subject_type ENABLE ROW LEVEL SECURITY;
ALTER TABLE system.trust_score_weights       ENABLE ROW LEVEL SECURITY;

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, updatePersonProfile
  • auth-context.tsx: removed fetch(/api/auth/sync) — now upserts identity.person directly
  • updateProfile() rewritten to call updatePersonProfile() against identity.person. Signature changed: (sessionJwt, fields)(personId, fields). All 4 callers updated.

2. Worker auth routes deleted (commit d, landed)

  • worker/src/routes/auth.ts removed entirely (POST /sync, GET /me, PATCH /profile)

3. Two prod Kraal bugs fixed

  • circles.post_reaction.reaction_type (was wrongly reaction)
  • identity.person.name/givenname/familyname columns (was wrongly display_name/given_name/family_name) — every Kraal author label was rendering "Member" in prod

Coming next in this PR

  • getAdminUseridentity.person.role (no more D1 users read)
  • worker/src/db/supabase.ts REST helper using SERVICE_ROLE_KEY
  • Apply migrations: device.* (3 tables) + engagement.referral

Coming in follow-up PRs

  • Events writes → events.* directly from frontend (RLS-protected)
  • Reviews → engagement.review; cities → places.places_geo; categories → engagement.interest_category
  • Themes → system.theme; audit logs → system.activity_logs
  • Kiosk routes (worker/src/routes/kiosk.ts) → device.* REST/edge function
  • Payments → nyuchi_pay_db (delete worker/src/payments/paynow.ts)
  • Queues → service_bus.events (delete Cloudflare Queue bindings)
  • Drop DB binding from wrangler.toml + delete D1 schema/migrations + delete worker/src/db/

Worker eventually keeps only: AI inference (Workers AI), R2 image uploads, Vectorize search.

Verified so far

npm run lint            # 0 errors
npm run test:run        # 160 / 160
cd worker && npx vitest run   # 279 / 279
cd worker && npx tsc --noEmit # clean
npm run build           # clean

https://claude.ai/code/session_01Dp6YFZCHz1HjL9svPWmso2

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
@vercel
Copy link
Copy Markdown

vercel Bot commented May 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nhimbe Ready Ready Preview, Comment May 8, 2026 1:16pm

Request Review

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 8, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants