Skip to content

HO Demo Seed

The HO tenant (T0ADQ6UAVKR) is the primary sandbox for VERA development and smoke testing. A daily reseed keeps it in a known, clean state so each session starts from a predictable baseline.

What the daily reseed does

lib/demo-seed.js runs automatically at 8:50 AM ET every weekday (10 minutes before the 9 AM morning briefing fires). It:

  1. Regenerates TSVs from generate-dummy-data/generate-v2.js (fresh random hours/budgets each run; names are stable).
  2. Acquires a Postgres advisory lock to prevent concurrent runs.
  3. Wipes all mutable tables for the HO tenant in FK-safe order: proposal_liabilitiesproposal_rolesproposals (these must go first — proposal_liabilities.vendor_user_id hard-FKs users with no ON DELETE clause, so any surviving row blocks the users wipe and rolls back the entire seed), then conversations, time_entries, budgets, pending_pto, unresolved_items, liabilities, rate_history, project_tasks, user_attribute_history, projects (children then parents), users, tasks, clients, disciplines, company_calendar. (user_attribute_history was missing from this list until issue #1860 — its user_id is a soft reference with no FK, so its rows silently survived every users wipe and accumulated, orphaned, on the demo tenant.)
  4. Re-inserts from TSVs in dependency order (disciplines → clients → tasks → users → projects → …).
  5. Re-appends the three preserved real users with collision-safe IDs.
  6. Seeds anchor data for the preserved users: Test Client, four anchor projects, time entries, envelopes, rate history, PTO requests, unresolved items, and a vendor scenario.

Scheduled by initDemoSeed(TENANTS) called at boot in index.js.

Manual reseed

DM VERA !test seed as an owner on the HO tenant. VERA replies with a summary of tables wiped and rows inserted.

If HO state gets messy mid-day, !test seed is the fastest recovery path.


Preserved real users

These three users are never wiped — their rows are read before the wipe and re-appended after.

Slack IDNameRole
U0AEJHN3MNVTom HartOwner
U0AKQ2EDWAUMTHManager
U0AJTN12DPYUTHUser

The generator produces a Tom Hart row with a fake Slack ID (U0000000001). That row is discarded during the merge; the real Tom Hart row (with the real Slack ID) takes its place.


The cast of characters

Generated users (15)

These users are produced by generate-v2.js on every reseed. Names are stable; budgets and hours vary.

IDNameRoleTypeNotes
1Tom HartOwnerFTEPlaceholder — replaced by preserved Tom Hart. Holds a 20h envelope on Meridian Health Digital Support (#1860 item 2 — previously had none, showing as a 0%-committed row on /console/capacity)
2Rachel KimOwnerFTESecond owner. Holds a 20h envelope on Harbor Logistics Platform Support (#1860 item 2, same fix as Tom Hart above)
3Sarah ChenManagerFTEPM archetype: consistent under-budget pace
4Mike JohnsonManagerFTEPM archetype: aggressive pace, often over
5Alex KimUserFTE
6Nina FosterUserFTE
7Priya PatelUserFTE
8Lena ZhaoUserFTE
9Chris TaylorUserContractor20 h/wk
10Marcus WebbUserContractor20 h/wk
11Jamie ParkUserContractor20 h/wk
12David OkonkwoUserContractor20 h/wk
13Amy WalshUserContractor20 h/wk
14Talia RodriguezUserVendorSells deliverables; logs no time
15Jordan ReedUserVendorSells deliverables; logs no time

Plus the three preserved real users (Tom Hart at real Slack ID, MTH, UTH) appended after the generated set.

Disciplines (6)

Six disciplines are seeded, each carrying a default_task_id that maps 1:1 to the corresponding task. This ensures every client time entry is categorised in the By-discipline segmentation view with zero Unmapped rows.

IDNameDefault task
1DesignDesign (task 2)
2DevelopmentDevelopment (task 3)
3StrategyStrategy (task 1)
4CopywritingCopywriting (task 4)
5Project ManagementProject Management (task 5)
6QA & ReviewQA & Review (task 6)

Task 7 (Admin) is internal-only and intentionally has no discipline — internal projects are excluded from segmentation.

Clients (11)

NameNotes
InternalRequired infrastructure — always id=1
Meridian HealthRetainer client
Harbor LogisticsRetainer client
Bloom RetailFixed Price client
Pulse AnalyticsT&M client
FreshFarmFixed Price client
CityCoreT&M client
Nexus MediaFixed Price client
SkyBridgeT&M + Fixed Price client
Apex FintechRetainer client
Future Forward FundPro Bono client

Generated projects (canonical shapes)

NameTypePMNotes
Admin / Internal Meetings / New Business DevelopmentInternalTom (id 1)Overhead
PTO / Sick / LeaveInternalProtected time-off projects
Meridian Health Support RetainerRetainerSarah Chen13+ completed periods + active Apr 2026
Harbor Logistics Platform RetainerRetainerMike JohnsonPacing over budget (demo alert trigger)
Apex Fintech Creative RetainerRetainerSarah Chen
Bloom Retail Brand RefreshFixed PriceSarah Chen
FreshFarm Brand IdentityFixed PriceSarah Chen
Nexus Media Campaign SuiteFixed PriceMike Johnson
SkyBridge Annual ReportFixed PriceMike Johnson
Future Forward Fund ImpactPro BonoSarah Chen
CityCore App DevelopmentT&MSarah ChenVendor liability (Talia Rodriguez, Design) — drives the By-discipline make-vs-buy face-off
Pulse Analytics DashboardT&MMike JohnsonVendor liability (Jordan Reed, Copywriting) — drives the By-discipline make-vs-buy face-off
SkyBridge Website RedesignT&MMike Johnson
Meridian Health Digital SupportFixed PriceSarah ChenBackbone engagement — spans the full 12-month window so the Monthly P&L never falls to $0 (the frozen retainers are stripped at seed). Fixed Price as of #1860 (was T&M) — shifts the mix without touching any project pinned by name/id in manual-testing.md. Also carries Tom Hart's (id 1) sole envelope (#1860 item 2).
Harbor Logistics Platform SupportFixed PriceMike JohnsonBackbone engagement — full-window billable base, neutral delivery position. Fixed Price as of #1860 (was T&M). Also carries Rachel Kim's (id 2) sole envelope (#1860 item 2).
Test – Front-loaded (planned vs linear)Fixed PriceSarah Chen#1860 item 1 — proves Phase 3 (#1857): a 200h envelope shaped front-loaded (130h across the first half of the schedule) reads on course against the planned baseline while the same numbers read off course against the naive linear baseline. Dates are derived from now on every reseed (never hardcoded) so it keeps discriminating regardless of which day the daily reseed runs. See tests/demo-seed-discriminator.test.js.

The three retainers above are generated but stripped at seed time (Retainer/Agile creation is frozen, #962), so they do not appear in the live demo. Their continuous monthly revenue is replaced by the two full-window backbone engagements, whose logged hours are spread evenly across the window (not front-loaded) so every month shows believable, non-zero revenue.

Proposals (5)

Issue #1861 — the demo tenant had zero proposals (wipeHoTables cleared proposals/proposal_roles/proposal_liabilities on every reseed, but nothing ever inserted them, so /console/proposals was permanently empty). Five proposals now cover every lifecycle stage. Console-only, Slack-unreachable, audit-silent (see docs/decisions/proposals.md) — seeding writes rows directly and posts nothing.

NameStatusNotes
Bloom Retail — Loyalty AppdraftMinimal, fully editable. No deadline, no hours budget — exercises the empty-state Effort-vs-timeline / Allocation cards.
SkyBridge — Loyalty Program BuilddraftRichly staffed: three staffing lines (one windowed + shaped, 150h across 10 weeks) plus a vendor liability (Talia Rodriguez). Has a deadline and hours budget, so the Cost breakdown, Proposed P&L, Effort-vs-timeline, and Envelope-budget bar all render.
Nexus Media — Q4 Campaign RefreshwonConvert-to-project button available; Edit shows disabled-but-visible.
Apex Fintech — Mobile RedesignlostRead-only; Delete stays available.
Meridian Health — Digital Support Renewalconvertedconverted_project_id points at the real seeded "Meridian Health Digital Support" project (never an invented id — the dead-link guard tests/demo-seed-proposals.test.js enforces this). Edit and Delete both disabled-but-visible; the list row shows a project → link.

At least one proposal_role carries a window and a shape (the SkyBridge Development line), so the Phase 3 capacity simulation has shaped demand to overlay, not just the pre-#1821 unshaped path.

Preserved-user anchor projects

Created by seedPreservedUserData after the bulk insert. These always exist post-reseed regardless of what the generator produces.

NameTypePurpose
Test ClientClient recordParent for anchor projects
Test – Acme RetainerRetainerMTH PM; three burn states (Owner <50%, User healthy)
Test – Blue Website (FP)Fixed PriceMTH PM; User over budget; vendor scenario
Test – Agile SprintAgileMTH PM; User at warning burn
Test – Pro Bono ClinicPro BonoZero-cost project
Test – Internal OverheadInternalShared overhead project for all preserved users

Verifying a reseed worked

Run these in Supabase SQL Editor after !test seed:

sql
-- Confirm test cruft (Foo* projects) is gone
SELECT id, name FROM projects
WHERE tenant_id = 'e0c22d94-f990-5cd3-99b7-a4e2a2228147'
  AND name LIKE 'Foo%';
-- Expect 0 rows

-- Confirm cast is restored
SELECT COUNT(*) AS user_count FROM users
WHERE tenant_id = 'e0c22d94-f990-5cd3-99b7-a4e2a2228147'
  AND status = 'active';
-- Expect ≥ 18 (15 generated + 3 preserved)

-- Confirm anchor projects exist
SELECT id, name FROM projects
WHERE tenant_id = 'e0c22d94-f990-5cd3-99b7-a4e2a2228147'
  AND name LIKE 'Test – %';
-- Expect 4–5 rows (Acme Retainer, Blue Website FP, Agile Sprint, Pro Bono Clinic, Internal Overhead)

-- Confirm every proposal lifecycle stage is present (#1861)
SELECT status, COUNT(*) FROM proposals
WHERE tenant_id = 'e0c22d94-f990-5cd3-99b7-a4e2a2228147'
GROUP BY status;
-- Expect 5 rows total: draft (2), won (1), lost (1), converted (1)

-- Confirm the converted proposal's converted_project_id resolves to a real project
SELECT p.id, p.name, p.converted_project_id, pr.name AS project_name
FROM proposals p
LEFT JOIN projects pr
  ON pr.tenant_id = p.tenant_id AND pr.id = p.converted_project_id
WHERE p.tenant_id = 'e0c22d94-f990-5cd3-99b7-a4e2a2228147'
  AND p.status = 'converted';
-- Expect project_name to be non-null (never a dangling id)

Safeguards

  • seedDemoTenant returns { success: false } immediately if tenant.id !== 'T0ADQ6UAVKR'.
  • !test seed in index.js checks tenant.id !== DEMO_TENANT_ID before calling the seed function.
  • Advisory lock prevents concurrent runs (cron + manual).
  • Every DELETE includes WHERE tenant_id = <HO UUID> as belt-and-suspenders beyond RLS.

Monitoring

Every successful seed run records a seed.daily event, visible on /status as "Demo seed (HO)". It follows the same weekday-only cadence as the morning/evening reminder checks — an amber reading early Monday morning (before the 8:50 AM ET run) is expected and self-heals once that run completes, not a failure to investigate.


Recovery if reseed fails

If !test seed fails mid-run, the database may be in a partially-wiped state. Re-run !test seed immediately — the wipe+insert is atomic (single transaction), so a failed run leaves the previous state intact or rolls back cleanly.

If state is unrecoverable, escalate to Tom for direct SQL intervention.