SCOPE: safety platform POC
A proof of concept for a safety and risk management platform, demonstrated on a waste-services operator: eight hauling depots, a landfill and a recycling center. Drivers file vehicle inspections (DVIRs) and incident reports on shared tablets, offline when there is no signal; supervisors, general managers and a VP work investigations, corrective actions and training from role-scoped dashboards.
Two things live here: a synthetic data set (about 840,000 rows over 24 months, deliberately planted storylines, all people fictional) and the app that captures and reports against it.
"Today" is 2026-09-28, the demo date. Every overdue, expiring and due-today flag is computed against it, never against the real clock. Four places carry the date and must agree:
scripts/window.py,app.today()in SQL,TODAYinpackages/core/src/config.ts, andFROM/TOinpackages/core/src/cofi/facts.ts.
Layout
apps/web Next.js 16 (App Router): every screen, tablet-first, installable PWA
packages/core business rules and types. No React, no DOM, no database client
packages/db data access: Supabase queries and RPC wrappers
supabase/ migrations (102) and local config
data/csv the generated sample data set (32 CSVs). Never hand-edit
data/policy the client rule book, extracted into CSVs
scripts/ data generator and its verification suites
scripts/db load, test and demo-setup scripts for a live database (see its README)
scripts/cofi the COFI reporting layer generator and checks
scripts/brand binds one client's branding into a build
tenants/ per-client branding, one folder per client (see its README)
vendor/ a Power BI project kept as a reference (see its README)
docs/ working documents; docs/archive is superseded, docs/internal is ours
Architecture in one paragraph
The browser talks to Supabase (Postgres, Auth, Storage, PostgREST). Row-level
security on every table scopes each persona to their own records, their site, or
their sites; reporting functions run as the caller, so a filter in the UI can
only narrow what the database already allowed. Writes that must be atomic or must
not trust the device (capture, sign-off, assignment) go through Postgres
functions, never Server Actions, so the same calls work from a future native app.
Business rules live in packages/core and are shared by the UI and the offline
DVIR, which queues captures in an IndexedDB outbox and uploads them when signal
returns. CLAUDE.md holds the full set of decisions and the constraints that
produce wrong output when violated.
In the repo but not in the product
- Ask (
/ask,packages/core/src/ask, theask_logtable): a future feature. A question box over the safety data that answers from a fixed catalog of tested queries, with no model. Built and tested, switched off: nothing links to it and its routes redirect home. - Leads (
/hub/leads, thesalesschema): an internal tool for the team selling SCOPE. Not part of the product; reachable only from the internal hub. - Demo machinery (the persona rail, the demo strip,
/hub, the demo reset and itsdemo_baselineschema): what runs the demo. Not part of a client deployment's product.
Running the app
npm install
npm run dev
npm test # every unit suite; also run by CI on each push
Getting started also means the local checks, once per clone (brew install pre-commit):
pre-commit install --allow-missing-config
On commit, in seconds: file hygiene, Ruff, Gitleaks, ESLint, and the project
guards (no hand-edited data/csv/, the four demo-date anchors agree, a new table
enables RLS and its audit columns, no two migrations share a version). The push
stage (type-check, unit suites, data suites) is opt-in, since CI runs it anyway:
pre-commit install --hook-type pre-push. CI runs the same file and is the one
that counts; what runs where, and why, is docs/precommit-and-ci.md.
Environment: .env.local at the repo root (used by the scripts) and
apps/web/.env.local (used by the browser). Neither is committed.
Root .env.local |
|
|---|---|
SUPABASE_DB_URL |
Postgres connection string, percent-encoded |
SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY |
|
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY |
mirrored into apps/web/.env.local |
Verifying the data
None of these need a database. All four must report zero failures, and a generator change is not finished until they do and two runs are byte-identical.
python3 scripts/generate_sample_data.py # regenerate; deterministic, seeded from the anchor date
python3 scripts/audit.py # foreign keys, logic checks, realism benchmarks
python3 scripts/verify.py # file hygiene, vocabulary, uniqueness, org structure
python3 scripts/coherence.py # narrative coherence; prints whole records
python3 scripts/precheck_ddl.py # every DDL constraint asserted against the CSVs
Rebuilding the database
supabase db push --db-url "$SUPABASE_DB_URL"
scripts/db/load.sh --reset
The demo personas, their storylines and the checks that prove the rules as real
logins come after that; the full sequence is the Commands section of CLAUDE.md,
and scripts/db/README.md says what each script is for.
The database holds state the CSVs do not. Some reference tables are created
and seeded by migrations, and load.sh --reset empties several of them, which
fails open: the certification gate stops gating and says nothing. load.sh
therefore runs scripts/db/reseed_reference.sql at the end of every load. Never
assume a reloaded database is a complete one.
Scale
10 sites · 601 people · 396 assets · 1,492 incidents · 2024-10-01 → 2026-09-28. Real site names in Rutland County, VT; every person is synthetic.
See CLAUDE.md for decisions and constraints, and
data/README.md for the data dictionary.