Workspace / org boundary
Under the 2026-05-26 hierarchy revision, Workspace = brand-tenant and Location = outlet. The “workspace-vs-org” distinction has collapsed for in-tenant writes — a workspace operator on a workspace page IS the brand-tier admin for that brand. The scope picker is the chip-strip’s ?location= URL state, not a separate route.
This page is the developer-side reference for how the scope boundary is expressed end-to-end. The operator-facing description is at workspace vs organization.
The rule
The chip-strip is the scope picker. Brand-tier writes happen from the same workspace page as outlet writes — but they omit
X-Workspace-Idon the wire.
In effect:
- Chip = “All outlets” (or a single-outlet brand) → brand-tier write
- Chip = a specific outlet → outlet-tier override
- The backend separator stays: brand-tier writes omit
X-Workspace-Id; outlet writes carry bothX-Workspace-Idand alocation_idbody field - Cross-brand mismatches (a request carrying
X-Workspace-Idwhile declaringscope_tier='org') are still rejected with403— that combination is malformed and indicates a UI bug
The pre-P10.1 /dashboard/defaults route is deleted. next.config.ts 301-redirects /dashboard/defaults/* → /dashboard/portfolio for stale bookmarks. Brand-tier authoring lives inside each product’s workspace page.
Enforcement points
The boundary is enforced at three layers.
1. Backend (API guard)
Every CRUD endpoint for a Shape A or Shape B substrate (see inheritance patterns below) checks the cross-brand mismatch:
# pattern
def create_thing(payload, workspace_id, ...):
if payload.scope_tier == 'org' and workspace_id:
raise HTTPException(403, "Cannot write org-tier with X-Workspace-Id header")
...PATCH / UPDATE on an existing org-tier row from a workspace context returns 403.
The agency-mode cross-brand boundary (operators jumping across brands via the Clerk org switcher cannot mix two brands’ data on one page) is enforced platform-wide — the /dashboard/portfolio route is the only legitimate cross-brand surface.
2. Frontend (UI affordance)
The dashboard surfaces brand-tier and outlet-tier authoring on the same product page. The chip-strip at the top of every workspace page selects scope:
- Chip = “All outlets” (or single-outlet brand) — brand-tier authoring cards mount, edits write to the brand row
- Chip = a specific outlet — outlet-tier override editor mounts on the same page, edits write to the outlet row
- The two are mutually exclusive on a single page render, so operators never see ambiguous “which tier am I editing?” state
For multi-row scoped substrates (Shape B, see below), the create/edit dialog includes a scope picker on the workspace surface but hides it on the brand surface (where only brand tier exists).
Inheritance patterns
Settings come in two shapes; the boundary applies differently to each.
Shape A — single record per tier (cascading value)
Substrate: one row per (org_id, workspace_id, property_id) triplet. Each tier optionally overrides selected fields. Reads resolve to the most-specific row.
Example: tenant_review_settings — every org/workspace/property has at most one row of nullable knobs.
UX: Brand-tier writes happen on each product’s workspace page when the chip-strip has no outlet selected. Outlet-tier (property) writes happen on the per-product Settings tab (e.g. /feedback → Settings) — and a single legacy InheritancePicker on the feedback settings tab points at the product’s workspace page for brand-tier edits.
Read resolution picks the most-specific row at the cell-plane layer and falls back to the brand defaults stored on the organization record if no row exists at any tier.
Shape B — multi-row scoped table
Substrate: arbitrarily many rows; each row owns its own (tenant_id, workspace_id, property_id) triplet. A workspace can have N rows; each row belongs to exactly one tier.
Examples: loyalty_earning_rules, external_review_platform_configs.
UX: NOT InheritancePicker. The right pattern is:
- Scope badge on each row (Org / Workspace / Property)
- Scope filter above the table (All tiers / Org defaults / This workspace / Specific property)
- Scope selector in the create / edit dialog (“Apply this rule at: …”)
The brand-tier authoring surface for Shape B substrates lives on the product’s own workspace page as a table-style card. The card reuses the workspace-surface create/edit dialog but hides the scope picker (only brand tier exists here) and writes to the brand row without an X-Workspace-Id header.
Decision rule
| If the setting is… | Use… |
|---|---|
| ”Set once, override per scope” | Shape A → InheritancePicker |
| ”Multiple records, each scoped” | Shape B → table with scope badge + filter + selector |
Mistaking one for the other is the most common drift source. Decide BEFORE designing UI.
Channels are workspace-scoped — never org-tier
Outbound channels (WhatsApp, SMS, email, RCS) cannot have org-tier defaults that cascade. The setup that makes a channel work is workspace-bound by the upstream provider:
- WhatsApp: each workspace registers its own WABA number with Meta — there’s no “shared org WhatsApp account” Meta will allow
- SMS: DLT sender ID is registered per-business with TRAI — every workspace must have its own
- Email: DKIM / SPF / domain verification is bound to a single sender domain per workspace
- RCS: per-workspace carrier enablement
Therefore: there is no brand-tier channels card. Channel configuration lives only at /dashboard/{workspaceSlug}/settings/channels. Don’t reintroduce a brand-tier channels surface — it would mislead operators into thinking they could cascade something the provider doesn’t allow.
(Property-tier channels = in_app + push only — those ARE per-property because each property = one SDK install with its own VAPID / FCM / APNs keys.)
Historical scope-fix (May 2026)
A platform-wide sweep corrected two pre-existing CRUD bugs on multi-row scoped (Shape B) substrates: CREATE wasn’t stamping the workspace owner, and LIST was hiding brand-tier rows from workspace operators. After the fix, every historical rule shows up as “Brand default” in the new scope-badge column. No data migration is possible — we can’t infer the operator’s original intent. Tenants who relied on the old behaviour as if rules were workspace-scoped need to manually deactivate and re-create at the correct tier.
What’s next
- Workspace vs organization — operator-facing view
- Data model — broader account-model architecture
- Agency overview — the multi-org case (agency org → N client orgs)