Skip to main content

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 as REQUIREMENTS §n.m. Some subsections carry a number that differs from their parent (§5's subsections are numbered 4.x, §6's are 5.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.

#DecisionChoice
D1DatabaseOne Supabase project, with the schema designed around tenancy. Migrations are forward-only and live in supabase/migrations/.
D2FrontendReact + Vite + TypeScript + shadcn-ui. Compliance logic lives outside the UI, in domain/.
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 and 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). 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.
D7Customer hierarchyTenant → Client → Building → Unit → Project.
D8Service and phase definitionDefined in code, not in the database. There is no admin workflow studio; services and programs are typed TypeScript modules.
D9BillingNo subscription billing. Every tenant is unlimited: no plans, no entitlements, no Stripe.
D10BuildingsA citywide public reference registry plus a tenant-scoped tracking layer. One row per real building, shared; tenant relationships are recorded separately.
D11The firms conceptThere are no firms inside a tenant. The tenant is the legal entity, so there is no firms table and no firm_id column.
D12Archivearchive/ holds the earlier application, for reference only. It is not built, linted or shipped, and nothing builds against it.
D13Behavior is deliberateA 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.
D14Vocabularytenant = the white-label organization, always. The resident is an occupant.
D15BackendSupabase-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.
D16White-label routingOne 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.
D17Reference dataPublic 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.
D18Inspector tenancyOne 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.
D19Agency feedsEvery 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.
D20Edge function pruningA function removed from the repository is deleted from live Supabase, not left deployed.
D21Phase as labelPhase 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.

TermMeaning in this system
TenantA white-label customer organization of Complied (Secure Environmental Group, Abated NYC). 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.
Document levelWhat 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 typeAn 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​

  1. 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/ (see domain/tests/fidelity/), so a change to an outcome fails a test before it ships.
  2. 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:

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() and the area's permission key (has_permission), plus the project-collaboration exception below. Tenant membership alone opens nothing — see docs/architecture/permissions.md.
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 (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.

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 (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.

OriginTriggerSource
violationAn agency cited the propertyone or more public_events rows
obligationA recurring duty came duean obligations ledger row, when started from a need on Deadlines
occupant_requestAn occupant asked for an inspectionmanual 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:

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.
  • Rulebook fidelity. The nyc-lead-paint rulebook's outputs are pinned case by case in domain/tests/fidelity/; an outcome changes only when the test and the rulebook change together.
  • Every project records its program. projects.program_id is 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​

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 — 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, 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.
MigrationsForward-only and reviewable. No migration ships without its RLS policies.
PerformanceThe 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:

  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, obligations, pricing, XRF determination; no I/O, no Supabase import, runnable in Node. It 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.
  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. RLS policies reference auth.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.