Complied — decisions and glossary
The locked product decisions (D1–D21), the vocabulary, the tenancy and access model, the domain
model, the agency/program abstraction, and the non-functional requirements. Where this document
and any other file disagree on intent, this document wins. What the running system does is
described in docs/product/overview.md; the HPD legal rules are in
docs/hpd/rulebook.md and docs/hpd/system-model.md.
Section numbers are cited from code. Source comments in
src/,domain/,supabase/and the migrations cite this file asREQUIREMENTS §n.m. Some subsections carry a number that differs from their parent (§5's subsections are numbered4.x, §6's are5.x); the citations depend on those numbers, so they stay as they are. Add new subsections at the end of a section, and never renumber an existing one.
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 — Secure Environmental Group, Abated NYC, and any others — 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 (schedules it and captures it in the field — XRF, dust wipe, paint chip, abatement), produces the paperwork, and proves it with a defensible, auditable record.
2. Decisions
These are settled. Do not relitigate them without an explicit change to this file.
| # | Decision | Choice |
|---|---|---|
| D1 | Database | One Supabase project, with the schema designed around tenancy. Migrations are forward-only and live in supabase/migrations/. |
| D2 | Frontend | React + Vite + TypeScript + shadcn-ui. Compliance logic lives outside the UI, in domain/. |
| D3 | Tenant isolation | Shared tables + tenant_id + Postgres RLS. Every tenant-owned row carries tenant_id; isolation is enforced in the database, not the client. |
| D4 | Cross-tenant work | Per-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. |
| D5 | Identity | One user belongs to exactly one tenant. Clients (building owners and managing agents) log into a separate white-label portal as a distinct account type. Complied platform operators sit above all tenants. |
| D6 | Abstraction axis | Agency and compliance program, within NYC. The core is neutral across NYC agencies (HPD, DOB, DOHMH, FDNY, ECB/OATH, DOF, DEP). One program is implemented: NYC Lead Paint, owned by HPD with DOHMH participating. Region/jurisdiction is a second, outer axis that is reserved and not built. See §8. |
| D7 | Customer hierarchy | Tenant → Client → Building → Unit → Project. |
| D8 | Service and phase definition | Defined in code, not in the database. There is no admin workflow studio; services and programs are typed TypeScript modules. |
| D9 | Billing | No subscription billing. Every tenant is unlimited: no plans, no entitlements, no Stripe. |
| D10 | Buildings | A citywide public reference registry plus a tenant-scoped tracking layer. One row per real building, shared; tenant relationships are recorded separately. |
| D11 | The firms concept | There are no firms inside a tenant. The tenant is the legal entity, so there is no firms table and no firm_id column. |
| D12 | Archive | archive/ holds the earlier application, for reference only. It is not built, linted or shipped, and nothing builds against it. |
| D13 | Behavior is deliberate | A compliance rule's outcome is never a side effect of a refactor. Deadlines, applicability, required documents, signer roles, notarization, order-code behavior, pricing and XRF determinations are pinned by tests in domain/; changing one is a product decision recorded here or in the rulebook. See §4. |
| D14 | Vocabulary | tenant = the white-label organization, always. The resident is an occupant. |
| D15 | Backend | Supabase-only. No separate API service: the browser talks to PostgREST/RPC 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. |
| D16 | White-label routing | One subdomain per tenant (for example secureenv.<domain>, abated.<domain>). The hostname identifies the tenant and a sign-in on another tenant's subdomain is refused; it never authorizes anything. Custom domains are not supported. |
| D17 | Reference data | Public and reference data is derived from agency feeds, never copied between environments (buildings, units, public_events, PLUTO, and so on). Only tenant-authored data — clients, projects, documents, invoices, XRF history, profiles — is carried over. |
| D18 | Inspector tenancy | One inspector, one tenant. An inspector profile belongs to exactly one tenant and never works across two. D5 stands; there is no multi-tenant JWT model. |
| D19 | Agency feeds | Every active agency sync stays. They are the compliance-intelligence layer (§9.2). One orchestrator runs them, and they write only to public_events; there are no per-agency mirror tables. |
| D20 | Edge function pruning | A function removed from the repository is deleted from live Supabase, not left deployed. |
| D21 | Phase as label | Phase is never a capability. Nothing in the app may read projects.phase to decide what a user can do. Phase is a manually set operations label. Any future check gates an action on data facts, never on the phase value. See docs/architecture/overview.md. |
3. Glossary and vocabulary
The word tenant is overloaded in this domain and causes bugs when left ambiguous.
| Term | Meaning in this system |
|---|---|
| Tenant | A white-label customer organization of Complied (Secure Environmental Group, Abated NYC). The unit of data isolation. |
| Occupant | A person living in a residential unit. This is what NYC law calls a "tenant." |
| Client | A customer of a tenant — a property management company or building owner. |
| Platform operator | A Complied employee with cross-tenant access. |
| Building | A physical structure, keyed by BIN, in the citywide registry. |
| Unit | An apartment within a building. |
| Project | A unit of billable compliance work, attached to a unit or directly to a building. |
| Violation | A reactive compliance event: an agency has cited the property. |
| Obligation | A proactive compliance event: a recurring legal duty with a running clock, independent of any citation. |
| Event | Anything landing in public_events from an agency feed — the raw material both violations and obligations are derived from. |
| Document level | What a document is attached to: platform_internal, platform_to_tenant, platform_published, tenant, client, building, unit, project, or user (a document about a person, tenant-owned). Exactly one anchor per level. |
| Document type | An entry in the document_types catalog — category, access class, allowed levels, signer/notary requirement, reuse policy. Mirrored in domain/src/docs/documentTypes.ts. |
tenant is reserved exclusively for the white-label organization (D14). The third project
origin is occupant_request, and the access outcome for a resident who refuses entry is
occupant_refused.
4. Stability of behavior (D13)
The behaviors a compliance firm relies on are fixed by tests, not by convention.
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 changes only as a product decision
Any observable behavior: a compliance rule's outcome (deadlines, applicability, required documents, signer roles, notarization, order-code behavior); a data feed's cadence and coverage; a document or report the system generates; a calculation (pricing, deadlines, XRF pass/fail, exemption status, RPO determination); a workflow a user can complete end to end.
4.3 How it is enforced
- Rules have fidelity tests. The rulebook's per-order behavior, the deadline calculator and
the document-selection logic are reproduced case by case in
domain/tests/(seedomain/tests/fidelity/), so a change to an outcome fails a test before it ships. - A feature is done when it works against real data, not when the code compiles (§10).
4.4 The risk to watch
The least visible area is 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 the
caller's profile.
Three data classes, three policy shapes:
| Class | Examples | Policy |
|---|---|---|
| Public reference | buildings, units, public_events, PLUTO lots, agency lookup tables, HPD order maps | Readable by all authenticated users. Written only by service-role sync jobs. USING (true) is acceptable here and nowhere else. |
| Tenant-owned | clients, projects, inspections, documents, invoices, profiles | tenant_id = current_tenant_id() and the area's permission key (has_permission), plus the project-collaboration exception below. Tenant membership alone opens nothing — see docs/architecture/permissions.md. |
| Platform | tenants, platform_operators, impersonation_audit, sync run logs | Platform 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 (db-tests/), not by inspection.
4.2 Roles
Scoped to a tenant unless stated otherwise. The tenant roles below are presets, not a fixed
enum: a role is a tenant-owned set of permission keys that an admin can rename, re-grant or add
to, and no policy reads a role's name. The keys, the presets' defaults and the field-job
assignment rule ("on assigned projects" below) are in
docs/architecture/permissions.md.
| Role | Scope | Capability |
|---|---|---|
platform_operator | Global | Cross-tenant read, tenant provisioning, audited impersonation |
tenant_admin | Tenant | Everything within the tenant, including user and branding management |
project_manager | Tenant | Create and run projects, assign work, approve documents |
inspector | Tenant | Field execution on assigned projects; XRF, samples, photos, notes |
back_office | Tenant | Documents, filing packages, invoicing |
client_user | Client | Read-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
(field_execution, abatement, clearance_sampling, lab_coordination); the collaborator
sees only that project, and within it only the surfaces its role covers. Financials are
never shared — proposals, rate cards, invoices and vendor payments stay visible to the owner
tenant alone, regardless of role. Every collaborator action is attributed to
(user_id, acting_tenant_id). 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 in docs/architecture/data-model.md;
what follows is the decisions the schema encodes, not a restatement of it.
5.1 Buildings and units (D10)
buildings is a citywide public registry keyed by BIN, seeded and 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 or 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.
Staff do not create rows in units, because the table is shared: one tenant's typo would
appear in every other tenant's view of the building. Apartments enter the registry from agency
data, and the app never invents an apartment label — HPD cites real ones (3B, PH, BASEM),
and an invented label would never match a citation.
5.2 public_events — the canonical event table
Every agency feed lands in public_events, the only table the application reads compliance
events from. Normalized status is status_norm ∈ {OPEN, CLOSED, DISMISSED}. Every event
resolves to a building_id: one that cannot is quarantined in unlinked_events with a
reason, and a silent null is 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, evaluated against a building or
unit independent of any citation: the annual lead-paint notice cycle, the one-time Local Law 31
XRF test, exemption monitoring visits and risk assessments, turnover-triggered work,
investigation duties. Needs are computed on read: the database stores tenant-scoped facts
about each unit, the pure rules in domain/ compute what each unit needs next, and the only
materialized rows are obligations ledger entries written when a person acts on a need. Where
a fact is unknown, the engine reports it as unknown rather than assuming an answer — for
example child_under_6_present is nullable with no default, because a default of false would
claim "no child under six" for every unit no one has asked about.
6.3 Field completion
A field visit is complete when its service's capture contract is met
(domain/src/field/serviceDefinitions.ts, enforced by fieldJobReadiness): rooms, instrument,
captured data, chain of custody and floor plan as each service requires. The gate is evaluated
again against the database at the moment of completion. Chain-of-custody and field-gate content
is otherwise under §7.3 (Phases) and §9.3 (Field work).
7. Projects
projects columns and constraints are in
docs/architecture/data-model.md; the decisions below are
what that schema holds.
7.1 Origins
A project is created one of three ways; the origin is recorded and immutable.
| Origin | Trigger | Source |
|---|---|---|
violation | An agency cited the property | one or more public_events rows |
obligation | A recurring duty came due | an obligations ledger row, when started from a need on Deadlines |
occupant_request | An occupant asked for an inspection | manual intake |
7.2 Scope
A project attaches to a unit, or — when work is not apartment-specific (common-area XRF,
building-wide filings) — directly to a building. building_id is always populated; unit_id
is set only for apartment-scoped work.
7.3 Phases (D8, D21)
The six phase names are hardcoded and identical for every tenant — intake → scheduling → field → docs_qa → billing → closed — plus recoverable side-states (blocked, on hold,
cancelled) that are orthogonal to phase (setProjectSideState does not move phase).
Phase is a label. setProjectPhase writes the column from the client (the advance_projects
permission is enforced by a trigger, and every change is logged to project_phase_transitions),
nothing auto-advances, and nothing may branch on phase to enable or disable work. The
workspace is Decide → Work → File → Money, and all four stay reachable at any phase. Progress
inside a phase is a sub-state, not a phase, and billing is decoupled from filing — commercial
close does not wait for agency acceptance. The completeness list (signed proposal, scheduled
visit, captured field data, documents, paid invoice) is an operations checklist, not
enforced.
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 the field capture contract. Staff do not declare services on a project: they are
derived from the linked HPD orders and their recorded decisions (servicesForTracks), so
there is no project_services table. There is no workflow studio and no workflow_* tables.
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 that is left room for and not built.
8.1 The three levels
// 1. Jurisdiction — the legal geography. One is implemented.
interface Jurisdiction { id: 'nyc'; name: string; agencies: Agency[]; }
// 2. Agency — issues citations, publishes data, receives filings. NYC has many;
// the platform ingests 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
}
The types live in domain/src/jurisdictions/types.ts.
One program is implemented: nyc-lead-paint — the HPD rulebook (order codes, deadlines,
documents, signer roles, notarization, RPO logic, Local Law 31 status, exemption monitoring),
in domain/src/programs/nyc-lead-paint/. applicability
is not populated: the obligation rules are a typed description of the families that ship, and
their logic is ordinary TypeScript in the program's directory.
8.2 Why this split matters — ingestion is agency-wide, workflow is program-scoped
The product ingests from agencies it has no program for — DOB, ECB/OATH, DOF, 311 — none of it lead-paint work. This is the compliance intelligence layer: it shows a building's whole risk picture, not just the slice sold against. The layers are governed differently:
| Layer | Scope | Rule |
|---|---|---|
| Ingestion | Per agency | Every agency feed lands in public_events regardless of whether a program consumes it. Adding a feed never requires a program. |
| Intelligence | Cross-agency | Surfacing, mapping, alerting and risk scoring read public_events across all agencies. Program-agnostic. |
| Workflow | Per program | Projects, 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 add | You write | You must not touch |
|---|---|---|
| A new agency's data feed (e.g. DEP water) | A FeedDefinition + ingestion adapter | Anything else |
| A new program under an existing agency (e.g. DOB façade / LL11) | A ComplianceProgram module | Projects, phases, documents engine, schema |
| A new program spanning agencies | Same, listing multiple participatingAgencies | Same |
| A new city (LA, Chicago) | A Jurisdiction + its agencies + its programs | Same |
8.4 Binding rules
- One rulebook per program, single source.
orderRegistryis 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.
- Rulebook fidelity. The
nyc-lead-paintrulebook's outputs are pinned case by case indomain/tests/fidelity/; an outcome changes only when the test and the rulebook change together. - Every project records its program.
projects.program_idis non-nullable, even though one program exists.
9. Platform scope
What the platform covers, grouped by module. Each module's current behavior is described in
docs/product/overview.md and the operator guides under
docs/operator/.
9.1 Core — buildings, units, projects, workspace
The citywide registry with PLUTO/BIN resolution · tenant building tracking · client management · the three project origins · the Decide / Work / File / Money workspace · scheduling per field visit · an audit trail of phase-label changes.
9.2 Compliance intelligence
Agency feed ingestion into public_events with guaranteed building linkage · the violations
explorer · the citywide map · the Deadlines worklist for obligations · saved views · a daily
digest function for tracked buildings.
9.3 Field work
Visit dispatch (outreach, access, scheduling, assignment) · XRF (instruments, calibration, component groups, room requirements, readings, CSV import, findings, report generation and versioning) · dust-wipe, paint-chip and abatement capture · lab partners and chain of custody · floor plans and room presets · online-only capture: every field read and write requires connectivity.
9.4 Documents, portal, and money
Document types, levels, sharing and versions · generated HPD response documents · filing package assembly · proposals with rate cards, invoices and vendor payments · the white-label client portal (per-tenant and per-client branding) · outbound email with delivery state, suppression and unsubscribe.
9.5 Outside the platform
Subscriptions, plans, entitlements and Stripe (D9) · an admin workflow studio (D8) · any
jurisdiction other than NYC (D6) · a firms layer (D11) · a native mobile app (field work is
responsive web) · offline field capture · e-signature or notary integrations (signing and
notarizing record a name and a time on the document) · submitting filings to HPD (staff mail or
upload the generated documents) · a phase-gate engine keyed on projects.phase (D21).
10. Non-functional requirements
| Area | Requirement |
|---|---|
| Security | RLS 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. |
| Auditability | Append-only audit log for state transitions, document generation, impersonation and cross-tenant collaborator actions. Compliance work must be defensible years later. |
| White-label | Per-tenant branding — logo, palette, sender identity, document letterhead — resolved at request time. No Complied branding leaks into a tenant surface. |
| Data integrity | Foreign keys enforced. No orphan tables. No nullable tenant_id on tenant-owned data. No nullable building_id on events. |
| Testing | Vitest for domain logic — rulebook, deadlines, obligations, pricing. A feature is not done until it has been exercised end to end against real data. An empty table means untested, not finished. |
| Migrations | Forward-only and reviewable. No migration ships without its RLS policies. |
| Performance | The citywide map and event lists 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)
There is no API service. The browser talks to Postgres through PostgREST and RPC, with RLS as the enforcement layer — a deliberate trade, since both launch tenants (Secure Environmental Group and Abated NYC) are internally owned and a testable product matters more than architectural purity.
That trade expires. Database calls scattered through UI components are the pattern that makes a backend migration expensive, so these seams are mandatory:
- No
.from()or.rpc()outsidesrc/data/andsrc/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. - Domain logic is pure TypeScript in
domain/(@complied/domain) — rules, deadlines, obligations, pricing, XRF determination; no I/O, no Supabase import, runnable in Node. It moves to a server unchanged when the backend arrives. Notsrc/domain/. - 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.
- Every write path is a named function. No component builds an update payload inline.
- Auth is accessed through one module. A single
src/auth/wrapper over Supabase Auth; nothing else importssupabase.auth. RLS policies referenceauth.uid()throughout, which makes it 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 is a rewrite of every call site.
Migration trigger (not scheduled by date — whichever comes first): the first external tenant onboards (cross-tenant correctness stops being internal, and RLS-only is no longer acceptable as the sole point of failure); cross-tenant collaboration (D4) is used past two tenants (the grant-scoped RLS is the hardest in the system and does not 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.