Module Marketplace Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Ship a polished, auth-gated marketplace at /dashboard/marketplace for paid module + resource addons. Tenants browse → request → pay invoice → superadmin approves → module activates. Spec: docs/superpowers/specs/2026-05-19-module-marketplace-design.md.
Architecture: New marketplace_listings + marketplace_releases tables hold CMS content. Existing addon_requests flow is extended (enum widening) to cover the 6 module addons in addition to storage / portal_seats. Tenant routes under /marketplace/* (auth-gated), superadmin CMS + request inbox under /superadmin/marketplace/*. Six new transactional email templates cover the lifecycle. Two crons handle overdue invoices + idle-subscription nudges. Frontend reuses dashboard shell + shadcn primitives, no new design system.
Tech Stack: Hono (server), Drizzle ORM + Postgres (pgEnum + jsonb), Cloudflare Workers + R2 (screenshots), TanStack Query (frontend), React Router, shadcn/ui + Tailwind, @react-email/render, Vitest (server tests), React Testing Library (UI tests).
Phases & Parallelism
This plan has 4 phases. Tasks within a phase may be parallelizable; phases are sequential.- Phase 1 (sequential): Schema + migration. T1 → T6.
- Phase 2 (parallel-friendly after Phase 1): Backend libs, routes, emails, crons. T7–T16.
- Phase 3 (parallel-friendly after Phase 2): Frontend pages + components. T17–T26.
- Phase 4 (sequential): Docs + end-to-end smoke. T27–T28.
Phase 1 — Schema & Migration
Task 1: Create marketplace_listings schema
Files:
-
Create:
server/src/schema/marketplace_listings.ts - Step 1: Write the schema file
- Step 2: Commit
Task 2: Create marketplace_releases schema
Files:
-
Create:
server/src/schema/marketplace_releases.ts - Step 1: Write the schema file
- Step 2: Commit
Task 3: Extend addon_pricing for module-addon pricing
Files:
-
Modify:
server/src/schema/addon_pricing.ts - Step 1: Add the jsonb column
portalSeats3PackProPlusPkr line, before updatedAt, add:
- Step 2: Commit
Task 4: Extend addon_requests enums + columns
Files:
-
Modify:
server/src/schema/addon_requests.ts - Step 1: Widen enums and add columns
rejectedReason: text('rejected_reason'), line, add:
- Step 2: Commit
Task 5: Wire new tables into the schema index
Files:-
Modify:
server/src/schema/index.ts - Step 1: Add exports
- Step 2: Verify type-check
cd server && pnpm tsc --noEmit
Expected: PASS (no type errors).
- Step 3: Commit
Task 6: Write the migration SQL
Files:-
Create:
server/drizzle/0045_marketplace.sql - Step 1: Write the migration
- Step 2: Apply to dev DB (DO NOT touch prod)
server/scripts/ before running.)
Expected: migration applied, 8 seed rows inserted in draft status.
- Step 3: Commit
Phase 2 — Backend
Task 7: Add marketplace permission keys + role defaults
Files:-
Modify:
ui/src/lib/permissions-keys.ts -
Modify:
ui/src/lib/permissions.ts -
Modify:
server/src/lib/permissions.ts(if mirror exists — verify) -
Step 1: Add to
permissions-keys.ts
- Step 2: Add default role grants in
permissions.ts
owner+clinic_admin: all three (marketplace.view,marketplace.request,marketplace.cancel).doctor+nurse+receptionist:marketplace.viewonly.portal+patient: none.
server/src/lib/permissions.ts, mirror the same grants there.
- Step 3: Type-check + commit
Task 8: State machine helper lib + tests
Files:-
Create:
server/src/lib/marketplace-state.ts -
Test:
server/src/lib/__tests__/marketplace-state.test.ts - Step 1: Write the failing tests
- Step 2: Run tests (expect FAIL)
cd server && pnpm vitest run src/lib/__tests__/marketplace-state.test.ts
Expected: FAIL — module not found.
- Step 3: Implement
- Step 4: Run tests (expect PASS)
cd server && pnpm vitest run src/lib/__tests__/marketplace-state.test.ts
Expected: PASS (7 tests).
- Step 5: Commit
Task 9: Tenant marketplace routes + tests
Files:-
Create:
server/src/routes/marketplace.ts -
Test:
server/src/routes/__tests__/marketplace.test.ts -
Modify:
server/src/api.ts(register route) - Step 1: Write the failing tests
seedClinic, seedListing, seedRequest, authedRequest are existing test helpers in server/src/test-utils.ts — if any of these helpers are missing, add the minimal helper in the same task before continuing.)
- Step 2: Run tests (expect FAIL)
cd server && pnpm vitest run src/routes/__tests__/marketplace.test.ts
Expected: FAIL — route not registered.
- Step 3: Implement the route
- Step 4: Register in
api.ts
app.route('/addon-requests', addonRequestsRoute)) and add nearby:
- Step 5: Run tests (expect PASS)
cd server && pnpm vitest run src/routes/__tests__/marketplace.test.ts
Expected: PASS (all 8+ tests).
- Step 6: Commit
Task 10: Superadmin listings CMS routes + tests
Files:-
Create:
server/src/routes/superadmin/marketplace.ts -
Test:
server/src/routes/__tests__/superadmin-marketplace.test.ts -
Modify:
server/src/api.ts(register) - Step 1: Write the failing tests
- Step 2: Implement (similar shape to existing
server/src/routes/superadmin/addon-requests.ts)
- Step 3: Register + run tests + commit
server/src/api.ts:
cd server && pnpm vitest run src/routes/__tests__/superadmin-marketplace.test.ts
Expected: PASS.
Task 11: Superadmin requests inbox (extend approval flow) + tests
Files:-
Modify:
server/src/routes/superadmin/marketplace.ts(add request handlers) -
Test: extend
server/src/routes/__tests__/superadmin-marketplace.test.ts -
Modify:
server/src/lib/modules.ts(helper to enable/disable a single module) - Step 1: Write failing tests
- Step 2: Add request handlers
server/src/routes/superadmin/marketplace.ts:
- Step 3: Add
setClinicModulehelper inserver/src/lib/modules.ts
- Step 4: Run tests + commit
Task 12: Six email templates
Files:- Create:
server/src/emails/MarketplaceListingLaunchedEmail.tsx - Create:
server/src/emails/MarketplaceReleaseEmail.tsx - Create:
server/src/emails/MarketplaceInvoiceReadyEmail.tsx - Create:
server/src/emails/MarketplaceInvoiceOverdueEmail.tsx - Create:
server/src/emails/MarketplaceAddonLiveEmail.tsx - Create:
server/src/emails/MarketplaceAddonIdleEmail.tsx - Create:
server/src/lib/marketplace-emails.ts(orchestrator)
server/src/emails/WelcomeEmail.tsx. Reuse footer/header components.
- Step 1: Write
MarketplaceInvoiceReadyEmail.tsx(canonical example)
- Step 2: Write the other 5 templates by analogy
MarketplaceListingLaunchedEmail—{ clinicName, listingDisplayName, listingTagline, exploreUrl }MarketplaceReleaseEmail—{ clinicName, listingDisplayName, versionLabel, summary, viewUrl }MarketplaceInvoiceOverdueEmail—{ clinicName, listingDisplayName, invoiceId, daysOverdue, payUrl }MarketplaceAddonLiveEmail—{ clinicName, listingDisplayName, quickstartUrl }MarketplaceAddonIdleEmail—{ clinicName, listingDisplayName, daysIdle, openUrl, cancelUrl }
- Step 3: Write the orchestrator
marketplace-emails.ts
clinic.notificationPreferences.marketplaceEmails !== false — if undefined, default to enabled.)
- Step 4: Commit
Task 13: Cron jobs — overdue invoices + idle nudge
Files:-
Modify:
server/src/scheduled.ts -
Create:
server/src/lib/marketplace-usage.ts - Step 1: Idle-usage heuristics
marketing_campaigns if marketing addon’s tables aren’t built yet — guard with try/catch returning null so the cron doesn’t crash. Add a comment noting the missing dependency.)
- Step 2: Add the crons to
scheduled.ts
- Step 3: Verify the wrangler/CF config has these cron expressions
wrangler.toml (or wrangler.jsonc) [triggers].crons to include "0 4 * * *" and "0 5 * * 1" if not already present.
- Step 4: Commit
Task 14: Screenshot serving — auth-gated worker route
Files:-
Modify:
server/src/routes/marketplace.ts(add GET/listings/:key/screenshots/:idx) - Step 1: Add the route
getR2Bucket actually exists in server/src/lib/r2.ts — if not, follow the existing screenshot-serving pattern in the codebase, e.g. how patient files are streamed.)
- Step 2: Commit
Phase 3 — Frontend
Task 15: TanStack Query hooks for listings + requests
Files:-
Create:
ui/src/hooks/use-marketplace-listings.ts -
Create:
ui/src/hooks/use-marketplace-requests.ts -
Modify:
ui/src/lib/queryKeys.ts - Step 1: Add query keys
queryKeys.ts add:
- Step 2: Write the hooks
- Step 3: Commit
Task 16: ListingCard + RequestStateBadge components
Files:
-
Create:
ui/src/components/marketplace/ListingCard.tsx -
Create:
ui/src/components/marketplace/RequestStateBadge.tsx -
Step 1: Build
RequestStateBadge
- Step 2: Build
ListingCard
- Step 3: Commit
Task 17: MarketplaceIndex page + nav entry
Files:
-
Create:
ui/src/pages/marketplace/MarketplaceIndex.tsx -
Modify:
ui/src/lib/nav-registry.ts -
Modify:
ui/src/App.tsx - Step 1: Build the index page
- Step 2: Add to nav registry
ui/src/lib/nav-registry.ts, add (between Settings and Help, or wherever feels right):
- Step 3: Register route in
App.tsx
- Step 4: Commit
Task 18: MarketplaceDetail page (subscribe + cancel flows)
Files:
-
Create:
ui/src/pages/marketplace/MarketplaceDetail.tsx -
Create:
ui/src/components/marketplace/ListingHero.tsx -
Create:
ui/src/components/marketplace/ChangelogList.tsx -
Step 1: Build
ListingHero(icon + title + tagline + CTA)
ListingCard. Add primary action button bound to subscribe/cancel mutations. Confirmation dialog using existing AlertDialog shadcn primitive.)
- Step 2: Build
ChangelogList
- Step 3: Build the detail page
<Tabs> content section using the existing shadcn Tabs primitive. Screenshot tab uses <img src={/api/marketplace/listings/} /> so requests go through the auth-gated worker route.
(Implementation pattern mirrors ui/src/pages/dashboard/...Detail.tsx files in the codebase — verify the closest analogue and follow its structure.)
- Step 4: Register route
App.tsx:
- Step 5: Commit
Task 19: MarketplaceRequests (tenant request history)
Files:
-
Create:
ui/src/pages/marketplace/MarketplaceRequests.tsx - Step 1: Build the page
useMarketplaceRequests(). State badge reuses RequestStateBadge.
- Step 2: Commit
Task 20: Superadmin Marketplace shell (listings + requests + pricing tabs)
Files:-
Create:
ui/src/pages/superadmin/SuperadminMarketplace.tsx -
Create:
ui/src/components/superadmin/MarketplaceListingEditor.tsx -
Create:
ui/src/components/superadmin/MarketplaceRequestRow.tsx - Step 1: Listings tab
marketplace_listings columns. Markdown preview pane using react-markdown. Screenshot upload drag-drop area uploads to a new /superadmin/marketplace/listings/:id/screenshots endpoint (implement in Task 10 if not done — verify).
Release manager: small sub-form (“Add release” — version, summary, markdown body, isMajor checkbox) that POSTs to /superadmin/marketplace/listings/:id/releases. Show confirmation modal on isMajor: “This will email all subscribed clinics — continue?”
Publish action: prominent button when status is draft. Confirmation modal: “Publishing will make this visible to all eligible tenants. Send launch announcement email? [checkbox]” → flips status + optionally fires G1.
- Step 2: Requests tab
- Step 3: Pricing tab
- Step 4: Register route in App.tsx
- Step 5: Commit
Task 21: Superadmin nav entry + permissions
Files:-
Modify: superadmin nav (verify path — likely
ui/src/components/layout/SuperadminSidebar.tsxornav-registry.tswith role filter) - Step 1: Add the entry
Store from lucide. Label: “Marketplace”. Order: between Tenants and Pricing.
- Step 2: Commit
Phase 4 — Docs & Smoke
Task 22: Update API reference
Files:-
Modify:
docs/api-reference.md - Step 1: Append a “Marketplace” section
- Step 2: Commit
Task 23: End-to-end smoke test (against dev / test tenant ssh & Associates)
Files: (no new files — verification only)
- Step 1: Confirm migration applied to dev DB
SELECT id, status, category FROM app.marketplace_listings ORDER BY sort_order; should return 8 rows, all draft.
- Step 2: Publish one listing via superadmin UI
/superadmin/marketplace. Select MRN. Set tagline, hero color, what-you-get bullets, what’s new release. Publish. (Skip launch-email checkbox for the smoke test to avoid sending to real clinics.)
- Step 3: Tenant flow
/dashboard/marketplace. Verify MRN card renders, click into detail, click Subscribe, see confirmation modal, confirm, see state pill flip to “Pending review”.
- Step 4: Superadmin invoice + payment + approve
clinic_modules row for MRN flipped to is_enabled = true. Verify G4 “live” email captured.
- Step 5: Tenant cancel flow
cancelled, clinic_modules.isEnabled = false.
- Step 6: Cron simulation
runMarketplaceOverdueInvoiceCron and runMarketplaceIdleNudgeCron from a scratch tsx script. Verify they’re idempotent on second run (no duplicate emails).
- Step 7: Final commit
Self-Review
Spec coverage:| Spec section | Tasks |
|---|---|
| §2 Lifecycle | T8, T11 |
| §3 Schema (4 tables) | T1–T4, T6 |
| §4.1 Tenant routes | T9, T14 |
| §4.2 Superadmin routes | T10, T11 |
| §4.3 Cron jobs | T13 |
| §4.4 Security (rate limits, audit, R2 auth-gated) | T8, T9, T10, T11, T14 |
| §5.1 Tenant UI (index/detail/requests) | T17, T18, T19 |
| §5.2 Superadmin UI | T20, T21 |
| §6 Emails (6 templates) | T12 |
| §7 Security checklist | T7 (perms), T9 (rate limits), T11 (state guards), T14 (auth-gated screenshots) |
| §8 Migration | T6 |
| §9 File-level deliverables | covered |
(verify path) notes are explicit dev-time checks, not unfinished work.
Type consistency: MarketplaceListing.subscriptionState literal-union matches TenantStatus.fromDb output. State enum strings match between schema, server lib, and frontend hook types. addonType enum widening is consistent across schema, route handlers, and frontend.
Risk notes for the executor:
- Postgres
ALTER TYPE ... ADD VALUEcannot run inside a transaction — apply the migration in autocommit mode. marketplace-usage.tsreferences tables that may not exist (ipd_admissions,insurance_claims,marketing_campaigns) — guard with try/catch returning null.- Test-helper functions (
seedListing,seedRequest,superadminRequest,tenantRequest,captureEmails) referenced in tests may need to be added if missing inserver/src/test-utils.ts. - Never run the migration against production without explicit confirmation per the
feedback_no_live_tenant_executionmemory rule. - Identifier is always
ssh— never write the other one anywhere.

