ADR-0023: A scoped Sales workspace on the existing CRM
Status: Implemented. Rolled out behind sales-workspace-v2; that flag has since been retired and Sales is permanent for every organization.
Amended by: ADR-0025 — a trial scheduled into a class session holds a seat there.
Date: 2026-10-04
Context
A studio receiving approximately 150 leads daily needs staff beyond the owner and admins to work leads without granting broad membership, payment, or organization administration. Coaches today have no CRM access, and the existing owner/admin/coach/member role matrix cannot express “this coach works only their assigned leads”. Leads, memberships, course entitlements, conversations, tasks and the automation engine already exist.
Decision
Keep the existing role enum. Sales is a staff capability. Add one organization-scoped Sales grant per active coach membership, with own or team scope and a manager bit. Owner/admin keep implicit team-manager access. A coach receives no new access without a grant. A member (a customer) can never hold Sales access: the API refuses to grant it, and a grant row left behind by a role change gives no access. Only owner/admin may change grants; a Sales manager may distribute work and use reports/settings but cannot grant access.
Use the single organization-level PostHog flag sales-workspace-v2. Absent, false or failed evaluation retains the legacy UI and makes the new Sales APIs unavailable. Evaluate on organization ID with the organization group. Operational automation kill switches and existing unrelated feature flags retain their existing purpose. (Retired: flag removed in #531; Sales workspace is unconditional.)
Expose scoped APIs under /organizations/:orgId/sales-workspace. Authorization checks the active membership, organization, grant and lead owner at the server. A query filter is never authorization. Member conversations and payment configuration remain outside Sales-only access. Mutating lead work locks its organization-lead record and validates the optimistic version. Stable operation IDs bind retries to their original request.
Keep the lead entity and its existing activity ledger. Typed result columns support honest effort reports; old contact notes remain unspecified. The existing lead follow-up task service owns contact-task synchronization. R6: lead tasks, including custom ones, stay in Tasks while the flag is on, visible within the viewer’s Sales scope (none without Sales access, owned leads’ tasks with an own grant, all with team scope). Tasks and the lead sheet link both ways and write lead tasks through one Sales lead-task path, which keeps the lead version and its follow-up consistent whichever surface made the change.
A conversation’s unread count and reply obligation are separate facts. Accepted human email or explicit acknowledgment resolves only the captured inbound message ID. Reading, a queued/failed send, bulk email and automated outreach cannot fulfill that obligation. Stable provider keys protect send retries; a stale in-flight claim may retry only within the provider’s idempotency window.
Trials are recorded appointments with explicit outcomes. They do not reserve a seat in a class (amended by ADR-0025: a trial in a class session holds a seat). A passed time creates outcome work; it is not proof of a no-show. Membership linkage, free course access and paid conversion are distinct. Payment reports derive cash from canonical settlement/refund records and expose missing historical attribution.
The queue is the single place lead work is recorded. Configurable pipelines stay, but inside Sales they appear on a read-only Board: exact active-lead counts per stage within the viewer’s scope, each stage opening the queue filtered to it. The work record drives the phase: results and conversion move a lead between stage categories (contacted, trial, lost, converted), placing it at the first stage of the new category in its pipeline. The queue row’s stage menu only refines the stage within the current category, as a scoped, versioned write. Pipeline/stage editing lives in Sales → Manage for owners and admins.
Consequences
No role-enum migration, replacement CRM, vendor synchronization layer or second automation engine is required. The new capability has a bounded administration surface. Every new route must use the Sales access service rather than assuming staff status.
During the rollout, rollback switched the flag off and retained the additive schema and history; it did not undo emails, completed tasks, assignments or membership changes. With the flag retired there is no flag rollback. Generated migrations still require review and owner-controlled production application. See the operations runbook.
The cost is continued ownership of queue semantics, concurrency, transport retries and financial attribution in Taikan. Capability checks must coexist with legacy role permissions and cannot be replaced by UI gating. Delivery callbacks for already-created messages must always be recorded, whatever entry points the product offers. Older unstructured history cannot support every new metric, so reports explicitly retain unknown/unspecified buckets.
The implementation uses row locks, optimistic versions and payload-bound operation IDs rather than adding an event-sourcing platform. Result, callback, pause, mirrored task and activity writes use one transaction/connection. External email is outside that transaction and requires its own persistent send claim and provider idempotency key. These mechanisms have different failure boundaries and must not be treated as one distributed transaction.
Alternatives considered
| Alternative | Decision |
|---|---|
| Give Sales staff admin access or add a new global Sales role | Reject: admin access exposes unrelated powers; a global role still does not express own/team lead scope. Attach a bounded capability to an active coach membership instead. |
| Let member-role people (for example a front-desk worker signed up as a member) hold Sales grants | Reject: member is the customer role. Offering every customer as a grant candidate made access management unreadable and one wrong click would expose leads to a customer. Someone who works leads gets a staff role first. |
| Buy a headless CRM and synchronize leads, memberships and payments | Defer: current entities and workflows already exist. Migration, identity reconciliation, recurring vendor cost and dual ownership exceed the benefit for this scope. Keep transport integrations behind existing adapters. |
| Build a second automation engine or adopt a workflow vendor | Reject for this increment: reuse enrollment, step execution and recovery, fixing targeting and lead-work consistency in place. |
| Replace configurable pipelines with fixed sales stages | Reject: pipeline position and the next piece of work are separate. Add queue reasons and commitments alongside existing pipelines. |
| Keep a drag-to-move pipeline board inside Sales | Reject: it was a second, unchecked way to progress leads. Drags bypassed the version and scope checks, a trial drop created no Sales trial, and stages drifted from queue results. The Board is read-only and records nothing. |
| Infer no-show from elapsed time or paid win from membership conversion | Reject: neither inference establishes the business fact. Require explicit attendance and canonical settlement evidence. |
| Cache/materialize every queue or introduce a search service | Defer: grouped joins, bounded pages, indexes and same-connection activity logging meet the measured local scale targets. Reconsider using staging/production evidence. |
| Split rollout into several new flags | Reject for this integrated behavior: one organization flag keeps queue, task scoping, access and inbox semantics aligned. Existing independent safety controls remain. |