Verified end to end on 2026-09-07 from a fresh clone: npm ci 44 s for 661
packages, npm run verify green in 73 s, npm run build green in 103 s, and
next start serving /login with no database running at all.
Node 22.22.2+, or 24.15.0+ on the 24.x line. Node 20 fails. The floor is not
Next’s (it takes 20.9) — it comes from jsdom and undici in the lockfile, and
it is declared nowhere but .nvmrc, which names the 22.22.2 that production
itself builds on.
nvm use # .nvmrc
cp .env.example .env # then edit it: BETTER_AUTH_SECRET ships empty and
# DATABASE_URL ships CHANGE_ME, so a verbatim copy cannot boot
npm ci
npm run db:generate # gitignored client, no postinstall — typecheck fails without it
npm run verify # needs no database
npm run build # needs no database
Only the database commands need PostgreSQL. Use 16, the version
.do/app.yaml pins for production; the SQL’s own floor is 12.
initdb -D ~/pg-sinolife -A trust -U "$USER"
pg_ctl -D ~/pg-sinolife -o "-p 5433 -c listen_addresses=127.0.0.1" start
createdb -h 127.0.0.1 -p 5433 sinolife
npm run db:deploy && npm run db:seed && npm run db:seed:users
npm run db:check # 11 of 11 pass on a freshly seeded demo database
A db:check failure on a database that has been sitting around is a stale
database, not a broken invariant — the demo seed is clean on a fresh migrate.
npm run verify # typecheck && lint && test
npm run build
npm run db:check # 11 data-integrity invariants
All three must be green before a phase is called done. Not “should work” — run them.
Currently: 928 tests, 0 lint errors, 0 type errors — measured 2026-09-07 on a fresh clone.
A count in prose goes stale the moment somebody adds a test, so treat it as a
floor rather than a fact: npm run verify is what actually answers the
question. This line said 285, then 628, for long enough each time that it
stopped being read.
Money. Never number for an amount. BigInt minor units, field name ends
in Minor, converted to a display value only at the API edge.
Undefined results. Return null or a tagged union, never a
plausible-looking fake. No +Infinity%, no 0% standing in for “nothing
resolved yet”. null (no data) and 0 (a real measurement) must stay
distinguishable all the way to the UI.
Dates. All boundaries come from src/server/domain/period. Never construct
a period inline; never call new Date() inside a pure function — inject it, so
tests are deterministic.
Layer boundaries. Enforced by ESLint. If a rule blocks an import, the fix is almost never to disable the rule.
DATA_SOURCE. Read only in providerFactory.ts. Anywhere else, depend on
CrmProvider.capabilities or take the provider as a parameter. Lint enforces
this — it is what keeps the Bitrix24 cutover from becoming a rewrite.
Authorisation. Every permission decision goes through
src/server/auth/rbac.ts. A check spelled user.role === 'ADMIN' anywhere
else is how permissions drift. Route handlers spread ctx.scope after the
parsed query so a caller cannot widen their own scope.
Secrets. Never in a client component, never in a log, never in an API
response, never committed. .env.example carries names and comments only.
tests/domain/ money, metrics, period, sales, performance, finance
tests/integrations/ demo provider, sync engine
tests/http/ envelope, errors, query validation, scope composition
tests/auth/ the RBAC matrix
The domain layer is pure, so it needs no database and no mocks. The sync engine takes its persistence through an interface, so idempotence, incremental watermarks, failure isolation and deletion sweeps are all tested with in-memory fakes.
Write the test that would have caught the bug, not the test that restates the implementation. The valuable ones here are the edge cases: zero baselines, empty collections, month boundaries, February, and scope escalation.
| Phase | State |
|---|---|
| 1–2. Audit, architecture | Done |
| 3. Database | Done |
| 4. Backend API | Done |
| 5. Demo provider | Done |
| 6. Analytics engine | Done |
| 7–10. Dashboard, employees, KPI, deals | Done |
| 11. Auth / RBAC | Done |
| 12–14. Tests, performance, polish | Done for current scope |
| 15. Bitrix24 integration | Live — mapping confirmed, sync worker in production |
Not built: reports/export page, admin sync screen.
These were all found by running things rather than reasoning about them, and each is now covered by a test or a check.
calendarMonth() plus containment of the
period’s as-of instant.db:check.issuer column the schema lacked.cookiePrefix.ts.Secure cookies broke local production. Keyed off NODE_ENV instead of
the URL scheme.DATABASE_URL still contains the placeholder password — edit .env. The
check is deliberate.
PrismaClient needs non-empty options — Prisma 7 requires a driver
adapter. Use the singleton in src/server/db/prisma.ts.
Unknown argument from Prisma after a schema change — run
npx prisma generate. A running dev server also holds the old client; restart
it.
Redirected to /login while signed in — the cookie prefix in
cookiePrefix.ts and the auth config have diverged, or BETTER_AUTH_URL says
https while you are serving http.
useSearchParams() should be wrapped in a suspense boundary — the page is
being statically prerendered. Add export const dynamic = 'force-dynamic';
every authenticated page has it.
Cannot find name 'LayoutProps' — Next’s generated route types are
missing. Run npm run typecheck, which runs next typegen first.
BigInt literal errors — tsconfig targets ES2022. If they reappear after a
config change, delete tsconfig.tsbuildinfo; the incremental cache goes stale.
Prisma CLI vulnerability in npm audit — a stack-exhaustion issue in
deepmerge-ts, reachable only through the Prisma CLI config loader, not at
runtime. audit fix --force downgrades Prisma. Left as is, tracked.