TradeForce migration docs

#TradeForce — system architecture

#Purpose

TradeForce is a shift marketplace where companies post construction jobs, contractors claim and work them, both sides track time and approvals, and platform admins verify people and rates. This document describes the running target application: a React SPA under app/ talking to Supabase Auth, Postgres with row-level security, and Edge Functions for secrets and privileged orchestration.

The source product was a Bubble app. The migration keeps product journeys and data meaning, but does not copy Bubble’s browser-as-database trust model. Assessment finding LOG-CLIENT-WRITES (about 97% of database writes lived in client workflows) is an architecture law here: every persistent write lands behind authenticated server authority (RLS policies and/or SECURITY DEFINER RPCs / Edge Functions under app/supabase). Money-adjacent mutations (rates, paid amounts, approval flags) additionally follow MON-CLIENT-WRITES: transactional server authority, never trust the browser as the writer of record.

Programme boundary MON-MARKETPLACE fences Stripe Connect, KYC, live balances, and payouts as separate scope until answered. Auth cutover ACC-AUTH-SURFACE chooses how existing Bubble identities move onto Supabase Auth.

#Runtime context

Target stack: React + Vite + TypeScript SPA (app/) and Supabase backend (app/supabase/ — migrations, RLS, Edge Functions). The browser holds only the anon key and the user JWT. Service-role keys and third-party secrets stay on the server.

flowchart TB subgraph clients [Browsers] SPA["React SPA<br/>app/src"] end subgraph supabase [Supabase] Auth["Auth<br/>email/password session"] DB["Postgres + RLS<br/>migrations"] Edge["Edge Functions<br/>secrets and privileged workflows"] end subgraph external [External systems] PM["Postmark<br/>transactional email"] CSC["Country State City API<br/>location lookup"] StripeDash["Stripe Dashboard<br/>read-only link when account id present"] end SPA -->|"JWT + anon key"| Auth SPA -->|"PostgREST / RPC"| DB SPA -->|"invoke with user JWT"| Edge Auth --> DB Edge --> DB Edge --> PM Edge --> CSC SPA -->|"open dashboard URL when fields present"| StripeDash

Delivery is by vertical slice: each product journey ships UI, client calls, domain rules, and any backend surface together. There is no separate “build the API first” phase.

#SPA modules

Capability modules map to authenticated host routes inside a shared cabinet workspace (header + role sidebar + main outlet). Auth and onboarding sit outside that chrome.

Module Role in runtime Primary host surfaces
Sign-in and account access Public auth index and password reset; creates session and profiles routing fields /, password-reset route
Workspace chrome Header, contractor/company/admin side navigation, role-gated menus Mounted on authenticated hosts below
Role onboarding Profile, skills, documents, location before marketplace entry /onboarding
Company shift dashboard Post/manage shifts, applicants, job detail, end-job, company payment approvals /company/shifts (+ tab contract)
Contractor shifts and profile Browse/claim shifts, MyShifts, contractor profile docs /contractor/shifts
Time tracking and approvals Live tracking, reports, company review; queue redesign for recursive approve /contractor/time-tracking (+ company review surfaces)
Contractor payments Schedules and payment history; Stripe fields read-only pending marketplace programme /contractor/payments
Admin verification and ops Verify companies/contractors, rates, shifts, invoices/schedules /admin
Messaging Threads, send, new-message email /chat

Shared job UI (detailed job view, end-job popup, shift row actions) has a single implement owner in the company-shifts module. Contractor and time-tracking modules reuse those components; they do not fork alternate implementations.

The Bubble stub 404 page is out of product scope.

#Auth and session

Identity target is Supabase Auth (email/password), with a profiles row created on signup for role, active, onboarding-complete, and approved flags used by post-login routing and gates.

Public flows:

  1. Sign in → session JWT → redirect by role and profile flags (onboarding vs role home).
  2. Sign up with contractor/company role → profile defaults (inactive until activation path) → verify / onboarding.
  3. Request reset email → confirm new password on the reset surface → return to sign-in.

Session client lives under app/src/integrations/supabase with the anon key only. Auth errors map once at the boundary to stable domain errors shown in the UI.

ACC-AUTH-SURFACE is still open: the export uses Bubble native login/signup/reset, which do not transfer sessions or password material. An ADR must record (a) credential cutover for existing users (forced reset, dual-run, or equivalent — not a silent password migration) and (b) whether signup activation preserves the active=yes deep-link semantics or replaces them with Supabase confirm-email. Until that ADR is accepted, auth foundation work remains blocked on those decisions (see HQ-1).

Unauthenticated hits to authenticated host routes redirect to the auth index. Chrome is not shown without a session.

#Data access

Schema authority is docs/DATA-MODEL.md (idiomatic Postgres names) realized as migrations under app/supabase/migrations/. The SPA uses the user JWT against PostgREST and RPCs. RLS is the default read/write gate; the SPA must not ship a service-role key or reimplement authorization in the client.

#Server authority (LOG-CLIENT-WRITES / MON-CLIENT-WRITES)

Class of mutation Authority rule
Ordinary persistent creates/updates/deletes (shifts, profiles lists, chat, documents, admin edits) RLS allowing the actor, or an authenticated RPC/Edge path with explicit authz — never “browser wrote the row unchecked”
Money-adjacent fields (rates, paid amounts, send-paid / approve-company, computed payment totals) Transactional server RPC (or Edge) that validates amounts and commits; SPA only requests the change
Secrets and vendor APIs (Postmark, Country State City, future Connect secrets) Edge Functions only
Background tracking orchestration (create tracking lists, approve fan-out, delay helper) Server jobs / Edge / queue — not a line-by-line port of self-scheduling client workflows

Source backend events that ran with privacy rules disabled (create_list_tracking, send_tracking_approve) have no automatic RLS equivalent. Their elevated behaviour must be expressed as tightly scoped privileged server paths or redesigned under ordinary RLS (see HQ-3).

#Core entity families (target-oriented)

Family Purpose
Profiles (← source user) Role, activation, onboarding, approval, rate fields, Stripe account display fields
Shifts / jobs Posted marketplace work, applicants, stop-hiring / close transitions
Time entries Hours, breaks, company approval, paid/send flags
Documents / skill sets Onboarding and profile credentials
Chat / messages / notifications Messaging and in-app alerts
Custom rates Admin and company skill-rate history

Detailed columns and naming maps belong in the data-model document, not here.

#External systems

System Used for Target placement
Postmark Canceled shifts, new message, new time-tracking report, upcoming payment cycle (and related templates) Edge Function with API secret; SPA never holds the token
Country State City API States/cities during onboarding location Edge proxy; SPA calls the function with user JWT
Stripe Connected-account client id / public key present in export; user fields for account id and display name Read-only display and optional dashboard link in-app until MON-MARKETPLACE is decided. No Connect onboarding, KYC, live balance fetch, or payouts in core migration scope
Bubble platform email (SendGrid) The auth mail nobody else sends: the custom “Verify” signup email (bTHqT1) and the native password-reset mail (bTHqz1) run through Bubble's built-in sender — app facts use_sendgrid: true, sendgrid_verified: false on domain tradeforce.dev. It is a separate channel from the Postmark templates above. Chosen per ADR-002: either Supabase Auth's own templates or an Edge send. No Bubble sender survives cutover, and the unverified SendGrid domain does not carry over — the target needs its own verified sender.
Browser plugins (upload, select, icons, etc.) UI affordances in source Replaced with SPA libraries; not separate runtime backends

#Failure modes

Failure User-visible behaviour Server behaviour
Auth credentials invalid / session expired Inline auth error; redirect to sign-in when session required No data mutation
RLS / RPC denies write Clear domain error on the control that tried to save Reject; no partial money commit
Postmark or location API unavailable Message send / location lookup shows unavailable-style error; core CRUD still possible where independent Edge returns typed unavailable; timeouts required
Tracking approve / delay job fails Approval state stays pending; operator can retry; no silent success Queue/job records failure; no unbounded recursive self-schedule
Stripe Connect not in programme “Connect” control disabled or interim message; existing account id may still open dashboard No Connect plugin writes; no balance API with Stripe-Account header
Missing ACC-AUTH-SURFACE ADR Auth implement blocked No invented silent password migration or third activation path

#Traceability

Product concept Target (route / component / table) Bubble evidence
Auth index and reset /, reset-password page; Supabase Auth; profiles bTGYf, AAL
Workspace chrome Header, SideBar, SideMenuCompany; cabinet hosts bTHOe, bTHMl, bTMyh2
Onboarding /onboarding; CSC edge bTHDn0, external_ns:bTKtr
Company shifts /company/shifts; shifts; shared job/end-job/RG action bTITd, bTIgC, bTHkx, bTHTn, bTMBT
Contractor shifts /contractor/shifts bTHMg
Time tracking /contractor/time-tracking; time entry RPCs/jobs bTJFi0, bTLsx, bTMBg, bTKPV
Contractor payments /contractor/payments; read models; Stripe fields bTIzV0, stripe_account_id_text, stripe_name_text
Admin /admin bTHun0
Chat /chat; Postmark new-message bTJQD0, bTJQB0, external_ns:bTNMZ
Client write → server authority RLS/RPC/Edge under app/supabase Module-map LOG-CLIENT-WRITES / MON-CLIENT-WRITES; assessment write census
Marketplace programme fence Payments Stripe affordance only Module-map MON-MARKETPLACE; public_key:stripe_client_id_test
Auth provider ADR docs/adr/ADR-002-acc-auth-surface.md (status open — to accept) Module-map ACC-AUTH-SURFACE

#IR evidence

#Module evidence roots and pages

Module Inventory roots Page / definition pointers
MOD-AUTH bTGYf, AAL /pages/bTGbC (index), /pages/AAW (reset_pw)
MOD-SHELL bTHOe, bTHMl, bTMyh2 /element_definitions/bTHOf, /element_definitions/bTHMm, /element_definitions/bTNEP2
MOD-ONBOARDING bTHDn0 /pages/bTHDr0
MOD-COMPANY-SHIFTS bTITd, bTIgC, bTHkx, bTHTn, bTMBT /pages/bTIbd; reusables /element_definitions/bTIgD, bTHky, bTHTo; api /api/bTMBU (shift)
MOD-CONTRACTOR-SHIFTS bTHMg /pages/bTHMh
MOD-TIME-TRACKING bTJFi0, bTLsx, bTMBg, bTKPV /pages/bTJIF0; api /api/bTLsy (create_list_tracking), /api/bTMBh (send_tracking_approve), /api/bTKPZ (15_minuts_delay)
MOD-PAYMENTS bTIzV0 /pages/bTIzW0
MOD-ADMIN bTHun0 /pages/bTHuo0
MOD-CHAT bTJQD0, bTJQB0 /pages/bTJSr0; /element_definitions/bTJQC0

Supporting slices (prepared bundle): ir/slices/bTGbC____root.json, AAW____root.json, bTHDr0____root.json, bTHMh*, bTIbd*, bTJIF0____root.json, bTIzW0____root.json, bTHuo0*, bTJSr0____root.json, edef_* for shell/reusables, styles____part_all.json.

Path-key aliases: inventory id → prefer inventory pointer (e.g. page id bTGYf/pages/bTGbC). API event ids resolve to wf_name in inventory meta (create_list_tracking, send_tracking_approve, 15_minuts_delay, shift).

#Backend API events

Inventory id wf_name Notes for target
bTLsx create_list_tracking ignore_privacy_rules: true — elevated path (ACC-IGNORE-PRIVACY)
bTMBg send_tracking_approve Self-scheduling in source (LOG-RECURSIVE); rewrite as queue/cursor job; also ignore-privacy
bTKPV 15_minuts_delay Delay helper for tracking flows
bTMBT shift Company post/shift backend event

#Privacy and authorization

#External HTTP namespaces

#Secrets to re-provision (values are redacted in the sanitized export)

The export carries 14 secret_ref keys. None transfers; each is either re-issued in Supabase/Edge config or retired with the surface it served. Named here because nothing else in the docs claims them:

Secret ref Serves Target
apiconnector2 (10 stored values) API Connector auth — the Postmark server token and the Country State City key, including the three private shared headers on external_ns:bTNMZ and the private call headers on external_call:bTKtr:bTKts / external_call:bTKtr:bTKvG Edge Function env; never reaches the browser
stripe_private_key_test Stripe secret key (test) Out of core scope under MON-MARKETPLACE; re-issue only when that programme starts
1658072861502x…_appsecret / _appsecret_test / _AAu / _AAu_test, 1491714407143x…_BQI_test, 1491714407143x…_shared_headers_BEl_test, 1623670386921x…_appsecret_test Plugin-scoped secrets for installed Bubble plugins Retire with the plugins they serve (plugins are replaced by SPA libraries)
username, password, general_keys, cRb Legacy app-level keys with one stored value each; no surface in this export claims them Retire unless a caller is found before cutover
use_algolia_integration Zero stored values — Algolia was never wired Nothing to migrate

#Open risks (module-map)

Id Bound module Architecture implication
LOG-CLIENT-WRITES Company shifts (+ admin, payments, time-tracking) All persistent writes → server/RLS/Edge
MON-CLIENT-WRITES Company shifts (+ money UIs) Money fields → transactional server RPC
MON-MARKETPLACE Payments Separate Connect/payouts programme (HQ-2)
ACC-AUTH-SURFACE Auth Provider + cutover + activation ADR (HQ-1)
LOG-RECURSIVE Time tracking Queue redesign for send_tracking_approve

#Needs human clarification

Id Question Why unclear (plain + evidence) Impact
HQ-1 How should existing Bubble users authenticate on Supabase, and which signup activation path do we ship? Plain: Sessions and password hashes do not transfer from Bubble native auth. Decompose assumes Supabase Auth but still requires an accepted ADR for cutover (forced reset vs dual-run) and for activation (preserve active=yes deep-link vs Supabase confirm-email). Evidence: chased catalog pages bTGYf / AAL and assess seed ACC-AUTH-SURFACE; inventory shows native LogIn/SignUp/Reset workflows — no portable credential material. blocks_implement
HQ-2 Is Stripe Connect / KYC / balances / payouts a separate programme after core cutover, or in the same migration scope? Plain: Export shows connected-account configuration and user Stripe id/name fields, but not a complete product decision on live Connect onboarding. Core architecture fences Connect as out of scope until answered; payments may still show read-only fields. Evidence: stripe_account_id_text, stripe_name_text, public_key:stripe_client_id_test, page bTIzV0; module-map MON-MARKETPLACE. blocks_implement
HQ-3 Should ignore-privacy tracking API events become privileged service-role endpoints or be redesigned under ordinary RLS? Plain: Two backend workflows run with privacy rules disabled; Postgres RLS has no automatic “ignore rules” switch. Time-tracking queue redesign must pick elevated RPC/Edge vs rewrite under normal policies. Evidence: inventory meta on bTLsx / bTMBg (ignore_privacy_rules: true, wf_name create_list_tracking / send_tracking_approve); assess seed ACC-IGNORE-PRIVACY. blocks_implement

Generated from the repository at 2026-09-22 15:55 UTC · source of truth is docs/ in the workspace, not this site.