Take-Home · Part 1

Review of the Platform Foundation handover

Reading review/handover-architecture.md as the engineer who now owns the system. What I'd act on, how bad each item is, what I'd do — and one thing that looks wrong but is actually fine.

Severity uses the brief's own scale: critical = data loss, data leak, or legal exposure · serious = breaks under real load or real use · minor = worth fixing, not urgent.

The parts the doc is confident about — the transactional outbox, "standard JWT", "nothing exotic" — are broadly the right shapes. The damage is concentrated in the one thing it asks the reader to trust the database to backstop: tenant isolation. That control is undercut at several layers at once, and for a clinic under Israeli privacy law plus a foundation and a public body, an isolation failure is the whole ballgame.

Critical — data leak or legal exposure

C1 Tenant context leaks across pooled connections Critical

ProblemThe tenant is set with SET app.current_tenant = '…', which is session-scoped, not SET LOCAL. The value persists on the pooled connection after the request finishes. The next request that grabs that same connection — before, or if it ever skips, re-setting the value — runs under the previous tenant's identity.
In practiceSilent cross-tenant reads and writes. No error, no log — the wrong client's rows just come back. It is invisible in any test that uses a single tenant, which is exactly how it survived to handover.
FixTransaction-local, parameterized context on a connection pinned for the whole request:
await client.query('BEGIN');
await client.query(
  "SELECT set_config('app.current_tenant', $1, true)", [tenantId]);
// …all queries on this same `client`…
await client.query('COMMIT');
The true (is_local) scopes it to the transaction; parameterizing also removes the string-interpolation smell (latent, since tenantId is a server-derived UUID, but still wrong).

C2 No field-level authorization — confidential columns reach every role Critical

ProblemList queries are SELECT * and the only access check is a coarse per-route role gate. There is no redaction keyed to the sensitivity flag in the field definitions.
In practiceClient C reception is allowed to see the referral queue (correct), and therefore receives clinical_notes and national_id — both marked confidential, and both things the brief says reception must not see. That is disclosure of health data and a national identifier to an unauthorized role, under a regulator that expects to audit. Legal exposure, live today.
FixStop returning raw rows. Select explicit columns, and enforce a per-role field allowlist derived from field_definitions.sensitivity, applied server-side before serialization — never rely on the client to hide a column it received.

C3 Verify Row-Level Security is actually forced Critical

ProblemRLS policies do not apply to a table's owner unless FORCE ROW LEVEL SECURITY is set. If app_user owns the tenant tables (easy to end up with if the same role ran the migrations), every policy in the doc is a silent no-op and there is zero isolation.
In practiceEither isolation works or it doesn't exist — and you cannot tell by reading the doc, which asserts "RLS always applies" without showing the guarantee. This is the single fastest thing to check and the most catastrophic if wrong.
FixALTER TABLE <t> FORCE ROW LEVEL SECURITY; on every tenant table; confirm app_user is not the owner and lacks BYPASSRLS. Lock it down with a test that connects as app_user and asserts it cannot read or write another tenant's row.

C4 audit_log has no tenant_id Critical

ProblemThe audit table is the one table with no tenant column, so it sits outside RLS and mixes every client's activity together.
In practiceYou cannot scope or export one client's audit trail, and any query touching the table sees all clients at once. For a clinic that "expects to be audited" under Israeli privacy law, an audit trail you cannot produce per-tenant — and that itself leaks cross-tenant — is a compliance failure, not an inconvenience.
FixAdd tenant_id NOT NULL, enable and force RLS on it, backfill from the referenced entities, index (tenant_id, occurred_at), and write it in the same transaction as the action so the log can't diverge from what committed.

Serious — breaks under real load or real use

S1 Outbox dedup key drops legitimate repeat events Serious

ProblemHandlers skip work keyed on (event_type, entity_id). That pair is unique per entity per type, not per event.
In practiceThe second legitimate application.status_changed for the same application is discarded as a duplicate. Worse for Client C: a referral that goes urgent → is handled → goes urgent again suppresses the second duty-nurse alert. A dropped four-hour urgent alert is a patient-safety issue, not a missed email.
FixDedup on a per-event identity (the outbox row id / a unique event_id), stored in a handler-side inbox table — not on the entity.

S2 Background workers run with no tenant context Serious

ProblemThe outbox relay and handlers run detached from the request where the tenant context is set. Nothing shows them re-applying it.
In practiceUnder fail-closed RLS the handler's queries return nothing, so the 14-day email and the duty-nurse alert silently never fire; on a leaked pooled connection (see C1) they could act as the wrong tenant. The outbox row already carries tenant_id — it just isn't being used.
Fixset_config('app.current_tenant', row.tenant_id, true) at the start of each event, same pinned-connection/transaction discipline as requests.

S3 Create path misreads the result and notifies inline Serious

ProblemTwo bugs in one handler. const record = await db.query(…RETURNING *) assigns the pg result object, so record.resident_phone and record.id are undefined. And the SMS is sent synchronously, after an already-committed insert, before the 201.
In practiceThe confirmation SMS targets undefined; the 201 body is the raw result envelope. If the send throws, the caller gets a 500 even though the record exists, so a retry creates a duplicate report and a duplicate SMS. The doc frames "send before the 201" as a safety feature — it's the opposite, and it bypasses the very outbox built to solve it.
FixUse result.rows[0]; enqueue the notification through the existing transactional outbox instead of sending inline.

Minor / verify — worth fixing, not urgent

Looks wrong, but is actually fine

The RLS policy has USING but no WITH CHECK — and that's OK. Correct as written

My first instinct was to flag this as a critical: USING filters reads, so surely writes are unconstrained and a handler could insert or move a row into another tenant. I checked the Postgres semantics before writing it down, and it's wrong. When WITH CHECK is omitted, PostgreSQL applies the USING expression as the write check too, for both INSERT and UPDATE. So a cross-tenant write is already rejected by tenant_id = current_setting('app.current_tenant', true)::uuid.

Adding an explicit WITH CHECK is still worth doing for clarity and to survive a future refactor that introduces a separate USING clause — but it is not a security hole, and I'd be wrong to send someone to "fix" it as one. I'm calling it out because the difference between "looks suspicious" and "is actually broken" is a claim about Postgres I had to verify, not assume.

Which I'd fix first

C1 — the tenant-context leak — gated behind the app_user isolation test from C3. Before writing any fix I'd add a test that connects as app_user and asserts it cannot read or write another tenant's row. That single test also settles C3.

Why ahead of C2, which is genuinely tempting to grab first because it's regulated data leaking today: blast radius and detectability. C2 is bounded to one tenant's internal staff and is visible once you look for it. C1 is cross-tenant, affects every client including the clinic, and fails silently. It's also load-bearing — every other control, including the redaction fix for C2 and the audit trail in C4, assumes the database knows which tenant it is acting as. Layering fixes on top of a context that can serve the wrong tenant's rows is polishing a broken foundation.

Pin the connection, make context transaction-local and parameterized, get the isolation test green — then C2 next, because it's regulated data leaking now and it's a contained, well-understood fix once the foundation holds.