Files
classroomio/prd/multi-workspace/README.md
T

235 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: Multi-Workspace (Multiple Orgs Under One Account)
overview: Allow a single customer account to operate multiple organizations ("workspaces") under one subscription. The customer pays in their primary workspace; secondary workspaces inherit the subscription, share pooled quotas (learners, AI tokens), and don't bill separately. Surfaces as a paid tier feature — Growth includes 2 workspaces, Business 3, Enterprise unlimited; extras as a paid add-on. Wedge story: "one account, multiple academies" — customer-facing + internal + partners under one bill.
todos:
- id: schema-decision
content: Decide between Option A (parentOrganizationId on organization) and Option B (separate account table). Recommendation: Option A for v1.
status: pending
- id: data-model
content: Add parentOrganizationId column to organization table; backfill existing orgs to NULL (primary)
status: pending
- id: billing-routing
content: Refactor Stripe subscription handling so the primary workspace holds the subscription and webhook events propagate state to all linked workspaces
status: pending
- id: pooled-quotas
content: Move learner-count and AI-token quota enforcement from per-org to per-account (sum across linked workspaces)
status: pending
- id: account-endpoints
content: Build POST/GET/DELETE /account/workspaces and GET /account/usage endpoints
status: pending
- id: account-settings-ui
content: Build account settings page accessible from primary workspace — list workspaces, per-workspace usage, "Create workspace" CTA
status: pending
- id: workspace-switcher
content: Update existing org switcher to clearly distinguish "workspaces in my account" vs "orgs I'm invited to"
status: pending
- id: secondary-billing-ui
content: Hide or alias billing UI in secondary workspaces — show "Billing managed by [primary workspace name]"
status: pending
- id: downgrade-grace
content: Implement 7-day read-only grace period when a customer downgrades below their current workspace count
status: pending
- id: free-tier-cap
content: Enforce 1-workspace cap on Free tier to prevent abuse
status: pending
- id: pricing-page-update
content: Update marketing pricing page to surface "Workspaces included" row and add-on pricing
status: pending
- id: homepage-section
content: Add "One account, multiple academies" section to apps/website homepage (after feature ships)
status: pending
isProject: true
---
# Multi-Workspace PRD
## Status
- Draft (2026-05-15)
- Owner: TBD
## Problem
Today, every ClassroomIO organization is its own billing entity. A customer who wants:
- A customer-facing academy on `learn.acme.com`, AND
- An internal employee training portal on `team.acme.com`
…must create two separate orgs with two separate paid subscriptions. This:
1. Doubles the cost for no clear product reason
2. Creates billing friction (two invoices, two payment methods)
3. Discourages expansion
4. Misses the wedge story — "one account, multiple academies"
## Goals
1. Let a customer create multiple workspaces (full orgs) under a single subscription
2. Enable centralized billing — one Stripe customer, one invoice
3. Share quotas (learners, AI tokens) across linked workspaces
4. Surface this as a paid tier feature: Growth includes 2 workspaces, Business 3, Enterprise unlimited; extras as paid add-on
5. Keep each workspace fully independent operationally (own brand, domain, members, courses, certificates)
## Non-goals (v1)
- Cross-workspace data sharing (a learner enrolling in courses across both workspaces with a single identity)
- Cross-workspace analytics roll-ups
- Reparenting a workspace to a different primary mid-subscription (manual support op for v1)
- Self-serve "split off" of a secondary workspace into its own subscription (manual for v1)
- Per-workspace seat limits separate from pooled quota
## Users & jobs
| Persona | Job |
|---|---|
| **Account owner** (founder / VP CX) | "I want a second academy for our internal team without paying for a second plan." |
| **Workspace admin** (delegated owner of a secondary workspace) | "I want to manage my workspace fully — branding, courses, members — without seeing billing." |
| **Learner** | No change. Sees only their workspace. |
## Key concepts
- **Account** — the billing entity. Tied to one Stripe customer. Owns 1+ workspaces.
- **Workspace** — a full `organization` row (custom domain, branding, members, courses). Each workspace has exactly one parent account.
- **Primary workspace** — holds the subscription. The first org a user creates becomes their primary by default.
- **Secondary workspace** — any workspace beyond the primary in the same account. Inherits subscription state.
## User stories
### Onboarding
- As an existing paid customer on Growth, I can create a second workspace from a "Workspaces" tab in account settings without entering payment info again.
- The new workspace counts against my plan's workspace allowance (Growth = 2 included).
- If I exceed the allowance, I'm prompted to add a paid workspace add-on or upgrade tier.
### Day-to-day
- I can switch between workspaces via the org switcher.
- I can see pooled usage (X / 1,000 learners, Y / 10M AI tokens) and a per-workspace breakdown.
- A workspace admin (not the account owner) sees their workspace normally; billing UI is hidden or shows "Managed by [primary workspace name]".
### Billing
- The Stripe subscription lives on the primary workspace.
- One invoice lists: base plan + workspace add-ons + overages.
- If the primary workspace cancels or fails payment, **all** linked workspaces are affected (downgrade to free or readonly per existing policy).
### Downgrade / overage handling
- If a customer downgrades from Growth (2 included) to Starter (1 included) while having 2 workspaces:
- All workspaces remain readable but lock to read-only until the customer either deletes a workspace or upgrades back.
- 7-day grace period to resolve.
- If a customer exceeds pooled learner quota, existing per-tier overage policy applies.
### Edge: primary owner leaves the company
- Support can manually transfer subscription ownership to another admin in the primary workspace. Self-serve in v2.
## Functional requirements
### Data model (proposed)
**Option A — direct parent pointer (recommended for v1):**
```
organization.parentOrganizationId → organization.id (nullable)
```
- Primary = `parentOrganizationId IS NULL`
- All workspaces with same parent (or themselves as parent) belong to one account
- ✅ Minimal migration
- ⚠️ Reparenting requires updating all children
**Option B — explicit account table (cleaner long-term):**
```
account (id, stripe_customer_id, plan_id, created_at)
organization.account_id → account.id
```
- ✅ Cleaner billing semantics; future-proof for transfers
- ⚠️ Migration: every existing org needs an account row
**Recommendation: Option A for v1.** Migrate to Option B in v2 if data-model strain shows up.
### Subscription / billing
- One Stripe subscription per account, on the primary workspace.
- Subscription line items:
- 1× base plan
- N× workspace add-on (N = total workspaces − tier's included count, ≥ 0)
- Overage line items
- Webhook handlers must propagate subscription state to all linked workspaces.
### Quota enforcement
- Learner count = SUM of active learners across all workspaces in account.
- AI token usage = SUM of tokens consumed across all workspaces in account.
- Quotas checked at the **account** level on every usage event.
- Per-workspace usage tracked for visibility, not enforced separately.
### Routes & API surface
**New endpoints:**
- `POST /account/workspaces` — create a secondary workspace (account-level auth required)
- `GET /account/workspaces` — list all workspaces in account with per-workspace usage
- `DELETE /account/workspaces/:id` — delete a secondary workspace (cannot delete primary while account exists)
- `POST /account/workspaces/:id/transfer-primary` — promote secondary to primary (v2)
- `GET /account/usage` — pooled usage breakdown
**Existing endpoints affected:**
- All `organization/*` endpoints already scope to the active workspace — no change
- Billing/subscription endpoints operate on the account/primary workspace
### Dashboard UI
1. **Workspace switcher** — existing; needs to scope to "workspaces in my account" plus invited-as-member orgs
2. **Account settings page** (new) — accessible from primary workspace only:
- List of workspaces with status, learner count, AI usage
- "Create workspace" button (tier-aware: "1 of 2 included" / "Add another for $79/mo")
- Billing tab moved here from per-workspace settings (or aliased)
3. **Per-workspace settings** — billing UI hidden on secondary workspaces; shows "Billing managed by [primary workspace name]"
4. **Pooled usage banner** — in account settings: "1,247 of 5,000 active learners across 3 workspaces"
## Pricing logic
| Tier | Workspaces included | Extra workspace |
|---|---|---|
| Free | 1 | – (must upgrade) |
| Starter $49 | 1 | – (must upgrade) |
| Growth $149 | 2 | +$79/mo each |
| Business $399 | 3 | +$149/mo each |
| Enterprise | Unlimited | included |
- Extras prorate normally.
- Annual subscriptions: workspace add-ons priced at 10× monthly (2 months free), matching plan annual logic.
## Success metrics
- **Activation:** % of paid customers who create a 2nd workspace within 90 days. Target: 20% by month 6, 30% by month 12.
- **Expansion ARR lift:** `workspace_addon_mrr / base_plan_mrr`. Target: 10%+ blended lift.
- **Retention:** Net retention on customers with 2+ workspaces vs 1. Hypothesis: 2+ workspace customers churn ~50% less.
- **Support tickets:** No more than 1 ticket per 10 workspace creations in v1.
## Rollout plan
### v1 (4 weeks)
- Schema: add `parentOrganizationId` to `organization`
- Backend: secondary workspace endpoints, pooled quota enforcement, billing updates
- Dashboard: account settings page, workspace switcher tweak, pooled usage banner
- Marketing: ship "one account, multiple academies" homepage section
- Ship to existing paid customers first (manual feature flag), then GA
### v2 (post-launch, scope TBD)
- Self-serve "split workspace into own subscription"
- Cross-workspace analytics roll-up
- Account-level RBAC (single user as admin across all workspaces without per-org invites)
- Migrate to `account` table (Option B) if data model strain emerges
## Open questions
1. **Should free-tier users be able to create multiple workspaces?** Recommend: no (1-workspace cap).
2. **Quota fairness:** if Workspace A burns all the AI tokens, Workspace B has zero — do we expose per-workspace caps as an optional admin control? Recommend: not in v1.
3. **Are workspaces transferrable between accounts?** v1: no, manual support. v2: yes, with consent flow.
4. **Subdomain vs custom domain per workspace:** already supported per-org — confirm SSO/auth flows scope correctly.
5. **Billing edge case:** when adding a paid workspace add-on mid-cycle, prorate immediately or at next renewal? Recommend: immediate prorate (Stripe default).
## Risks
- **Confusing UX** if terminology isn't tight — pick one term ("workspace" or "academy") and use it everywhere
- **Quota gaming** — customers stuffing free workspaces; mitigated by 1-workspace cap on Free
- **Per-workspace SSO config** — each workspace's OIDC config is independent; confirm no cross-pollination of identities
- **Backwards compat** — existing orgs become primary by default; migration is a single backfill (no-op, NULL is already default)
## Out of scope
- Cross-workspace shared course library
- Account-level admin role separate from per-workspace admin
- Public marketplace of shared courses across accounts