Skip to content

M0 · Monorepo move

Rewritten on 2026-10-05 for D25. M0 is rebuilt from an empty folder, in the order of the timeline. BE-M0-01 is the timeline's row 1; packs A to F below fill rows 18 to 22 and 32, and BE-M0-32 (row 18a) fixes pack A.

  • D3 · Business rules, multi-step actions and access control live in Postgres; every pack below writes them there.
  • D4 · The Supabase CLI owns the schema; pnpm db:types writes the types into @opleet/db-types, committed.
  • D5 · supabase/ at the repository root; tools/ outside the pnpm workspace.
  • D6 · Conventional Commits; this runbook's commits use the supabase and tools scopes.
  • D9 · Schema V13's business rules and loophole fixes, P15 included (pack D).
  • D10 · Every refusal raises a stable key with a JSON detail, and a pgTAP test asserts it.
  • D11 · Migration files are edited in place until cutover; nothing is pushed to production.
  • D12 · pgTAP on the local Supabase stack, Postgres 17; the CI database job runs on a path filter (supabase/** and tools/db/**).
  • D13 · Publishable and secret keys; the local stack signs tokens with an ES256 key made on each machine (pack A).
  • D14 · Storage paths carry no pool; policies find the pool through the owning row's resolver (pack D).
  • D19 · Every rule and every RPC gets pgTAP tests in the same step.
  • D22 · packages/db-types holds the generated types only.
  • D25 · M0 is rebuilt from an empty folder, in the timeline's order. The delivered M0 and the design canvas are background: read, never copied in whole. Its 44 checks are the acceptance list (pack E). The plain-Postgres stub never enters the repository.
  • D27 · apps/docs holds the rules: every pgTAP description starts with the ID of the rule it proves, and pnpm docs:rules checks the links. Pack B waits for DOC pack A's IDs; pack E adds pnpm docs:reference for the generated pages and maps the 44 checks to rules.
  • D28 · GitHub Team: every pack works on a branch, its pull request needs the checks and pr-title jobs green, and zuki squash-merges it; agents never merge. A run is logged as a comment on the pack's issue. This runbook lives in apps/docs.
  • D29 · name (Indonesian) and name_en (English, optional) on charge_types, lead_sources, inspection_item_types and body_panels (pack B); proper nouns, pricing groups among them, keep one name.
  • OQ2 · Open. Pack A switches self sign-up off on the local stack, following the proposed answer.
  • OQ13 · Open. Realtime stays off on the local stack until it is decided.
  • OQ21 · Open. Proposed answer: the migration's author regenerates the types in the same commit.
  • OQ24 · Open (#47). The Backend Engineer's view is E1, a Postgres enum; until it is decided, pack B keeps app.error_keys and app.refuse() as written.

Interface, updated 2026-10-07. Pack B writes everything below unless a later pack is named. None of it has run on the Supabase stack yet, so it stays planned until zuki logs a clean run of the pack that writes it. The names, arguments and columns are fixed, so the Frontend Engineer can build against them.

  • public.my_permissions() · RPC with no arguments, for authenticated users only; anon cannot call it. Returns one row per pool and permission the caller holds after roles, grant overrides and deny overrides: organization_id uuid, pool_id uuid, pool_code text, permission_code text, ordered by pool code, then permission code. A role granted for all pools comes back as one row for each pool of the organization. Call it as rpc('my_permissions'). A suspended membership gives no rows. Written in pack B, BE-M0-13.
  • public.set_my_locale(locale text) · RPC for any signed-in user. It sets profiles.locale for the caller only and returns the stored value. It accepts id and en; anything else raises profile.locale_unsupported with the detail {"locale": …}. profiles.locale defaults to id (D7). Written in pack B, BE-M0-13 (FE-R5). Without a signed-in person it raises profile.not_signed_in.
  • Views v_charge_balance, v_driver_balance, v_checkpoint_due and v_driver_clearance · security_invoker, so RLS applies to whoever reads them. Written in pack B, BE-M0-20. How v_driver_clearance treats an older failed check is OQ7; its states wait for a rule (BC open 1).
  • Status changes · app.set_vehicle_status and app.set_driver_stage stay in the app schema, which the API does not expose, and check the caller's permission at the row's pool. An app user who edits vehicles.status, drivers.stage, rentals.status or charges.status directly gets vehicle.status_locked, driver.stage_locked, rental.status_locked or charge.status_locked. The actions that change a status get public RPCs with their modules (M2 onwards). Written in pack B (BE-M0-15, BE-M0-16, BE-M0-17, BE-M0-19).
  • Master data names (D29) · charge_types, lead_sources, inspection_item_types and body_panels have name (Indonesian, required) and name_en (English, may be null: show name then). The other master data has one name; pricing groups are named after the models they price. Written in pack B (BE-M0-14); the system charge types and lead sources carry both names from pack D (BE-M0-22).
  • Row-level security (pack C, BE-M0-21) · a row is visible to a caller who holds one of its table's read permissions at the row's pool, and written by one who holds its write permission there; the generated Row-level security page lists every policy after BE-M0-31. A read the policies refuse returns no rows. An insert they refuse raises 42501 (“new row violates row-level security policy”), and an update or delete they refuse changes nothing and raises nothing. A table with no policy for an action refuses it to every app user, which holds an open question until its rule exists. Today apps/web can't read organizations (DOC-Q11), permissions (DOC-Q12), vehicle_brands and insurer_garages (MD open 1) or maintenance_records (WO open 1). It can't request a pool transfer (TRF open 1), write debt letters, their items or collection calls (COL open 1), or read audit rows with no pool (AUD open 1). my_permissions() needs none of these.
  • Storage (pack D, BE-M0-24) · four private buckets: documents (KTP, SIM, STNK, contracts and BASTK PDFs), inspections (inspection photos and signatures), screening (background check evidence) and payments (proof of payment). A file's path is <organization_id>/<table>/<row_id>/<file name>, where the row is the one that stores the path, with no pool (D14). Uploading needs write access to that row, and reading needs read access to it, at the pool the row has now; the row must exist first. So apps/web makes the row's id itself (crypto.randomUUID()), inserts the row with the path the file will have, then uploads to that path. Upload with upsert off: a file is never replaced or deleted through the API, and a new version is a new path. Show a file through a signed URL. Path columns hold the storage_path type: a bucket path of at most 400 characters, never a URL or a data: URI. A long base64 run in a contract template, a driver note or v1's nota is refused too (23514). MIME types and size limits per bucket wait for STO open 1 (DOC-Q15); until then the stack's 50 MiB limit applies.
  • E2E logins (pack E, BE-M0-25) · pnpm db:e2e-users makes one login per system role in the E2E organization, with the role at every pool (E2A and E2B). The address is e2e.<role, with - for _>@e2e.opleet.test. .env.e2e holds E2E_PASSWORD and E2E_<ROLE>_EMAIL for the 11 roles, for example E2E_ADMIN_FLEET_EMAIL.
  • Generated types (pack E, BE-M0-26) · pnpm db:types writes packages/db-types/src/database.ts from the local database's public schema. It exports the types (Database, Tables, TablesInsert, TablesUpdate, Enums) and one value, Constants, which lists every enum's values.
  • Error keys (D10) · each refusal raises its key as the error message, with the JSON detail named here; Postgres's code is P0001 for a broken rule and 42501 for a missing permission. app.error_keys holds the list, and pack E's generated Error keys page publishes it (BE-M0-31). Whether the list becomes a Postgres enum that pnpm db:types turns into a TypeScript union is OQ24. Written in pack B, by step (key, code, detail fields):
  • BE-M0-12 · internal.unknown_error_key (P0001; key): a function tried to raise a key that is not registered. A bug, never the user's mistake.
  • BE-M0-13 · access.not_member (42501; organization_id) · profile.not_signed_in (42501; no detail) · profile.not_found (P0001; profile_id) · profile.locale_unsupported (P0001; locale) · settings.pool_not_found (P0001; pool_id) · settings.unknown_key (P0001; key) · document.wrong_organization (P0001; organization_id, legal_entity_id) · document.bad_period (P0001; period).
  • BE-M0-15 · vehicle.not_found (P0001; vehicle_id) · vehicle.status_forbidden (42501; vehicle_id) · vehicle.status_locked (42501; vehicle_id).
  • BE-M0-16 · driver.not_found (P0001; driver_id) · driver.stage_forbidden (42501; driver_id) · driver.stage_locked (42501; driver_id).
  • BE-M0-17 · rental.cross_pool (P0001; driver_pool_id, vehicle_pool_id) · rental.status_locked (42501; rental_id).
  • BE-M0-19 · allocation.payment_voided (P0001; payment_id) · allocation.exceeds_payment (P0001; payment_id, payment_amount, allocated) · allocation.exceeds_balance (P0001; charge_id, balance, amount) · allocation.cross_entity_blocked (P0001; payment_entity_id, charge_entity_id) · charge.status_locked (42501; charge_id).
  • Refusals by a constraint keep Postgres's own code and name the constraint instead of a key: 23505 for a unique (such as rentals_one_live_per_vehicle), 23514 for a check (such as drivers_phone_e164), 23503 for a foreign key (such as insurance_claims_partner_garage_fk). The module actions users call (M2 onwards) check first and raise keys; the constraints stay as the backstop.

Requests from FE, 2026-10-02, updated 2026-10-05 for D25:

  • FE-R1 · packages/db-types: the package name and import path for the generated Database type, and whether apps/web needs transpilePackages for it. Needed by FE-M0-06. Status: answered in part; transpilePackages waits for BE-M0-26.
  • FE-R2 · E2E logins on the local stack, one per role in the E2E organization. scripts/create-e2e-users.mjs writes to a database, so under the monorepo spec it belongs in tools/; it uses the secret key and refuses any non-local URL. Needed by FE-M0-08 and FE pack E. Status: planned in BE pack E (BE-M0-25).
  • FE-R3 · List my_permissions() here: its arguments and returned columns. It exists from M0. Needed by FE pack C. Status: answered.
  • FE-R4 · Confirm the local stack signs tokens with asymmetric keys, or set it in config.toml, so getClaims() verifies locally as D13 intends. With a shared secret it calls the Auth server on every request. Needed by FE-M0-08. Status: answered; BE pack A Done 2026-10-06.
  • FE-R6 · The E2E logins script (BE-M0-25) also writes what Playwright reads to the git-ignored .env.e2e at the repository root: E2E_PASSWORD and one E2E_<ROLE>_EMAIL per role, the role code in capitals (E2E_FINANCE_EMAIL). The values stay in that file, never in a runbook. Needed by FE pack E (timeline row 29) and the CI end-to-end job (TL-M0-19). Status: agreed 2026-10-06, planned in BE pack E (BE-M0-25).

BE answers, updated 2026-10-06:

  • FE-R1 · Answered by D25: the package is @opleet/db-types, created by TL-M0-10; import the type with import type { Database } from "@opleet/db-types". Pack E (BE-M0-26) writes the first generated types. The file exports one runtime value, Constants, besides its types, and the package exports its TypeScript source. So apps/web needs transpilePackages: ["@opleet/db-types"] in next.config as soon as it imports Constants (enum labels per D29 will); type-only imports need nothing. Status: answered 2026-10-07; BE-M0-26's run log confirms the export.
  • FE-R2 · Pack E (BE-M0-25): tools/db/create-e2e-users.mjs, one login per role in the E2E organization, using the secret key. It targets the local stack by default; a staging run needs --staging and is zuki's to run (pack F); a production URL is always refused. Run it with pnpm db:e2e-users. Status: written in pack E.
  • FE-R3 · Answered above, under my_permissions(). Status: answered; written in pack B (BE-M0-13).
  • FE-R4 · Pack A (BE-M0-10, BE-M0-11): an ES256 key made on each machine by pnpm db:signing-key and kept out of git, with signing_keys_path in config.toml. The stack then publishes its public key at /auth/v1/.well-known/jwks.json, so getClaims() verifies tokens locally. CI makes its own key in the database job. Status: answered; pack A Done 2026-10-06.
  • FE-R5 · Pack B (BE-M0-13): profiles.locale and public.set_my_locale(), described above. Asked in the M1 tab; D25 moved it into M0's tenancy step. Status: written; runs with pack B.
  • FE-R6 · Agreed, in pack E (BE-M0-25): after making the logins, the script writes E2E_PASSWORD and one E2E_<ROLE>_EMAIL per role, the role code in capitals, to the git-ignored .env.e2e at the repository root, replacing only those lines. It makes a random password on the first run and reuses the one in .env.e2e after that, so the values never appear in a runbook or a chat. Status: written in pack E.

M0 is rebuilt from an empty folder in the order of the timeline (D25). Each pack below fills one timeline row and runs when the rows above it are done. A step is done when zuki has logged a clean run on its pack's issue (D28). Step IDs continue from BE-M0-10, and withdrawn IDs are never reused.

Withdrawn on 2026-10-05 by D25: BE-M0-02 (the baseline of the delivered M0, never run), BE-M0-03, BE-M0-04 and BE-M0-09. BE-M0-05 to BE-M0-08 were planned and never written; their work is now in packs A to E.

Every pack is checked in BE's workspace before it is sent: on plain Postgres 16 with a stand-in for Supabase's auth and storage schemas that never enters the repository. Each step is run from its zip on its own, the generators must write the files the zip holds, and the rule check runs against DOC pack A's draft 1. Then 24 rules across packs B to D are broken on purpose, each one failing its test. The real run is zuki's, on the Supabase stack and Postgres 17.

BE-M0-01 · Preflight: tool versions (timeline row 1)

Section titled “BE-M0-01 · Preflight: tool versions (timeline row 1)”

Pack A · Local stack settings (timeline row 18)

Section titled “Pack A · Local stack settings (timeline row 18)”

Delivers: self sign-up off, the site URL for apps/web, an ES256 signing key on each machine so getClaims() verifies locally (FE-R4), and the services M0 doesn't use switched off: Logflare and Vector, Edge Functions, Realtime and image transformation. Two steps on one branch, merged as one pull request.

Runs after timeline row 17. Status: Done 2026-10-06 (logged by zuki).

Added 2026-10-07: pull request #6 committed supabase/signing_keys.json, the private key, and supabase/.gitignore has no line for it. BE-M0-32 ignores and untracks the file and makes a new key. It can run now, before pack B (#69).

Pack B · Schema V13, domain by domain (timeline row 19)

Section titled “Pack B · Schema V13, domain by domain (timeline row 19)”

Delivers schema V13 domain by domain in nine steps: 9 migrations with 70 tables, 66 enums, the rule functions and triggers, 4 views and the audit log, and 187 pgTAP tests in 10 files. Each step copies one folder of be-pack-b.zip into the repository, rebuilds the local database with pnpm supabase db reset, runs every test with pnpm supabase test db and commits. BE-M0-20 marks the rules the pack proves as built and opens the pull request. Written new from the V13 references; the delivered M0 was read as background only (D25). The error keys are listed under Interface.

Changes to the plan of 2026-10-05: settings (P10), pool dates (P11), document numbers (P12) and updated_at (P14) moved into BE-M0-13 beside their tables; each status rule (P9) sits with its table; every table has RLS switched on in the step that creates it, and pack C adds the policies. Changes of 2026-10-07, to follow DOC pack A's rules: a plate change keeps the old plate (R-VEH-09); the audit log writes a row for every update, even one that moves only updated_at, and logs an unmarked change as app (R-AUD-01, R-AUD-05); a charge's status is written only when it changes; the pool time-zone limit and the partner-filter helper are out until rules cover them. Changes for D29, later on 2026-10-07: name_en on the four bilingual lookups (BE-M0-14), and profiles.avatar_url became avatar_path, a Storage path that pack D types (P15).

Rule IDs (D27): every test description starts with the ID of the rule it proves, from DOC pack A's list (draft 1 of 2026-10-06); RULE-IDS.txt in the zip lists the tests for each rule. Each of the 135 rules DOC expects in M0 B has a test, and no ID is invented. Eight draft tests went out because no rule covers them: DOC-Q1, DOC-Q2 and DOC-Q9 below name seven, and pack C tests the eighth, the organization-level permission helper, through its policies. BUILT-IDS.txt lists the 134 rules BE-M0-20 marks built. Four cited rules stay planned: R-ACC-02 until pack C, R-NUM-01 until a test can run two sessions at once, and R-CONV-07 and R-TEN-01 until DOC-Q5 and DOC-Q6 are answered.

Questions for the Docs Engineer, from pack B (2026-10-07):

  • DOC-Q1 · Proposed rule: every function that runs as its owner (security definer) pins its search_path. All of pack B's do; their test went out until the rule has an ID. Status: open.
  • DOC-Q2 · Proposed rule: a pool's time zone is WIB, WITA or WIT (Asia/Jakarta, Asia/Pontianak, Asia/Makassar or Asia/Jayapura). Pack B no longer limits it. Status: open.
  • DOC-Q3 · R-AUD-01 says every update writes an audit row, so pack B now logs an update that moves only updated_at, with no changed field. If such updates should be skipped, a rule saying so brings the skip back. Status: open.
  • DOC-Q4 · R-AUD-06: pack B masks doc_number on every document, because a KTP's number is the NIK. Should SIM and other document numbers stay in clear? Status: open.
  • DOC-Q5 · R-CONV-07: V13 checks E.164 only on drivers, emergency contacts and queue tickets. profiles.phone (copied from the login), rental_companies.contact_phone, garages.phone and insurers.phone have no check. Does the rule cover them? Status: open.
  • DOC-Q6 · R-TEN-01: profiles and permissions are global in V13, with no organization, and the rule's exceptions don't name them. Status: open.
  • DOC-Q7 · R-CONV-10: drivers.registered_on, maintenance_records.started_on and debt_letters.issued_on default to the server's date (UTC on Supabase), as V13 has them. Are they business dates under the rule? Status: open.
  • DOC-Q8 · Two argument checks are cited under R-CONV-13, since their tests prove the refusal raises its key: an unknown setting name (settings.unknown_key) and a counter period that isn't yyyy or yyyy-mm (document.bad_period). Should either be a rule of its own? A daily period for NUM open 1 would change the second. Status: open.
  • DOC-Q9 · Untested until the open question has a rule; pack B's code does what the brackets say: ACC open 1 (no partner filter yet; its helper comes with pack C), VEH open 3 (a pool given on an inspection, work order or gate pass is kept), MON open 1 (a charge closed by adjustments alone is refunded if one is a refund, else waived), BC open 1 (the states of v_driver_clearance). Status: open.
  • DOC-Q10 · Behaviour that answers an open question for now: SET open 1 (the settings row is made with the organization, which R-SET-03's test relies on) and MON open 2, untested (a cash payment, or a charge with no legal entity, is never cross-entity). Status: open.

be-pack-b.zip, rebuilt for D29 on 2026-10-07: SHA-256 911b70bb639b0fe30d1ac8bb1406e1b0794f811f0a45354c7e9346acfbca7d7c. The zip sent earlier that day (abac3694…) and the draft of 2026-10-06 are withdrawn. Runs after timeline rows 18 and 14c: pnpm docs:rules fails on the IDs the tests cite until DOC pack A's pages merge. If DOC pack A merges with IDs other than its draft 1, BE rebuilds the zip first. Issue: #11.

Pack C · Row-level security (timeline row 20)

Section titled “Pack C · Row-level security (timeline row 20)”

Delivers the policies for all 70 tables in one step. supabase/migrations/20261006001000_access_helpers.sql adds the resolvers the policies need (a pool's, a membership's, a role's and a profile's organization, a GPS device's pool, and who may write an inspection of each kind). tools/db/gen_rls.py holds one spec, table by table, naming the rules each entry implements, and writes 20261006001100_rls.sql: 184 policies, all in the = any ((select app.pools_with_any(…))::uuid[]) pattern. It also writes the grants: anon loses every privilege; authenticated loses TRUNCATE, REFERENCES and TRIGGER, every write to the audit log and every access to the document counters; even service_role can't write the audit log. A last block refuses to finish if any public table lacks RLS or is missing from the spec. 21_rls.test.sql adds 73 tests: pool scoping, deny overrides, tenant isolation, anon, the document counters, the audit log's guard, and every M0 C rule.

A table with no policy for an action refuses it to every app user. That is how pack C holds an open question until its rule exists; the Interface above lists what apps/web can't do yet. R-WO-03 and R-WO-04 stay planned until WO open 1 decides which pool a maintenance record belongs to; their work-order half is tested. BE-M0-21 marks 65 rules built: every M0 C rule except those two, plus R-ACC-02, R-ACC-21 and R-MD-05.

Questions for the Docs Engineer, from pack C (2026-10-07):

  • DOC-Q11 · Who reads an organization's row? No rule says, so organizations has no policy and apps/web can't show the organization's name. A proposal: any member reads their own organizations. Status: open.
  • DOC-Q12 · Who reads the permission catalogue? permissions has no policy, and my_permissions() returns codes without labels. A proposal: every signed-in user reads it, since it holds no tenant data. Status: open.
  • DOC-Q13 · R-ACC-17 and R-ACC-18: pack C reads a member's rental companies with user:read, adds them with user:write and removes them with user:delete, in the membership's organization. Is that the rule's intent? Status: open.
  • DOC-Q14 · A profile is shared by every organization its person works for, so pack C lets a user delete it only with user:delete in each of them. Should that be a rule, or should deleting a profile be refused outright in favour of ending a membership? Status: open.
  • DOC-Q16 · AUD open 1: audit rows with no pool (organization-level changes) are read by no app user until it is answered. Status: open.

be-pack-c.zip: SHA-256 cd00f61809b5bee2e8d0adb1c752fe901c8ba9821c0ae3bf5ec3346290475292. Runs after row 19. Issue: #12.

Pack D · Seed and Storage (timeline row 21)

Section titled “Pack D · Seed and Storage (timeline row 21)”

Delivers the system rows, the sealed E2E organization and Storage in three steps on one branch. tools/db/gen_seed.py writes 20261006001200_seed_system.sql with the 45 permissions, the 11 system roles and their 193 grants (as v1 had them, renamed), the 8 system charge types and the 11 system lead sources, the last two with name and name_en (D29). supabase/seed.sql makes the E2E organization the end-to-end tests sign in to. 20261006001300_storage.sql makes the four private buckets, the file policies that ask the owning row through pack C's generated resolvers (D14), the storage_path type on all 14 path columns and the base64 guards (P15). Tests: 5 for the system rows and 10 for Storage, 275 in all. BE-M0-24 marks 7 rules built: R-ACC-25, R-ACC-27 and R-STO-01 to R-STO-05.

Question for the Docs Engineer, from pack D (2026-10-07):

  • DOC-Q15 · STO open 1: which MIME types and which size limit for each bucket? Pack D sets none, so the stack's 50 MiB limit applies to all four. Status: open.

be-pack-d.zip: SHA-256 db47518079575c5a19972cea08f5a6343b166ed13e29659a36210edc3b4b1534. Runs after row 20. Issue: #13.

Pack E · Logins, types and the acceptance check (timeline row 22)

Section titled “Pack E · Logins, types and the acceptance check (timeline row 22)”

Delivers four steps on one branch: the E2E logins script (FE-R2, FE-R6), pnpm db:types with the first generated types (FE-R1), the acceptance check, and pnpm docs:reference with the generated reference pages (D27). Of the delivered M0's 44 checks, 43 are proved by the tests of the rule each maps to, as BE-M0-27's table shows. Check 7 (satpam reads its pool's car) waits for VEH open 1: under R-VEH-14 only vehicle:read reads a car, and satpam holds gate:read. The pack adds no test and marks no rule built.

be-pack-e.zip: SHA-256 7fb2ba8bc7ae01ac64e5551d13304b54f777ab217799ce0c2767b5d46c976e5e. Runs after row 21. Issue: #14.

For the CTO: what the CI database job must run (timeline row 23, TL-M0-18)

Section titled “For the CTO: what the CI database job must run (timeline row 23, TL-M0-18)”

TL-M0-18 is the CTO's. From the backend side, the job must run these, in this order, on a path filter of supabase/** and tools/db/** (D12):

  • pnpm install --frozen-lockfile, which brings the pinned Supabase CLI.
  • pnpm db:signing-key: the signing key is git-ignored (from BE-M0-32 on), so the job makes its own before the stack starts.
  • pnpm supabase start: config.toml already switches off the services M0 doesn't use, so no -x flags are needed.
  • pnpm supabase db reset: every migration and the seed, from an empty database.
  • pnpm supabase test db: every pgTAP file. Through their rules, they cover 43 of the 44 acceptance checks (BE-M0-27).
  • python3 tools/db/gen_rls.py, python3 tools/db/gen_seed.py, then git diff --exit-code supabase/migrations: the generated migrations are current (packs C and D).
  • pnpm db:types, then git diff --exit-code packages/db-types: the committed types are current (pack E, D4).
  • pnpm docs:reference, then git diff --exit-code apps/docs/src/content/docs/reference: the generated reference pages are current (pack E, D27). Add apps/docs/src/content/docs/reference/** to the path filter, so a hand edit there fails too.
  • pnpm supabase stop --no-backup at the end, also after a failure.

The end-to-end job (TL-M0-19) needs pnpm db:e2e-users after the reset; it writes .env.e2e for Playwright inside the job. A fresh clone needs pnpm db:signing-key once before its first pnpm supabase start. The README and AGENTS.md (the CTO's files) could list both with the other commands.

  • BE-M0-28 · Link the CLI to the staging project zuki creates in TL-M0-20: pnpm supabase link --project-ref <STAGING_PROJECT_REF>, with <STAGING_DB_PASSWORD> typed at the prompt. Never linked to v1 or to production.
  • BE-M0-29 · First push: pnpm supabase db push --dry-run, then db push, then the seed with --include-seed (staging only, never production).
  • BE-M0-30 · E2E logins on staging: pnpm db:e2e-users --staging, which reads SUPABASE_URL, SUPABASE_SECRET_KEY and STAGING_PROJECT_REF from the git-ignored .env.staging and refuses any other host.

After the first push, migration files edited under D11 no longer match staging's history. Until the rehearsal job rebuilds staging every night, zuki rebuilds it by hand from a runbook step. That wipes staging: not reversible, L0, and never on production. Status: not written; issue #22.