onboarding-blueprint-modelrun.md00_intake/stub.mdThe demo holds the whole onboarding draft in client state; nothing survives a refresh. A mandatory redirect-until-complete wizard (this epic's end state) is only viable if the draft and the tenant's onboarding progress are persisted — the first user must be able to leave and resume, and the platform must be able to ask "is this tenant set up?" cheaply on every request. Issue #588 Q3 (persist vs ephemeral) and Q5 (confidence/provenance) are settled here: persist both.
A first-class, tenant-scoped onboarding blueprint document plus a tenant-level onboarding lifecycle. The blueprint spans the 7 areas from the demo spine (company profile, categories, products, services, skills, SLA tiers, settings); every generated item carries confidence, provenance (AI-drafted vs human-edited) and a review state (pending / approved / edited / rejected). The blueprint itself moves through a status lifecycle (e.g. draft → in-review → verified → committed), and the tenant records its onboarding state so gating (stub 7) and the wizard can resolve progress without loading the blueprint. Includes the service-layer CRUD for the blueprint (get/upsert section, set item review state, advance status) — no UI, no AI.
Follow the house Mongoose idiom (plugin trio, tenant-leading indexes, bounded arrays — mind the 7-section document size; sections with many items may need capping or sub-documents). touches: packages/services/src/db/models/, db/services/, db/services/tenant/, a migration via db-migration. The demo's section keys in apps/demo/lib/mock/onboarding.ts (profile|categories|products|services|skills|sla|settings) are the section vocabulary.
01_define/output/spec.mdThe AI onboarding demo (apps/demo/app/onboarding/) holds the entire onboarding draft in client
state — nothing survives a refresh. The productionised concierge wizard this epic builds is
mandatory and redirect-until-complete for fresh tenants, which is only viable if two things are
durable: the tenant's onboarding draft (so the first user can leave and resume) and the tenant's
onboarding progress (so the platform can cheaply answer "is this tenant set up?" on every
request without loading the draft). This is stub 1 of the tenant-onboarding-wizard epic
(sequence 1 of 7, depends-on: none) — the persistence foundation every later stub builds on. It
advances Build the Bridge / Q2-2026 O1 — Establish PMF with Vendor Partners (KR: onboard 8+
vendors onto paid tiers): no persisted setup, no repeatable vendor onboarding. Issue #588's Q3
(persist vs ephemeral) and Q5 (confidence/provenance) are settled here: persist both.
Add a first-class, tenant-scoped onboarding blueprint document plus a tenant-level onboarding lifecycle, and the service-layer CRUD to drive them. No UI, no AI, no committing into real services this run.
Onboarding blueprint document — one per tenant, spanning the 7 sections from the demo spine:
profile, categories, products, services, skills, sla, settings (the exact section keys
from apps/demo/lib/mock/onboarding.ts). The document carries:
draft → in_review → verified → committed.confidence — number 0–100 (matching the demo's confidence scale).provenance — ai_drafted | human_edited.reviewState — pending | approved | edited | rejected.Tenant onboarding state — a coarse onboarding status recorded on the tenant (embedded, not a
separate lookup) so gating (stub 7) and the wizard can resolve progress with a cheap tenant read and
without loading the blueprint. Status enum: not_started → in_progress → completed. Freshly
created tenants start not_started.
Service layer (@sustentus/services/server, following the house Mongoose idiom — plugin trio,
tenant-leading ESR indexes, async model factory): get blueprint; read/upsert a single section;
set an item's review state; advance the blueprint status; and get/set the tenant onboarding state.
Every call is tenant-scoped.
Seed — the existing tenant-defaults seed continues to apply; fresh tenants default to
not_started onboarding state. A migration (via db-migration) creates the collection/indexes and
backfills existing tenants to a sensible onboarding state.
draft → in_review → verified → committed) via the service layer, all scoped to a single
tenant.confidence (0–100), provenance (ai_drafted | human_edited)
and reviewState (pending | approved | edited | rejected), and the whole document survives a
process restart (persisted in MongoDB, verified by re-reading after reconnect).not_started | in_progress | completed) that answers
"setup done/verified/completed?" via a tenant read without loading the blueprint document.not_started; the existing tenant-defaults seed still
applies unchanged, and a migration backfills existing tenants./onboarding surface, and routing/redirect gating (stubs 6–7)./admin/settings/setup static checklist.02_build/output/notes.mdfeat: onboarding-blueprint-model — persisted blueprint model, tenant onboarding state, service CRUD + migrationpackages/services/src/db/models/onboarding-blueprint.ts (new): the tenant-scoped
onboarding_blueprints collection. One document per tenant, status lifecycle
(draft → in_review → verified → committed), and a sections array over the 7 demo-spine keys
(profile · categories · products · services · skills · sla · settings). Each section holds a
bounded items array; every item carries confidence (0–100), provenance
(ai_drafted | human_edited) and reviewState (pending | approved | edited | rejected), plus a
flexible data payload for section-specific fields. House idiom: plugin trio (schema → softDelete →
tenant), async getOnboardingBlueprintModel() factory, tenant-leading unique partial index
{ tenantId: 1 } (isDeleted:false), MAX_ITEMS_PER_SECTION = 200 array-length validator.packages/services/src/db/models/tenant.ts: added the embedded onboarding.status field
(not_started | in_progress | completed, default not_started) and the TENANT_ONBOARDING_STATUSES
enum — a cheap onboarding read that never loads the blueprint.packages/services/src/db/services/onboarding-blueprint/{index,instance}.ts (new):
OnboardingBlueprintService — getBlueprint, getOrCreateBlueprint, getSection, upsertSection,
setItemReviewState, advanceStatus (forward-only). Registered in the services barrel.packages/services/src/db/services/tenant/index.ts: getOnboardingStatus (projects only
onboarding, no blueprint read) and setOnboardingStatus on TenantService.packages/services/src/db/models/index.ts: exported the new model.packages/services/src/db/migrations/1783425600000-onboarding-blueprint-and-tenant-onboarding.ts
(new): creates the blueprint unique partial index and backfills existing tenants to
onboarding.status = "not_started". Symmetric down.OnboardingBlueprintService methods, all keyed on tenantId.BlueprintItemSchema; the document round-trips through the onboarding_blueprints
collection.tenantService.getOnboardingStatus reads tenant.onboarding.status via
.select("onboarding").not_started; the tenant-defaults seed is unchanged; migration backfills
existing tenants — schema default (applied on insert via setDefaultsOnInsert) + the migration's
updateMany. The seed needed no edit, so it "still applies unchanged" by construction.MAX_ITEMS_PER_SECTION); house idiom (plugin trio,
tenant-leading index, async factory); blueprint unique per tenant.tenant-defaults seed "still applies unchanged";
fresh-tenant not_started comes from the schema default, and existing tenants are handled by the
migration backfill — so no seed edit was needed. Flagging in case a reviewer expected a seed diff.data is Schema.Types.Mixed so each section keeps its own payload shape (pricing, SLA tiers,
…) without seven bespoke sub-schemas; the typed metadata (confidence/provenance/reviewState) is
enforced uniformly. This was the modelling latitude the spec explicitly left to Build.upsertSection is two writes (in-place $set on the positional match, else $push) because the
positional operator can't upsert an array element that may not exist yet.03_release/output/release.mdformat:check on a non-idempotent
prettier wrap in spec.md) was fixed by reflowing the acceptance criterion; re-run went green.@sustentus/services entrypoint. technical/packages/services describes structure
generically (no per-model/collection inventory), so nothing there goes stale.db-migrate workflow) + app deploys confirmed via
poll-deploy.sh post-merge — see below.OnboardingBlueprintService (get, getOrCreate, getSection, upsertSection,
setItemReviewState, advanceStatus), all keyed on tenantId.BlueprintItemSchema; round-trips through onboarding_blueprints.tenantService.getOnboardingStatus
reads tenant.onboarding.status via .select("onboarding").not_started; tenant-defaults seed unchanged; migration backfills existing
tenants — schema default (setDefaultsOnInsert) + migration updateMany.MAX_ITEMS_PER_SECTION); house idiom (plugin trio, tenant-leading unique
index, async factory); one blueprint per tenant.