← all docs · second-client3 September 2026

Second client instance — implementation plan

Status: Planned — reviewed and rebaselined; P0 blocked on kwiss's decisions
Author: Fable (PLAN second client instance)
Date: 2026-09-03 (rebaselined 2026-09-04 and again 2026-09-07 after two rounds of dual adversarial review)
Repo: north-os
Related: specs/2026-09-03-second-client-instance-design (the spec this executes; conflict ids C-n and test ids T-n refer to it) · specs/2026-07-29-zero-downtime-deploy-design · packages/db/MIGRATIONS.md

The dates are real. Today is 2026-09-04. The instance must be usable so the client's admin can be invited, onboard, connect OneDrive firm-wide and start ingesting; the firm is in real use from 2026-10-15. The weeks between were meant as deliberate bug-finding slack and are not to be spent finishing this plan.

What changed on 2026-09-04. Two independent adversarial reviews rejected the first version of this plan. Both concluded 09-20 was unreachable as scheduled; both found work on the critical path that the plan did not contain. The largest is C-17: a firm that does not use Clio cannot finish onboarding at all, so it never reaches the step where OneDrive is connected. Two others are external dependencies with lead times North does not control (the client's Entra tenant consent, a second email sending domain). One phase — P2's parameterization of the deploy shell — was found to be the wrong shape entirely, because deploy/preprod/install.sh seeds its database from the development database and deliberately runs NODE_ENV=development with the insecure cipher. This version is rebaselined around those facts. Spec §8 records every corrected claim.

What changed on 2026-09-07. A second review round (Fable 5.1 + GPT-6-Astra) on the rewritten version found the 09-04 fixes incomplete in three places: the scheduler-login split does not close the privileged path because the runtime login is also a member of the table-owning role forma; "no service references the owner DSN" cannot be implemented because every worker requires it; and "reuse the deploy machinery verbatim" does not work because bluegreen.sh pins heynorth.dev in production mode. It also sized C-17 down to one prop plus a persisted choice, moved the RLS enumeration tests off the client's critical path, withdrew the second-sending-domain dependency (heynorth.dev is North's domain, not HSE's), and added two connector findings that matter more than most of P3: the callback never checks which Microsoft tenant authorized it, and a delegated token sees the connecting account's corpus, not the firm's. Spec §8.1 records all of it.

0. The date, stated honestly

The first version scheduled the separate-deployment path to land exactly on 09-20, with no fallback trigger, while the spec simultaneously claimed the ingestion date was never at risk. Both cannot be true, and the second is now known to be false (C-17 blocks either topology). So:

What is needed for the second client, and what is not

Ordering rule. Nothing in P5, P7 or P8 may block P1, P2 or P4. And P8 does not start on 09-20: the slack between onboarding and 10-15 is for finding bugs in a live client's instance, which is the entire reason it exists.

P0 — External dependencies and the decisions they depend on

2026-09-04 → 09-08. Owner: kwiss (decisions and client-facing asks), then an omp worker (config). Blocks P2 and P4.

Done when: the subdomain answers over TLS with a placeholder and a distinguishable body; the client's tenant has consented to all scopes, one authorization has completed and the tenant id is recorded; the corpus inventory and connecting account are named; a real email has been received from instance 2; the bucket, scoped token, KMS key and Sentry project exist; and the deploy-path decision is written down.

P1 — Onboarding without a practice-management system (C-17) + tenant pinning (C-29)

2026-09-08 → 09-10. Owner: claude opus worker (onboarding UI, flow, copy) for C-17; omp worker (connector) for C-29. Critical path, on either topology. Round two sized C-17 down from five days to roughly one: Skip already exists behind a prop, the advance route has no Clio check, and the settings page already exists.

Today: signup lands the organization at onboarding_step="invitations"; the order is invitations → matters → vault → apps; the matters step enables Continue only when Clio is authorized and a matter is tracked or sync_mode="everything", with Skip hidden; and the vault step — where OneDrive is connected — redirects away unless the organization already reached onboarding_step="vault". A OneDrive-first firm is therefore stuck before it can connect anything, and cannot invite its own people either.

Acceptance, stated as flows, each pinned in packages/e2e/tests/onboarding.spec.ts (which today works around the wall by advancing the step in the DB — that workaround goes): (1) a synthetic admin admitted by signup code, choosing "no practice-management system", reaches the vault step, connects OneDrive firm-wide, selects sources, starts ingestion, and can invite a colleague; (2) a fresh firm that does not make that choice sees today's Continue-only matters step, unchanged; (3) a completed no-system firm connects Clio from settings later. Plus (4) a wrong-tenant Microsoft authorization is rejected.

Gates: bun run lint, bun run typecheck, apps/web tests, packages/connector-onedrive tests, the e2e onboarding guard.

P2 — Instance 2 bootstrap on a dedicated host

2026-09-08 → 09-12 (first production boot), → 09-16 (second release, rollback, backup and KMS proofs). Owner: omp worker (shell, systemd, provisioning), with kwiss's word for each step that touches a live host and the deploy bot performing it. Critical path. Round two's forecast, which I share: this is the phase most likely to slip, because it is the first NODE_ENV=production + Scaleway KMS boot of this stack anywhere and every step needs a human approval.

This replaces the first version's "lift instance identity into a per-instance config file". That approach assumed deploy/preprod/install.sh was a general installer whose logic could stay byte-for-byte equivalent. It is not: it requires the dev workspace and the dev app-postgres container, it creates its database by pg_dumping the development database, it copies the development environment minus an override list, and it deliberately runs NODE_ENV=development with the insecure in-memory cipher. Generalizing it would either import dev and HSE data into a paying client's instance or require exactly the semantic surgery the approach was chosen to avoid. On a dedicated host there is nothing to generalize for — no port collision, no name collision — so:

Verification: every service's boot probe reports its expected role and app_role is NOBYPASSRLS on the new database; pg_auth_members for north_app lists exactly app_role and sync_role; the front door is green on /api/health/deep compared by body, not status; first install, a second release and one rollback performed; KMS and restore proofs recorded; and T8 — instance 2's credentials cannot connect to instance 1's database, Redis, bucket, KMS key or internal APIs (a two-line check on a localhost-bound host). Do not deploy two instances concurrently; two simultaneous production builds are unmeasured.

Gates: deploy/preprod/bluegreen-sim.test.sh and deploy-build-isolation.test.sh still pass for instance 1 (this plan does not touch its shell, so a failure means something unintended moved), plus scripts/preprod-install-env.test.ts, scripts/monitoring-config.test.ts, scripts/preprod-caddy-ports.test.ts.

P3 — Credential hygiene and the tests that guard instance 2

2026-09-08 → 09-12. Owner: omp worker (provisioning, tests). Parallel to P1/P2. Narrowed twice: round one dropped T2/T6 to October; round two moved T1/T3 there too, because on the recommended topology they guard cross-org reads inside one database — and the database with 51 organizations and a real corpus is heynorth.dev, not instance 2. What stays is what a fresh instance gets for free at bootstrap plus the tests whose failure would mean a real hole on instance 2.

Mutation discipline is part of the deliverable. T4, T7 and T8 must each be shown to go red when the invariant they guard is broken — grant a membership back, hand a graph an unauthorized thread id, authorize from the wrong tenant — and that demonstration goes in the PR body. Two versions of this plan have now specified a T4 that would have passed on day one while a hole stayed open; that is the failure this rule exists to prevent.

Gates: bun run lint, bun run typecheck, bun run test from packages/db/ (its script is Vitest; the earlier bun --filter=@workspace/db test wording was wrong), plus the tests of any package whose export surface moved.

P4 — OneDrive end to end on a synthetic firm

2026-09-13 → 09-17. Owner: claude opus worker (flows, ui-test) plus omp worker (connectors, subscriptions). Gates the ingestion start. This is the first run of the firm-wide OneDrive path on this codebase — the only OneDrive connector in the live database is HSE's, revoked — so budget for finding things.

Against a synthetic organization on instance 2, in the client's consented tenant, with the client's real corpus inventory in hand:

Stop condition, stated in advance: if firm-wide connect fails, if the connecting account cannot see the inventoried corpus, or if revoked access (reauthorizationRequired) is not surfaced, the onboarding date moves. A lapsed subscription alone is bounded by the rewalk and is not a stop; lost access that nobody is told about is.

P5 — Production readiness

2026-09-17 → 10-10. Owner: omp worker plus kwiss's word for anything touching a live host. Needed before 10-15. The minimum backup/restore and KMS proofs moved to P2; this is the rest.

P6 — Client onboarding

From 09-20 if the 09-12 gate passed in full; otherwise from 09-27, conditional on the gate re-passing by 09-19. Owner: kwiss with the client's admin.

Generate one email-bound, short-expiry signup code for the admin (bun run scripts/gen-signup-code.ts --email …). They create the firm and name their own domain, then invite their own people from inside. Then connect OneDrive firm-wide, pick ingest sources, and start ingestion. Between onboarding and 10-15 the instance is watched for bugs — which is the reason the slack exists and the reason nothing in P8 should be consuming it.

P7 — Shared-deployment prerequisites (only if the fallback is taken)

Not on the critical path in the recommended topology, and note that taking the fallback does not recover the date: C-17 and the Microsoft consent both bind on either topology. If §7.1 nonetheless chooses a second organization inside heynorth.dev, these become P2-priority and must land before the client's admin exists.

P8 — Deferred: self-service posture and follow-ups

Not in this cycle, and explicitly not on 09-20. The first version scheduled the self-service work 09-20 → 09-24, inside the bug-finding slack it insisted must stay empty.

Risks, with the honest ones first

Who does what

Per docs/WORKFLOW.md § Who does what: P2, P3, P5, the C-29 connector change in P1 and the connector half of P4 go to an omp worker (schema, provisioning, shell, systemd, config, connectors). P1's onboarding change and P4's flow verification go to a claude opus worker (onboarding UI, flows, UX copy). Specs, plans, prompts, reviews, gates and git stay with the orchestrator. Only one agent touches the schema, in the P1 wave (the skip choice column and matter_hidden's policy ride the same migration). Every phase gets its own dual adversarial review before merge — a fresh Fable session and an omp reviewer at gpt-5.6-sol high — with an explicit instruction to audit what each diff deletes, and, for P1, to check that HSE's own onboarding path did not move.