Migration status — ADR-0038 + ADR-0039
- Last updated: 2026-05-04 (cumulative session: 33 commits landed)
- Owner: founder
- Update protocol: amend this file whenever a workspace stage advances or a Stripe meter goes live; outside those events, prefer ADR amendments
This is the durable status of the pricing + workspaces rollout. Read this before continuing work on either ADR — the session that produced the foundation is not coming back, and the only safe way to keep the rollout coherent is a single source of truth for “what’s shipped vs what isn’t.”
Headline
Section titled “Headline”- Pricing system (ADR-0038): SHIPPED end-to-end. Code is live on dev. Stripe is the only manual blocker for actual money flow.
- Workspaces (ADR-0039): Stages 1-5 functionally COMPLETE. Schema, backfill, write paths, hot-path reads, ACL, URL routing, admin CRUD, header switcher, soft-delete, recovery UI, hard-delete cron, and isolation test suite all shipped. The full multi-brand demo flow works end-to-end.
- Today’s behavior: every existing tenant still has exactly one
default workspace; the platform now supports creating more via the
admin page (
/(admin)/workspaces) and addressing them via/c/{tenant}/_w/{slug}/.... Tenants on Free/Team plans hit the workspace cap (1) on POST — the intended upsell trigger.
Deployments live right now
Section titled “Deployments live right now”| Component | URL | Auto/manual |
|---|---|---|
| App (CF Pages) | https://puccha-dev.hxlab.io | auto from main |
| Marketing (Worker) | https://puccha-marketing.harmonyx.workers.dev | manual wrangler deploy |
| Cron worker | puccha-dev-cron.harmonyx.workers.dev |
manual wrangler deploy |
| Dev D1 | puccha-dev (da855e8c) |
migrations applied via wrangler d1 execute --remote |
Cron schedules currently firing:
* * * * *→/api/cron/sla-breach(ADR-0037)*/15 * * * *→/api/cron/resolutions(ADR-0038)0 3 * * *→/api/cron/retention(ADR-0021)30 3 * * *→/api/cron/workspace-cleanup(ADR-0039 §Q3)0 1 1 * *→/api/cron/usage-reporter(ADR-0038, dormant — no Stripe env)
Migrations applied to remote dev D1
Section titled “Migrations applied to remote dev D1”| File | Purpose | State |
|---|---|---|
0019_pricing_v2.sql |
tenants.quotas_json, resolutions, api_query_events |
applied + backfilled |
0020_workspaces.sql |
workspaces table + nullable workspace_id columns on 9 tables |
applied |
0021_workspaces_backfill.sql |
one default workspace per tenant; populate workspace_id | applied + verified 0 NULLs |
Both existing tenants grandfathered (certogo, harmonyx) with
{legacyPlan: 'team', grandfatheredUntil: '2027-05-03'}. Hard caps and
overage do not apply during the grandfather window.
Code shipped (33 commits, cumulative)
Section titled “Code shipped (33 commits, cumulative)”ADR-0038 Pricing (8 commits, complete):
8ff9e79 feat(billing): hybrid pricing — resolutions + API meters4ebfab0 feat(billing): hard-cap chat at hardCapMultiplier × included02fa766 docs(marketing): rewrite pricing page + publish pricing-policy6b40813 feat(admin): billing page shows live resolution + API usagebe01a21 feat(billing): monthly Stripe usage reporter via Billing Metersf1c2b9e docs(ops): initial migration-status doc+ 2 inline (Stripe wiring, plan rewrite)ADR-0039 Workspaces (24 commits, Stages 1-5 + early Stage 6):
05b0d44 feat(db): workspaces schema + backfill (Stages 1+2)0372cbf feat(billing): tag new resolutions with workspace_id (Stage 3)686bd10 feat(app): resolve workspace in hooks.server.ts (Stage 3)b962216 feat(docs): tag new documents+chunks with workspace_id (Stage 3)f30e90c feat(docs): workspace-scope api/docs/[id] CRUD reads (Stage 3)1ed177e feat(rag): workspace-scope hybrid search hot path (Stage 3)1662e5c feat(docs): workspace-scope remaining doc/source admin reads3c8900e feat(conv): workspace-scope conversation create + list paths98e005a feat(visitor): workspace-scope visitor resolution + admin4fda985 feat(docs): POST /api/docs/:id/reindex — retry failed ingest8b264b6 feat(conv): workspace-scope inbox + CSAT submission2e96aec fix(visitor): scope captureContact email-merge to one workspace60fdf94 feat(auth): workspace-scoped membership ACL (§4)e81368e feat(tickets): workspace-scope tickets list + CRUD (Stage 3)27f5619 feat(workspaces): CRUD endpoints + admin page (Stage 5 starter)85b5b7a feat(auth): URL-aware workspace resolution (Stage 4 partial)a3dc0a3 feat(routing): /c/{tenant}/_w/{workspace}/... addressing (Stage 4)2dd537a test(e2e): cross-workspace isolation suite (§4)224c358 feat(workspaces): header workspace switcher (Stage 5)f417d53 feat(cron): hard-delete soft-deleted workspaces after 30d (§Q3)cd1b625 feat(workspaces): soft-delete recovery UI (§Q3)8164b44 feat(stage3): smaller conv routes + customers/[id]6fd7f35 feat(stage3): personas + smaller ticket endpoints3b75e02 feat(cron): one-shot Vectorize legacy re-index endpoint9cca987 feat(email): tag email-inbound tickets with default workspace4e389f5 fix(cron): time-bound + resumable vectorize-reindex8317e30 docs(ops): refresh migration-status mid-sessionADR-0038 (Pricing) status
Section titled “ADR-0038 (Pricing) status”- PLAN_META rewritten to hybrid (resolutions + overage)
-
getEffectiveQuotas,isGrandfathered,checkResolutionCaphelpers -
/api/chathard-cap pre-flight (Free=100, Team=2k, Business=10k, Ent=125k) - Resolution detector cron (every 15 min) — writes to
resolutionstable - Refund-on-failure hook on
POST /api/messages/:id/feedback - API query event recorder on MCP
tools/call - Public
/pricingrewritten with resolution model - Public
/pricing-policypage (90-day notice + 12-mo grandfather commitment) - Admin
/billingshows live usage meters with progress bars + grandfather banner - Stripe usage reporter cron (monthly) — code live, dormant until env set
Pending
Section titled “Pending”- Create Stripe products in dashboard (manual, ~1 hour) — see stripe-setup.md
- Set
STRIPE_METER_EVENT_RESOLUTIONandSTRIPE_METER_EVENT_APIsecrets viawrangler secret put - Update Checkout flow to attach metered prices alongside the base — small code change in
api/billing/+server.tscheckout-session handler (note in stripe-setup.md §4)
Resolved decisions (locked 2026-05-03)
Section titled “Resolved decisions (locked 2026-05-03)”- Resolution edge case: any thumbs-down kills the whole conversation
- API/MCP rate ratio: 1 API query = 0.2 resolution-equivalents
- Free hard-cap behavior: localized message, doc viewing remains
- Annual customers locked at sign-up overage rate for full term
- Workspaces price mechanism: quota only, no per-workspace overage
- Public pricing-policy page: yes (lives at /pricing-policy)
ADR-0039 (Workspaces) status
Section titled “ADR-0039 (Workspaces) status”- Stage 1: schema migration (workspaces table + workspace_id columns on 10 tables)
- Stage 2: backfill (default workspace per tenant, all rows tagged)
- Stage 3 — write paths: resolutions, documents, chunks, sources, personas (insert path), conversations, visitors (web + LINE), tickets, Vectorize metadata
- Stage 3 — read paths: hooks workspace resolution, RAG hot path (Vectorize post-filter + FTS5 join + substring + allowedDocs), api/docs/[id] CRUD, api/docs list, api/docs/bulk, api/connectors/webhook, api/chat conv create+get, api/inbox, api/conversations/[id]/csat, api/tickets list+create+CRUD, admin pages: documents, sources, conversations, customers
- Stage 3 — security: workspace-scoped membership ACL (§4), captureContact email-merge confined to single workspace (§7)
- Stage 4: URL routing via
_wmarker —/c/{tenant}/_w/{slug}/...for non-default, legacy/c/{tenant}/...resolves to default forever - Stage 5: workspace CRUD endpoints (POST/PATCH/DELETE/restore),
/(admin)/workspacesadmin page with create form + quota meter, header WorkspaceSwitcher mounted in admin layout, soft-delete recovery UI with per-day countdown - Stage 6 (early): hard-delete cron at
30 3 * * *permadeletes soft-deleted workspaces after 30 days; CASCADE wipes the data tree - Tests:
e2e/workspace-isolation.test.ts— 10 unauthenticated tripwires for the URL/auth boundary - Operational:
POST /api/docs/:id/reindex(failed-ingest retry, surfaced by Certogo’s failed ISO 27001 doc)
Stage 3 substantively complete
Section titled “Stage 3 substantively complete”Every customer-touching read + write path is workspace-scoped. The only remaining tenant-only filters are deliberate per ADR-0039 §4:
api/cron/sla-breach— SLA breaches are an org-level operations concern visible to tenant admins regardless of which brand the ticket lives in. Per-workspace inbox views can layer on top in Stage 5 polish without touching the cron.(admin)/dashboard— org health summary view; per-workspace dashboard variant deferred to a Stage 5 polish decision (org-wide vs workspace- scoped vs both).
Items resolved this session that were on the previous status doc’s pending list:
-
api/conversations/[id]/{csat,poll,tags,messages}routes -
lib/tickets.tsemail-inbound ticket creation (uses default workspace) -
api/tickets/[id]/{close,reopen,comment}smaller endpoints - Personas (admin page + api/personas)
-
(admin)/customers/[id]detail page -
captureContactcross-workspace email-merge fix - Workspace-scoped membership ACL in
hooks.server.ts
Pending — polish + ops
Section titled “Pending — polish + ops”- Per-workspace settings page (per-brand widget config, system prompt overrides — name + brandAccent already settable via the existing PATCH endpoint)
- Mobile-friendly workspace switcher (currently desktop only)
- Trigger the Vectorize legacy re-index on dev — endpoint shipped (
3b75e02) and made resumable (4e389f5). Two ways: cron-worker temporary schedule (we tried; CF cron propagation lag prevents quick verification) or admin-mode in browser (sign in as Certogo/Harmonyx owner, hit/c/{tenant}/api/cron/vectorize-reindexfrom DevTools) - Authenticated cross-workspace test fixtures for the isolation suite
Resolved decisions (locked 2026-05-03 / amended 2026-05-04)
Section titled “Resolved decisions (locked 2026-05-03 / amended 2026-05-04)”- Default workspace slug for backfill:
'default' - Widget embed URL: legacy
/c/{tenant}/...keeps working forever, new path uses_wmarker - Workspace deletion: soft delete + 30-day recovery + hard-delete cron
- New workspace creation: starts with no personas (the “copy-from-other-workspace” option deferred to a per-workspace settings build)
- Cross-workspace visitor linking: Enterprise-only flag, not implemented
- Allowed origins: tenant-wide (not per-workspace)
- (Amended 2026-05-04) URL pattern uses
_wmarker rather than the spec’s bare-slug shape — admin route names collide with the kebab-slug shape, so a marker was the only way to disambiguate without a fragile reserved-name list. Seehooks.tsfor the inline rationale.
Smoke tests you can run today
Section titled “Smoke tests you can run today”# App is live with new endpointscurl -s -o /dev/null -w "%{http_code}\n" https://puccha-dev.hxlab.io/api/cron/resolutionscurl -s -o /dev/null -w "%{http_code}\n" https://puccha-dev.hxlab.io/api/cron/usage-reporter# Both → 403 (auth required, correct)
# Marketing has new pricing model + policy pagecurl -s -L https://puccha-marketing.harmonyx.workers.dev/pricing | grep -i "outcome-based"curl -s -L https://puccha-marketing.harmonyx.workers.dev/pricing-policy | grep -i "90 days"
# Database is in expected statecd platform/apps/apppnpm wrangler d1 execute puccha-dev --remote --command \ "SELECT slug, plan, quotas_json FROM tenants;"pnpm wrangler d1 execute puccha-dev --remote --command \ "SELECT id, tenant_id, slug, name FROM workspaces;"pnpm wrangler d1 execute puccha-dev --remote --command \ "SELECT COUNT(*) FROM resolutions; SELECT COUNT(*) FROM api_query_events;"
# Sign in to /c/certogo/billing — should see:# - amber grandfather banner# - resolution + API meters at 0 (until detector fires on a billable conversation)# - new plan grid: Free/Team/Business/Enterprise with overage ratesWhat to do next, by priority
Section titled “What to do next, by priority”1. Stripe products + meters (the only real revenue blocker)
Section titled “1. Stripe products + meters (the only real revenue blocker)”Manual dashboard work, ~1 hour, walk through stripe-setup.md. Test mode first, validate, then live. After this, the dormant cron starts firing real charges — schedule the first cron-fire week to coincide with low-traffic so any issues are observable.
2. Demo to a multi-brand prospect (validates the build)
Section titled “2. Demo to a multi-brand prospect (validates the build)”The full Stage 5 demo flow works: create workspaces, switcher between them, upload data into one, verify isolation, soft-delete + recover. This is the moment to find out whether multi-brand is actually a market.
3. Vectorize legacy re-index (pre-Stage-5-cutover)
Section titled “3. Vectorize legacy re-index (pre-Stage-5-cutover)”The dev D1’s 313 existing chunks were embedded before workspaceId entered metadata. The search.ts post-filter treats missing metadata as “match” — correct during migration but means legacy vectors appear in every workspace once a second one exists. Re-index before customers create a second workspace in production. ~30 min via a one-shot script.
4. ADR-0040 (or amendment to 0039) — workspace ACL details
Section titled “4. ADR-0040 (or amendment to 0039) — workspace ACL details”Once a real multi-brand customer is live, document the ACL nuances (cross-workspace search opt-in, visitor linking, audit log scope) as a follow-up ADR. Defer until usage informs the spec.
5. Stage 3 mop-up (~2 hours) — DONE this session
Section titled “5. Stage 3 mop-up (~2 hours) — DONE this session”The previous status doc listed this as priority 2. All items closed: personas, customers/[id], smaller conversation routes (poll/tags/messages), ticket close/reopen/comment, email-inbound ticket creation. Only deliberate org-wide views remain (sla-breach cron, admin dashboard).
Risks I’m aware of
Section titled “Risks I’m aware of”-
Vectorize legacy vectors: existing 313 chunks in dev D1 don’t have
workspaceIdin metadata. The post-filter insearch.tstreats missing metadata as “match” — correct during the migration, but means legacy vectors will surface in EVERY workspace once a second one exists in production. Mitigation: re-index before any production tenant creates a second workspace. Listed in priority 3 above. -
Stripe cron firing without products configured:
/api/cron/usage-reporterfires on the 1st of each month. Today it’s a no-op (no env vars), but if env vars get set BEFORE Stripe products exist, the call will 4xx and log noisy errors. Set products first, then env vars. Ordering documented instripe-setup.md. -
Workspace-scoped editors blocking themselves: the membership ACL shipped in
60fdf94clearstenant.rolewhen the URL specifies a workspace different from the user’s scoped one. If a tenant-wide admin later adds a workspace-scoped membership for themselves on a different workspace, they could navigate via URL into a workspace they no longer have access to. Mitigation: tenant-wide membership wins (most permissive) — the lookup inhooks.server.tsprefersworkspace_id IS NULLrows. So this only bites users who genuinely don’t have a tenant-wide row; that’s the intended behavior, not a bug. -
Reindex endpoint exposes content_md regardless of audit settings:
POST /api/docs/:id/reindexreadsdocuments.content_mdand re-runs ingest. For tenants with a strict audit-log policy, this re-runs LLM contextualization — billable work. Mitigation: editor-role gate ensures only authorized callers; the reindex is logged asdoc.reindexin the audit table.
What was resolved this session (2026-05-03 → 2026-05-04, 33 commits)
Section titled “What was resolved this session (2026-05-03 → 2026-05-04, 33 commits)”ADR-0038 Pricing — 100% code-complete. Stripe wiring + product creation in dashboard remains as manual ops work; everything else (PLAN_META, hard cap, refund-on-failure, marketing pages, admin billing UI, monthly usage reporter cron) is shipped and live on dev.
ADR-0039 Workspaces — Stages 1 through 5 functionally complete plus Stage 6 early-shipped:
- Schema + backfill: all 10 workspace-scoped tables have
workspace_idcolumns; existing rows tagged with the per-tenant default workspace - Stage 3 write paths: every insert site stamps
workspace_id - Stage 3 read paths: every customer-touching query filters by workspace; only deliberate org-wide views (sla-breach cron, admin dashboard) keep tenant-only scope
- Stage 3 security: workspace-scoped membership ACL, captureContact merge confined to single workspace
- Stage 4: URL routing via
_wmarker, both legacy and workspace- aware paths supported forever - Stage 5: workspace CRUD endpoints, admin page with create form + quota meter, header switcher, soft-delete recovery UI
- Stage 6 (early): hard-delete cron at 03:30Z permadeletes soft-deleted workspaces after 30 days
- Tests: 10 unauthenticated isolation tripwires
- Operational tools:
POST /api/docs/:id/reindexfor failed-ingest recovery,GET /api/cron/vectorize-reindexfor legacy-metadata re-embed (time-bounded + resumable)
Items intentionally NOT shipped (judgement calls):
- Per-workspace settings page beyond name + brandAccent (already settable via the PATCH endpoint) — deeper per-brand widget config + system prompt overrides queued for the next Stage-5 polish session
- Authenticated cross-workspace test fixtures — needs a test-DB seeding harness, not a single-file commit
- Vectorize re-index against dev D1 — endpoint shipped + fixed, but triggering it cleanly requires either signed-in admin session or CRON_SECRET; tried via temp cron schedule but CF propagation lag prevented same-session verification