Skip to main content

Complied — Requirements (Source of Truth)

Status: locked decisions · Date: 2026-08-20 · Last amended: 2026-09-02 (D21)

This file is the source of truth for product decisions (D1–D21), vocabulary, capability-parity principle, tenancy, and the agency/program abstraction. Where this document and any other file disagree on intent, this document wins.

It is not the source of truth for what shipped. Architecture of the running app: docs/domain/. Honest walk-through status and the ranked next-fix list: docs/LIFECYCLE_ASSESSMENT.md §8. Rebuild change log: docs/history/CHANGE-LOG.md.

⚠️ Section numbering is load-bearing and deliberately irregular. Over 100 comments in src/, domain/, supabase/migrations/ and supabase/functions/ cite this file as REQUIREMENTS §n.m. §5's subsections are numbered 4.x and §6's are numbered 5.x — a historical drift that the citations now depend on, so it is kept on purpose. Renumbering them to "tidy up" would silently break ~59 citations. Add new subsections at the end of a section; never renumber an existing one.

The HPD rulebook domain content (docs/RULEBOOK.md, docs/HPD_SYSTEM_MODEL.md, domain/src/programs/nyc-lead-paint/) is hard-won domain truth, ported forward faithfully. The code twin lives in domain/, not src/lib/hpd/ (that path is archive/).


1. What Complied is

Complied is a multi-tenant, white-label lead-paint compliance platform. Complied (the company) is the platform operator; each customer organization — SecureEnv, Abated, and future ones — is a tenant running its own white-labeled instance: branding, clients, buildings, projects, staff. For a tenant, the product watches agency data for compliance events, surfaces what needs doing, runs the work (schedule and field-capture it — XRF, dust wipe, paint chip, abatement), produces the paperwork, and proves it with a defensible, auditable record.


2. Decisions locked

These are settled. Do not relitigate them without an explicit change to this file.

#DecisionChoice
D1DatabaseFresh schema in a new Supabase project. One clean, consolidated migration set designed around tenancy from line one. The 215 existing migrations are archived, not carried forward.
D2FrontendKeep React + Vite + TypeScript + shadcn-ui. Rebuild the UI against the new schema. Salvage genuinely good logic (HPD rulebook, XRF engine, report renderer); rewrite the pages.
D3Tenant isolationShared tables + tenant_id + Postgres RLS. Every tenant-owned row carries tenant_id; isolation is enforced in the database, not the client.
D4Cross-tenant workPer-project collaborator invitation. The owning tenant explicitly invites another tenant onto one specific project with a scoped role. Projects are the only cross-tenant surface.
D5IdentityOne user belongs to exactly one tenant. Clients (building owners / managing agents) log into a separate white-label portal as a distinct account type. Complied platform operators sit above all tenants.
D6Abstraction axisAgency and compliance program, within NYC. The core is neutral across NYC agencies (HPD, DOB, DOHMH, FDNY, ECB/OATH, DOF, DEP). v1 ships exactly one program: NYC Lead Paint, owned by HPD with DOHMH participating. Region/jurisdiction is a second, outer axis reserved for later. See §8.
D7Customer hierarchyTenant → Client → Building → Unit → Project.
D8Service & phase definitionDefined in code, not in the database. No admin workflow studio in v1.
D9BillingNone in v1. Every tenant is unlimited. No plans, no entitlements, no Stripe.
D10BuildingsCitywide public reference registry + a tenant-scoped tracking layer. One row per real building, shared; tenant relationships recorded separately.
D11The firms conceptRemoved entirely. There are no firms inside a tenant. firms, firm_settings, firm_licenses, firm_documents and every firm_id column are deleted.
D12Git historyReset. The repo starts fresh; the old history remains on the GitHub remote for reference.
D13Capability parityNothing the system does today may be lost. The rebuild changes architecture, vocabulary, and structure — never capability. Every existing behavior is inventoried and carried forward or consciously retired with written sign-off. See §4.
D14Vocabularytenant = the white-label organization, always. The resident is an occupant.
D15BackendSupabase-only for the MVP. No separate API service in v1. Browser → PostgREST/RPC stays, with RLS as the enforcement layer. A NestJS service is the known next step, triggered by the first external (non-internal) tenant onboarding — see §10.1. The seams that make that migration cheap are mandatory in v1, not optional.
D16White-label routingSubdomain per tenant in v1 — e.g. secureenv.complied.com, abated.complied.com. Custom domains deferred.
D17Production data cutoverRe-derive all public/reference data from agency feeds at cutover (buildings, units, public_events, PLUTO, etc.). Migrate tenant-authored data only (clients, projects, documents, invoices, XRF history, profiles). Live production migration is gated on explicit owner approval when ready.
D18Inspector tenancyOne inspector, one tenant. An inspector profile belongs to exactly one tenant and never works across two. D5 stands; no multi-tenant JWT model.
D19Agency feeds (v1)Keep all active agency syncs — they are the compliance-intelligence layer (§9.2). Retire confirmed-dead code only (sync-doh-data, sync-fdny-data, W7 backfills). One orchestrator; feeds write only to public_events (no legacy per-agency mirror tables).
D20Edge function pruningConfirmed RETIRE functions are deleted from live Supabase, not left deployed.
D21Phase as capabilityForbidden. Nothing in the app may read projects.phase to decide what a user can do. Phase is a manually-set ops label. Any future check gates an action on data facts, never on the phase value. (Rung 6, owner ruling 2026-08-31; see docs/domain/GATING.md.)

3. Glossary and vocabulary

The word tenant is overloaded in this domain and it will cause bugs if left ambiguous.

TermMeaning in this system
TenantA white-label customer organization of Complied (SecureEnv, Abated). The unit of data isolation.
OccupantA person living in a residential unit. This is what NYC law calls a "tenant."
ClientA customer of a tenant — a property management company or building owner.
Platform operatorA Complied employee with cross-tenant access.
BuildingA physical structure, keyed by BIN, in the citywide registry.
UnitAn apartment within a building.
ProjectA unit of billable compliance work, attached to a unit or directly to a building.
ViolationA reactive compliance event: an agency has cited the property.
ObligationA proactive compliance event: a recurring legal duty with a running clock, independent of any citation.
EventAnything landing in public_events from an agency feed — the raw material both violations and obligations are derived from.

RESOLVED 2026-08-20 (D14). tenant is reserved exclusively for the white-label organization; the resident is an occupant. The third project origin is occupant_request; access_status = 'tenant_refused' becomes 'occupant_refused'. Any legacy identifier using tenant to mean resident is renamed — a vocabulary rename, never a behavior change.


4. Capability parity — the governing constraint (D13)

This rebuild is a refactor, not a reduction. The owner's position, stated explicitly:

"I'm not here to break the workflow logic. Whatever it achieves today, I want to achieve the same thing — just in a much more refactored and better manner. Renaming the vocabulary, better architecture, removing redundant stuff. But what it essentially achieves today, I still want to be able to achieve."

4.1 What may change freely

Table names and shapes · column names and types · file and module layout · component structure · vocabulary and labels · the number of tables · how data is stored, indexed, and joined · which layer a rule lives in.

4.2 What may not change without written sign-off

Any observable behavior: a compliance rule's outcome (deadlines, applicability, required documents, signer roles, notarization, order-code behavior); a data feed running today, on the same cadence, producing the same coverage; a document or report the system can generate today; a calculation (pricing, deadlines, XRF pass/fail, exemption status, RPO determination); a workflow a user can complete today, end to end.

4.3 How parity is enforced

  1. The Capability Inventory is written before anything is deleted — a behavior list (every scheduled job, feed, generated document, calculation, completable workflow), not a table list.
  2. Every entry is dispositioned: CARRY, RETIRE (reason + owner sign-off), or UNKNOWN (investigate — never a default). Nothing is dropped on an UNKNOWN.
  3. The inventory is the acceptance checklist. A phase is done when its CARRY entries work against real data — not when the code compiles.
  4. Rules get fidelity tests. Ported compliance logic must reproduce the old implementation's output case-for-case before the old code is deleted.

4.4 The specific risk to watch

The highest-risk area is the least visible: public-data ingestion — crons writing to tables no page reads directly, failing silently. A table that looks unused is not evidence that it is. No table is dropped until every edge function, cron, trigger, view, and RPC touching it has been traced.


5. Tenancy and access control

4.1 The isolation rule

Every table holding tenant-owned data carries a non-nullable tenant_id uuid references tenants(id). RLS on every such table restricts rows to the caller's tenant, resolved from a JWT claim.

Three data classes, three policy shapes:

ClassExamplesPolicy
Public referencebuildings, units, public_events, PLUTO lots, agency lookup tables, HPD order mapsReadable by all authenticated users. Written only by service-role sync jobs. USING (true) is acceptable here and nowhere else.
Tenant-ownedclients, projects, inspections, documents, invoices, profilestenant_id = current_tenant_id(), plus the project-collaboration exception below.
Platformtenants, platform_operators, impersonation_audit, sync run logsPlatform operators only.

Hard rules: no tenant-owned table ever gets USING (true); every SECURITY DEFINER function ships a paired REVOKE EXECUTE ... FROM anon, authenticated in the same migration, granting only the roles that need it; client-portal users see only rows belonging to their own client, within their tenant; tenant isolation is proven by an automated cross-tenant leakage test suite, not by inspection — a v1 deliverable.

4.2 Roles

Scoped to a tenant unless stated otherwise:

RoleScopeCapability
platform_operatorGlobalCross-tenant read, tenant provisioning, audited impersonation
tenant_adminTenantEverything within the tenant, including user and branding management
project_managerTenantCreate and run projects, assign work, approve documents
inspectorTenantField execution on assigned projects; XRF, samples, photos, notes
back_officeTenantDocuments, filing packages, invoicing
client_userClientRead-only portal access to their own buildings, projects, and documents

Impersonation by a platform operator is time-bounded, reason-required, and written to an append-only audit log that the impersonated tenant can read.

4.3 Cross-tenant collaboration (D4)

The only cross-tenant surface is the project. A project has exactly one owner tenant, which may create a project_collaborators row inviting another tenant with a scoped role (e.g. field_execution, lab_coordination, abatement); the collaborator sees only that project, and within it only the surfaces its scope covers. Financials are never shared — proposals, rate cards, invoices, and vendor payments stay visible to the owner tenant alone, regardless of scope. Every collaborator action is attributed to (user_id, acting_tenant_id) in the audit log. RLS reads: a row is visible if tenant_id = current_tenant_id() OR it belongs to a project on which the caller's tenant holds an active collaboration grant covering that surface.


6. Domain model

Table-by-table shape is documented authoritatively in docs/domain/SCHEMA.md; what follows is the decisions the schema must encode, not a restatement of it.

5.1 Buildings and units (D10)

buildings is a citywide public registry keyed by BIN, seeded/refreshed from PLUTO and agency feeds, owned by no tenant. units are likewise shared; tenant-specific facts about a unit are tenant-scoped rows referencing it, never columns on the shared row. tenant_buildings is the tenant-scoped tracking layer — which buildings a tenant watches/services, which client they belong to, its own annotations. Two tenants servicing the same building share one building row and hold two tenant_buildings rows; neither sees the other's.

5.2 public_events — the canonical event table

Every agency feed lands in public_events, the only table the application reads compliance events from; per-agency tables (dob_violations, hpd_complaints, etc.) were partial legacy artifacts and do not survive. Normalized status status_norm ∈ {OPEN, CLOSED, DISMISSED}. Every event must resolve to a building_id — one that can't is quarantined in unlinked_events with a reason, surfaced on an ops dashboard; silent nulls are a defect. Ingestion is idempotent, and every sync run is logged with counts, duration, and outcome.

5.3 Obligations

An obligation is a recurring legal duty with a deadline, generated on a schedule against a building or unit, independent of any citation: annual lead-paint notice, periodic (LL31) unit inspection, turnover-triggered work, investigation/testing duties, RPO checks, exemption monitoring visits. The obligation engine takes building/unit facts plus the jurisdiction rulebook and emits dated obligations with an applicability rationale; where a fact is unknown, it emits "possibly applicable, verify" rather than silently dropping — applicability.ts does this correctly today and that behavior is preserved.

6.3 Moved — retained as a citation anchor

Superseded — chain-of-custody / field-gate content that once lived at this number is now under Projects and v1 scope: see §7.3 (Phases) and §9.3 (Field work). Kept because source comments cite REQUIREMENTS §6.3; do not delete this heading without repointing them.


7. Projects

projects columns and constraints are documented in docs/domain/SCHEMA.md; the decisions below are what that schema must hold.

7.1 Origins

A project is created one of three ways; the origin is recorded and immutable. A manually-created project must still declare one of the three.

OriginTriggerSource
violationAn agency cited the propertya public_events row
obligationA recurring duty came duean obligations row
occupant_requestAn occupant asked for an inspectionmanual intake

7.2 Scope

A project attaches to a unit, or — when work isn't apartment-specific (common-area XRF, building-wide filings) — directly to a building. Exactly one of unit_id / building_id-only is set; building_id is always populated.

7.3 Phases (D8, amended D21)

The six phase names are kept, hardcoded, identical for every tenant — intake → scheduling → field → docs_qa → billing → closed — plus recoverable side-states (blocked / on-hold / cancelled), orthogonal to phase (setProjectSideState does not move phase).

D21 supersedes the original gate-engine plan. Rung 6 deleted domain/src/phases/gates.ts and advance-project-status; setProjectPhase writes the column from the client, nothing auto-advances, and nothing may branch on phase to enable or disable work. The workspace is Decide → Work → File → Money, always reachable. Within-phase progress is a sub-state, not a phase, and billing is decoupled from filing — commercial close does not wait on agency acceptance. The original per-boundary completeness list (signed proposal, scheduled visit, captured field data, documents, paid invoice) is a useful ops checklist, not enforced; candidate action-level warnings are in docs/history/DESIGN-C.md, unshipped.

7.4 Services (D8)

Service types — xrf, dust_wipe, paint_chip, abatement, and combinations — are defined in TypeScript, not database rows; a service definition declares required documents, licenses, and field capture contract. Staff don't declare services on a project — they're derived from linked HPD orders and project_order_decisions (servicesForTracks); there's no project_services table.

The Admin Workflow Studio is removed from v1, along with workflow_catalogs and every other workflow_* table.


8. The abstraction axis: agency and program (D6)

The thing that varies in this domain is not primarily the city. It is which agency issues the rule, and which body of rules you are working under. Complied's abstraction is built on that axis first. Region is a second, outer axis we leave room for but do not build in v1.

8.1 The three levels

// 1. Jurisdiction — the legal geography. v1 ships exactly one.
interface Jurisdiction { id: 'nyc'; name: string; agencies: Agency[]; }

// 2. Agency — issues citations, publishes data, receives filings. NYC has many;
// we already ingest from most of them.
interface Agency {
id: 'hpd' | 'dob' | 'dohmh' | 'fdny' | 'ecb_oath' | 'dof' | 'dep' | 'dot' | 'dsny';
name: string;
feeds: FeedDefinition[]; // ingestion
citations: CitationRegistry; // codes and what they mean
filingChannels: FilingChannel[]; // how work files back, if at all
}

// 3. Compliance program — THE unit of abstraction: a body of rules with its own
// citations, deadlines, obligations, documents, services. Usually one agency;
// sometimes several participate.
interface ComplianceProgram {
id: 'nyc-lead-paint';
jurisdiction: 'nyc';
primaryAgency: 'hpd';
participatingAgencies: ['hpd', 'dohmh'];
orderRegistry: OrderRegistry; // citation code -> required behavior
obligationRules: ObligationRule[]; // recurring duties and their clocks
applicability: ApplicabilityEngine; // does this rule apply to this building?
deadlines: DeadlineCalculator;
documents: DocumentRequirementSet; // forms, signers, notarization
services: ServiceDefinition[]; // xrf, dust_wipe, paint_chip, abatement
}

v1 ships exactly one program: nyc-lead-paint — the existing HPD rulebook, ported faithfully (order codes, deadlines, documents, signer roles, notarization, friction surfaces, RPO logic, LL31 status, exemption monitoring), plus the DOHMH side of lead (inspections, poisoning-case response), already touched via doh_inspections/doh_violation.

8.2 Why this split matters — ingestion is agency-wide, workflow is program-scoped

This is already how the system behaves; the rebuild makes it explicit. The product ingests from agencies it has no program for — DOB, ECB/OATH, FDNY, DOF, 311, ACRIS — none of it lead-paint work. Not scope creep: it's the compliance intelligence layer, showing a building's whole risk picture, not just the slice sold against. The two layers are governed differently:

LayerScopeRule
IngestionPer agencyEvery agency feed lands in public_events regardless of whether a program consumes it. Adding a feed never requires a program.
IntelligenceCross-agencySurfacing, mapping, alerting, and risk scoring read public_events across all agencies. Program-agnostic.
WorkflowPer programProjects, obligations, services, documents, and filings are always scoped to one compliance program. A project belongs to nyc-lead-paint.

8.3 Growth paths, and what each costs

Want to addYou writeYou must not touch
A new agency's data feed (e.g. DEP water)A FeedDefinition + ingestion adapterAnything else
A new program under an existing agency (e.g. DOB façade / LL11)A ComplianceProgram moduleProjects, phases, documents engine, schema
A new program spanning agenciesSame, listing multiple participatingAgenciesSame
A new city (LA, Chicago)A Jurisdiction + its agencies + its programsSame

8.4 Binding rules

  • One rulebook per program, single source. orderRegistry is canonical; adapters and validation derive from it, never re-hardcoding an order's behavior elsewhere.
  • The core never names an agency. No if (agency === 'hpd') outside a program module; NYC-specific vocabulary (BIN, BBL, HPD order numbers) lives in agency-scoped columns or JSON, never in core columns.
  • Programs are code, not configuration (D8) — a typed TypeScript module, versioned and tested.
  • Fidelity gate. The ported nyc-lead-paint rulebook must reproduce the old implementation's output case-for-case before the old code is deleted.
  • Every project records its program. projects.program_id is non-nullable, even though v1 has only one value.

9. v1 scope

All four module groups are in scope for v1 as intent. Status below is a snapshot; the living record is docs/LIFECYCLE_ASSESSMENT.md and docs/domain/CUTOVER.md.

9.1 Core — buildings, units, projects, workspace

Citywide registry with PLUTO/BIN resolution · tenant building tracking · client management · the three project origins · Decide/Work/File/Money detail · scheduling per inspection · audit trail on phase-label changes. Shipped, with holes (units create UI, post-create event linking on obligation/occupant-request origins) — detail in CUTOVER.md.

9.2 Compliance intelligence

Agency feed ingestion into public_events with guaranteed linkage · violation surfacing · citywide map · tracked-building alerts/digest email · saved views · ingestion ops dashboard. Layer 1/2 ingest, /map, /violations shipped; obligation engine, cron, ops dashboard, and alert rules are not — detail in CUTOVER.md.

9.3 Field work

Inspection scheduling/access tracking · XRF (instruments, calibration, component groups, room requirements, readings, CSV import, findings, report gen/versioning) · dust wipe/paint chip/abatement sampling · lab partners and chain of custody · floor plans and room presets · online-only field capture (OPEN-6 resolved 2026-08-21: no IndexedDB queue-and-sync — every inspection-wizard read/write requires connectivity). /field + XRF CSV → report shipped; non-XRF reports, CoC, lab-result entry, and admin are not wired in UI.

9.4 Documents, portal, and money

Document templates/validation · affidavit/proposal generation · paperwork bundles, signature and notary tracking · HPD filing package assembly · invoicing, rate cards, line items, vendor payments · white-label client portal (per-tenant/per-client branding) · outbound email with delivery state, suppression, unsubscribe. Generators, portal, branding, and email pipeline shipped; filing package has no merged PDF, empty rate cards yield $0, and email stops at queued_unsent without RESEND_API_KEY.

9.5 Explicitly out of scope for v1

Subscriptions/plans/entitlements/Stripe (D9) · the admin workflow studio (D8) · any jurisdiction other than NYC (D6) · the firms layer (D11) · a native mobile app (field work is responsive web) · Airtable migration tooling · every legacy per-agency violation table · a phase-gate engine keyed on projects.phase (D21).


10. Non-functional requirements

AreaRequirement
SecurityRLS on every table. Tenant isolation proven by an automated cross-tenant leakage suite. No service-role key ever reaches the browser. Edge functions that read client PII or financials gate on resource ownership, not merely on a valid JWT.
AuditabilityAppend-only audit log for state transitions, document generation, impersonation, and cross-tenant collaborator actions. Compliance work must be defensible years later.
White-labelPer-tenant branding — logo, palette, sender identity, document letterhead, portal domain — resolved at request time. No Complied branding leaks into a tenant surface.
Data integrityForeign keys enforced. No orphan tables. No nullable tenant_id on tenant-owned data. No nullable building_id on events.
TestingVitest for domain logic — rulebook, deadlines, applicability, pricing. Playwright for the critical paths. A feature is not done until it has been exercised end-to-end against real data. An empty table means untested, not finished.
MigrationsOne consolidated, reviewable baseline. Every subsequent migration forward-only and reversible in intent. No migration ships without its RLS policies.
PerformanceCitywide map and event lists must stay responsive over millions of rows — server-side pagination, indexed filters, no client-side full-table loads.

10.1 Backend architecture and the migration seam (D15)

v1 has no API service. The browser talks to Postgres through PostgREST and RPC, RLS as the enforcement layer — a deliberate MVP trade, since both launch tenants (SecureEnv, Abated) are internally owned and shipping a testable product matters more right now than architectural purity.

That trade expires. The Lovable-era app had 824 .from() calls across 106 tables scattered through the UI — the pattern that makes a backend migration expensive, and the thing not being repeated. These seams are mandatory in v1:

  1. No .from() or .rpc() outside src/data/ and src/auth/. One typed repository module per table, intention-revealing functions (listOpenViolationsForBuilding), never raw query builders; components call repositories, never Supabase. src/auth/ identity lookups are the documented exception. Enforced by an ESLint rule, not convention.
  2. Domain logic is pure TypeScript in domain/ (@complied/domain) — rules, deadlines, applicability, pricing, XRF determination; no I/O, no Supabase import, runnable in Node. Moves to a server unchanged when the backend arrives. Not src/domain/.
  3. Database functions are reserved for two jobs: atomic operations (invoice number allocation) and non-client-forgeable ones (tenant resolution, permission checks). No business logic in PL/pgSQL — the old system's 140 SECURITY DEFINER functions are a warning, not a template.
  4. Every write path is a named function. No component builds an update payload inline.
  5. Auth is accessed through one module. A single src/auth/ wrapper over Supabase Auth; nothing else imports supabase.auth. 878 RLS references to auth.uid() make this the deepest Supabase hook and the one most worth isolating.

Hold those five and the NestJS migration becomes "reimplement src/data/, move domain/, point the client at a new base URL." Break them and it's a rewrite of hundreds of call sites.

Migration trigger (not scheduled by date — whichever comes first): the first external tenant onboards (cross-tenant correctness stops being internal, RLS-only is no longer acceptable as the sole point of failure); cross-tenant collaboration (D4) ships past two tenants (the grant-scoped RLS is the hardest in the system, doesn't scale safely by hand); or ingestion reliability becomes a recurring incident (cron-plus-edge-function has no retry, dead-letter, or run observability — a job queue is the fix, and it needs a server). Until one fires, Supabase-only stands and is not to be relitigated.


11. Execution plan

Complete. Phases 0–8 and Phase 9's file-promotion step finished 2026-08-22; hosting went live 2026-08-26. Full phase-by-phase history: docs/history/CHANGE-LOG.md. Remaining work is the CUTOVER gap list — see docs/domain/CUTOVER.md — not a rebuild phase.


12. Open questions

None open. All resolved 2026-08-20 through 2026-09-02 — see D16–D21 in §2, including OPEN-6 (offline field capture: online-only for v1) and D21 (phase is not a capability, 2026-08-31 Rung 6 owner ruling).


13. Change log

Rebuild phases are complete and hosting is live; this section is no longer active-decision content. Full phase-by-phase history: docs/history/CHANGE-LOG.md.

WhereWhat it is
This file §2–§8, §10Locked decisions and architecture intent
docs/domain/What the running system is
docs/LIFECYCLE_ASSESSMENT.mdCan we run a real project?
docs/domain/CUTOVER.mdProduction / CARRY gaps