demo-reset-audit-and-cronrun.md02_define/output/spec.mdThe demo tenant is the sales surface for the Refine the bridge initiative (Q2-2026 Objective 1 — Establish Product-Market Fit with Vendor Partners): a prospect walkthrough runs against it, so it has to open clean and its destructive operations have to be accountable. Neither holds today.
Who reset the tenant, and when, is recorded nowhere. The admin button's result lives only in
client component state — it vanishes on navigation, and a reset run from another session or device
is invisible. The cron path logs to console and persists nothing. view-as-audit cannot host the
record either: it is itself purged on every reset. That is a gap against the demo tenant's stated
hard constraint, "an audit trail of who reset and when" (audit report §1.2 item 2).
And the nightly reset never runs. apps/web/vercel.json carries only the 02:00 CSM snapshot job;
/api/cron/demo-reset ships complete and authorised but unscheduled, deliberately pending a
hand-run and a two-run diff on preview (§1.2 item 3). Both preconditions are now met — preview
carries DEMO_TENANT_CLERK_ORG_ID, so a flagged demo tenant exists there — so the tenant still
accumulates a demo's residue overnight for no remaining reason.
Record the latest reset on the tenant document (decision D9 — a tenant field, not a new collection: the purge never touches the tenant document, so the record survives the very operation it describes, with no policy classification to maintain).
resetDemoTenant writes the record itself, so the admin button and the nightly sweep produce
identical records rather than each inventing their own. The caller identifies the actor: the
server action passes the signed-in admin; the sweep passes the cron. Each record carries who,
when, the outcome, the purge and re-seed totals, and the storyline that was seeded — and, on a
failed reset, the reason instead of the totals.
The actor's display name is captured at write time, so the page renders "who" without a second lookup and the record stays readable even if that user later disappears from the tenant.
Failures are recorded only once the tenant has cleared requireDemoTenant — a guard refusal writes
nothing at all, because the tenant it refused is by definition not a demo tenant and nothing may
write to it. Recording the failure must never mask the underlying error the caller sees.
The admin demo-data settings page then renders the record server-side on load ("Last reset: …"), replacing reliance on transient client state. The existing button keeps its live per-collection breakdown for the reset the admin just ran; the persisted record is what the page shows on a cold load.
Finally, schedule the cron: {"path": "/api/cron/demo-reset", "schedule": "0 1 * * *"} in
apps/web/vercel.json — an hour before the 02:00 snapshot job, so each morning's snapshot
describes the freshly reset world (D10). The route's "Not yet scheduled" header comment is rewritten
to describe the schedule that now exists.
requireDemoTenant has passed (e.g. no Clerk-backed admin to
re-seed as, or a purge that throws) records a failure outcome with the reason, and replaces
any previous record. The caller still receives the original error unchanged; a failure to
write the record does not replace it.requireDemoTenant (tenant missing, or not flagged isDemo) writes
nothing to that tenant./admin/settings/demo-data shows the last reset — who, when, outcome — read on the server
during render, present on a cold load in a fresh browser session with no prior client state.
A tenant that has never been reset shows an explicit never-reset state, not a blank or a
placeholder date.apps/web/vercel.json carries {"path": "/api/cron/demo-reset", "schedule": "0 1 * * *"}
alongside the existing snapshot job./api/cron/demo-reset route header comment no longer says the job is unscheduled and
describes the schedule that now exists.requireDemoTenant still precedes every write on
both paths, and the new tenant fields are the only additions to what a reset writes outside
the seeder.policy.test.ts / reseed-actor.test.ts.demo-data and the storyline are untouched; the record reports the
seeder's existing result, it does not alter it.isDemo: true tenants —
there is no tenant to write to. The route's existing 500 on an empty sweep stands unchanged.CRON_SECRET) or to the route's response contract.Context budget: read the stub, _source/decisions.md (D9, D10) and _source/audit-report.md §1.2
in place of a scope.md — this epic was cut from an audit and its breakdown records that it has no
scope.md; decisions.md is its settled equivalent. Plus targeted reads of the five files the
stub cites, to fix touches: and the exact write points. Initiative/objective carried from the
stub rather than re-derived from the knowledge map.
03_build/output/notes.md5f98a34 feat: persist the reset audit trail, schedule the nightly reset ·
dc0eb5e style: collapse an import Husky did not formatpackages/services/src/db/models/tenant.ts: optional lastReset sub-document —
at, actorKind (user | cron), actorId, actorLabel, outcome, purged,
seededLeads, storylineId, error. No default, so absence is what distinguishes a
never-reset tenant from a reset with empty totals. Optional and unindexed → no migration
(checked against the mongoose-model skill's idiom: plugin trio and factory untouched, no
new index, no reserved pathname).packages/services/src/db/services/demo-reset/index.ts: resetDemoTenant takes a
ResetActor ({kind:"user",id,label} or {kind:"cron"}) and records the outcome itself.
Pure resetRecord(actor, at, detail) builds the record — the clock is the caller's, so both
outcomes are unit-covered without a DB, mirroring assertReseedActor. The purge/seed
sequence moved into a private purgeAndSeed so the try/catch wrapping it stays readable.
resetAllDemoTenants passes {kind:"cron"}.packages/services/src/utils/date.ts: formatDateTime — date alone is ambiguous for a job
that runs nightly. Added to the existing shared date home rather than as a third copy
(utils/date.ts and components/workspace/format.ts already both carry a formatDate).apps/web/app/(app)/admin/demo-data/actions.ts: passes the signed-in admin, label from the
existing displayName helper with email as fallback.apps/web/app/(app)/admin/demo-data/_components/demo-reset-summary.tsx: new server
component rendering the persisted record; explicit never-reset copy.apps/web/app/(app)/admin/settings/demo-data/page.tsx: renders it above the reset button
from the tenant the page already loads — no new query.apps/web/vercel.json + app/api/cron/demo-reset/route.ts: the 0 1 * * * entry, and the
header comment rewritten from "not yet scheduled".resetDemoTenant success path.resetAllDemoTenants passes
{kind:"cron"}; actorLabel is the shared CRON_ACTOR_LABEL, actorId absent.$set); the
original error is rethrown untouched. Building the record happens inside recordReset's
try precisely so an actor-id cast failure cannot replace it.requireDemoTenant is outside the try, so nothing in
the recording path is reachable for a tenant it refuses.$set on one sub-document, no history.vercel.json carries {"path": "/api/cron/demo-reset", "schedule": "0 1 * * *"}.requireDemoTenant still precedes every write on both paths;
lastReset is the only addition to what a reset writes outside the seeder.reset-record.test.ts (6 tests, success + post-guard failure, user +
cron), plus formatDateTime cases alongside the existing formatDate ones.format:check for a single import in demo-reset/index.ts that lint-staged would normally
have collapsed. Fixed by hand (Prettier is blocked locally by the hook). Worth knowing that
the pre-commit safety net was not actually in play for this branch, so formatting rests on CI.reset-record.test.ts and the two
date.test.ts additions are the new assertions; read them off the Quality check run.resetDemoTenant signature changed (actor is now the second positional param). Both
call sites are updated — the server action and the sweep — and there are no others; worth a
grep at review to confirm.formatDateTime renders in server-local time (UTC on Vercel). Its test constructs the
date from local components, like the neighbouring formatDate test, so it is
timezone-stable; the rendered page shows UTC rather than the admin's local time. Called
out rather than solved — a tz-aware surface is a wider decision than this run.0 1 * * * entry proves itself on the first night after merge. The
button path exercises the identical recording code on the preview.lastReset until their next reset — which is the never-reset state the page
renders.04_verify/output/verify.mdstandard) — CI Claude review is disabled
(ENABLE_CLAUDE_REVIEW unset; the "Review diff against CONVENTIONS.md" job reports skipped),
so this was run locally rather than triaged. 2 findings, both also caught by
production-readiness; both addressed.b724874: Quality green (format · lint · typecheck · tests),
Migrate preview database green, Audit database green — the last being independent
confirmation that the lastReset sub-document introduced no leanness finding.Preview: https://web-git-claude-pipeline-demo-reset-audit-cron-10aps7-sustentus.vercel.app
(deployment 35yUH2qJBaFnWxogChhYhQmMndHK, READY on 46cc80f).
Agent-verified — reachable without signing in:
GET /admin/settings/demo-data
returns 307 → /sign-in?redirect_url=%2Fadmin%2Fsettings%2Fdemo-data (agent)vercel.json carries {"path": "/api/cron/demo-reset", "schedule": "0 1 * * *"}
alongside the 02:00 snapshot job — file read on the branch tip (agent)$set of the whole lastReset sub-document, no history
collection, no $push; a failure record therefore also drops the success-only totals (agent)requireDemoTenant sits outside the recording try
(demo-reset/index.ts:288), so a tenant it refuses receives no write at all; lastReset is
the only addition to what a reset writes outside the seeder (agent, traced in diff)recordReset's try, so an actor-id cast failure
cannot replace the error the caller is about to rethrow (agent, traced in diff)resetDemoTenant(tenantId, actor, storyline?); both
call sites updated, repo-wide grep finds no third (agent)process.env reads anywhere in the diff, so turbo.json globalEnv needs no
addition; CRON_SECRET was already catalogued (agent)Operator-demonstrated — all four reported passing by Jamie, 2026-08-17:
/admin/settings/demo-data shows the last reset on a cold load in a fresh browser
session, with no client state in play (operator)notifyX call, no Ably publish, no Resend send){kind:"cron"} argument, unit-covered in reset-record.test.ts. Left unticked
deliberately — it is the one criterion nothing before merge can evidence.Walkthrough clip (#build): not recorded at the time of writing. Visibility, not a gate —
the contract is explicit that the business's approval ended at Scope.
Fixed on this branch:
date-fns format string would have been a lie off-Vercel, since format renders in the
runtime's zone. formatDateTime now formats the instant explicitly in UTC via Intl and
labels it — true in every environment, and it makes the test timezone-independent rather than
merely timezone-lucky.MAX_RESET_ERROR_LENGTH (500) at write time; the full text stays in the function logs.recordReset runs and lastReset still reads as the previous night's success.Escalated and RESOLVED — no code change needed:
resetAllDemoTenants resolves
targets from isDemo: true, set only by 1786492800000-flag-demo-tenant, which skips silently
when DEMO_TENANT_CLERK_ORG_ID is unset — and db-migrate.yaml documents that as a valid
state, so nothing in the branch could assert it. Jamie confirmed (2026-08-17) that production
holds a flagged demo tenant, so the sweep has a target and the 500-on-empty path is
unreachable in practice. The route's response contract is therefore left exactly as it was —
which is what the spec's out-of-scope line required. Note the guarantee is operational, not
structural: if the flag were ever cleared, the job goes red nightly rather than silently doing
nothing, which is the behaviour you want from a misconfiguration.Deferred to Ship (in this PR, not after):
apps/docs → technical/demo-environment still says the route is "not yet scheduled" — now
false — and does not mention tenant.lastReset. docs-sync runs at Ship.changelog-entry runs at Ship.Sent to intake, not release scope:
formatDateTime lives in apps/web/components/leads/progress-list.tsx:16 with
different output; the new shared one is the right home and the local one should collapse into it.maxDuration (or bounding the sweep) before a second demo tenant exists.Process note carried from Build: Husky's pre-commit lint-staged did not fire in this session —
the first Quality run failed on format:check for one import. Formatting on this branch rested
entirely on CI, which caught it.
05_ship/output/changelog.mdResetting the demo tenant used to leave no trace. The result appeared once, in the browser tab that ran it, and vanished on navigation — so if a colleague reset the tenant, or you reset it yesterday from another device, there was nothing to tell you.
Configure → Demo data now shows the last reset: who ran it, when, and whether it succeeded, along with how much was cleared and re-seeded. It is read fresh every time the page opens, so it is there after a reload, on another machine, and for anyone else on the tenant. A tenant nobody has reset yet says so plainly rather than showing a blank.
The reset also runs on its own at 01:00 UTC each night, so the demo world is back to the approved storyline before the working day starts instead of carrying the residue of the last walkthrough. Nightly runs are recorded the same way, listed as the scheduled job rather than a person — so a morning that opens on a clean tenant is something you can confirm rather than assume.
Times are shown in UTC and labelled, so a reset never reads as having happened at a different hour than it did.
Resetting by hand works exactly as before, and what the reset clears and keeps is unchanged.
05_ship/output/investor-update.mdWho it's for: Admins running client walkthroughs What shipped: The demo tenant resets itself nightly, and the admin page shows who last reset it and when. Why it matters: Walkthroughs start on the approved story, not yesterday's residue — Refine the Bridge, Q2-2026 Objective 1: Establish Product-Market Fit with Vendor Partners.
Before this, that was recorded nowhere.
Dig deeper: https://github.com/sustentus/sustentus/pull/813 · https://help.sustentus.com/changelog/2026-08-17-demo-reset-audit-and-cron
05_ship/output/release.mdapps/docs/app/technical/demo-environment (the "not yet scheduled" claim was
false as of this branch — replaced with the live 01:00 schedule, the single-tenant sizing
assumption, and a new Audit trail subsection) · apps/docs/app/technical/deployment (the
cron table gained the 0 1 * * * row, with a line on why the ordering against the 02:00
snapshot job is deliberate)demo tenant mentions under business/ are
snapshot management and presenter persona switching, and this run touches neither. The change is
an admin/demo-ops capability, documented under technical/.ship-note.yaml fires on the
squash and sends 05_ship/output/investor-update.md verbatimreset-record.test.ts);
not demonstrable before merge, since Vercel fires crons against production only. Proves
itself on the first nightly run after this merge.recordReset's
try precisely so a cast failure cannot mask itrequireDemoTenant is outside the recording try
(traced in Verify)$set of the whole sub-document, no history (D9)/admin/settings/demo-data shows who/when/outcome on a cold load, and an explicit
never-reset state — both demonstrated on the preview by the operator (Verify)apps/web/vercel.json carries {"path": "/api/cron/demo-reset", "schedule": "0 1 * * *"}requireDemoTenant still precedes every write on both pathsQualityThree findings from Verify were fixed on this branch rather than deferred: the timestamp is now
formatted explicitly in UTC and labelled (appending a literal "UTC" to the old format string would
have been wrong — date-fns format renders in the runtime's zone, which is UTC only on Vercel),
the stored failure reason is capped at 500 characters, and the single-tenant sizing assumption is
recorded in the route header and now in the docs.
One finding was escalated rather than fixed: resetAllDemoTenants 500s on an empty sweep, so a
production without a flagged demo tenant would go red nightly. Jamie confirmed production holds a
flagged demo tenant, so the path is unreachable and the route's response contract stays untouched —
which the spec's out-of-scope line required. The guarantee is operational, not structural: clear
that flag and the job goes red nightly rather than silently doing nothing.
Sent to intake rather than release scope: a third formatDateTime in
apps/web/components/leads/progress-list.tsx that should collapse into the shared one, and raising
the sweep's maxDuration before a second demo tenant exists.