diff --git a/enterprise-api-room-template-research/IW-1-enterprise-api-room-template-brief.md b/enterprise-api-room-template-research/IW-1-enterprise-api-room-template-brief.md new file mode 100644 index 0000000..3f2f563 --- /dev/null +++ b/enterprise-api-room-template-research/IW-1-enterprise-api-room-template-brief.md @@ -0,0 +1,90 @@ +# Enterprise API: "Create Mural from Template" — Research Brief + +**Author:** Willis Kirkham +**Date:** 2026-03-12 +**Linear:** IW-1 + +--- + +## Summary + +An enterprise customer wants to programmatically create rooms and populate them with template-based murals using an API key. The enterprise API already has room and mural creation endpoints, but they are only accessible to Mural global admins — not to enterprise customers. This is a deliberate scope restriction, not a bug or feature flag. Beyond this access restriction, no "create mural from template" endpoint exists, though the capability is fully implemented internally. If the business decides to proceed, the work is straightforward: create new narrow scopes so customers can access these endpoints, and build the missing template instantiation endpoint. Estimated effort is 2–4 days. + +--- + +## What the Customer Needs + +The customer wants to automate a workflow their teams currently perform manually: create a room in a workspace, then place a pre-built template mural inside it. This is a provisioning use case — standing up structured collaboration spaces at scale, triggered by an internal system (onboarding, project kickoff, training rollout). It must work with an API key (company-level authentication), not OAuth. The customer has an "admin user" whose identity can attribute the created content. + +--- + +## What Already Works + +| Customer Need | Enterprise API Status | Notes | +|---|---|---| +| Create a room in a workspace | Exists, but not customer-accessible | Requires an internal-only scope | +| Create a blank mural in a room | Exists, but not customer-accessible | Same scope restriction | +| Create a mural from a template | Does not exist | Capability exists in internal/public APIs | +| Authenticate with an API key | Fully supported | Standard enterprise API pattern | +| Attribute actions to a specific user | Fully supported | Every write endpoint uses an "owner email" field | + +The enterprise API's write endpoints follow a consistent pattern: the request includes an owner email address, the system resolves it to a real Mural user, and that user becomes the creator/owner of the resource. This pattern would carry over to template instantiation without architectural changes. + +--- + +## The Gap + +**Access gap.** The enterprise API splits endpoints into two tiers. Nine customer-facing scopes (SCIM, audit log, eDiscovery, reporting, etc.) can be assigned by any company admin. Five internal-only scopes are restricted to Mural global admins. Room creation, mural creation, and workspace management all sit behind a single broad internal-only scope that also governs workspace deletion, migration tools, and content transfer. A feature flag audit confirmed there are no additional gates — no feature flags, no conditional route registration, no entitlement checks. The scope restriction is the sole barrier. + +**Feature gap.** Even with access resolved, the enterprise API only creates blank murals. The internal and public APIs both support "create mural from template" as a single operation. The business logic is shared and reusable — the work is wiring it into the enterprise API surface, not building something new. + +--- + +## Feasibility & Complexity + +The room and mural creation endpoints already exist and work — they just need new narrow scopes so that enterprise customers (not only global admins) can use them. Every customer-facing endpoint in the enterprise API already follows this pattern: a feature-specific scope that company admins can assign when creating an API key. The work is adding scopes for room and mural operations, updating the key creation logic to make them available, and building the missing template instantiation endpoint. + +**Effort estimate:** 2–4 days for an engineer familiar with the codebase. This covers: new scope definitions, key creation logic updates, re-scoping existing routes, one new endpoint file for template instantiation, a request body schema, and tests. + +**Permission nuance:** The user specified in the owner email field must either be a member of the target workspace or the template must be in a company template collection. For enterprise customers whose admin user is a workspace member — the expected setup — this is straightforward. Worth validating with the customer. + +--- + +## Product Strategy Considerations + +This customer's request sits at the boundary between admin and product capabilities. They are asking for a content operation (create from template) through the admin channel (API key authentication), but the use case — automated workspace provisioning attributed to an admin user — is closer to infrastructure setup than day-to-day content creation. + +The internal-only endpoints already straddle this boundary. Room CRUD, workspace CRUD, and mural CRUD exist in the enterprise API today but are not customer-accessible. Adding template instantiation would extend this pattern. The narrow-scope approach keeps this selective — the scope model is granular enough to expose specific capabilities without opening the full set of internal operations. + +The precedent question is worth noting. If room and template operations become customer-accessible, customers may subsequently ask for workspace creation, mural archiving, and other internal-only operations. Whether this represents scope creep or an opportunity depends on how Mural wants to position API key-driven provisioning. + +There is also a documentation dimension. The existing internal-only endpoints are undocumented. Newly-exposed capabilities could follow the same pattern or be formally documented. Undocumented endpoints still become de facto contracts once customers depend on them — the distinction is primarily about support expectations and stability commitments. + +--- + +## If Proceeding + +1. **Scope decision.** Define the new narrow scopes needed (at minimum, one for room operations and one for mural/template operations). This gates all subsequent work. +2. **Customer validation.** Confirm the admin user has workspace membership in the target workspaces and that templates are accessible via workspace membership or company collections. +3. **Implementation.** Create new scopes, re-scope existing routes, build the template instantiation endpoint, and write tests. A template listing endpoint may also be needed for programmatic template discovery. +4. **Documentation decision.** Determine whether newly-exposed endpoints should be added to the developer portal or remain undocumented. + +--- + +## Appendix: Key Technical Details + +**The owner email pattern.** Every enterprise write endpoint requires an `ownerEmail` field. The system resolves it to a real Mural user, validates company membership, and uses that user as the actor. There is no synthetic or system user — all content creation is attributed to a real person. + +**The permission model.** Creating a mural from a template requires three checks: the user can create murals in the target room, the user has not exceeded active mural quotas, and the user has access to the template. Template access is granted via workspace membership, a public sharing link, the global flag, or company template collection membership. + +**The scope model.** Scopes are defined in a central registry and filtered at key creation time based on whether the user is a Mural global admin. Adding a new scope requires: defining it in the registry, adding it to the UI filter function, and updating route middleware. Existing routes can accept multiple scopes, so internal tools using the broad scope would continue to work. + +| Purpose | Path | +|---------|------| +| Enterprise API route registration | `api/src/api/enterprise/v1/index.ts` | +| Enterprise room creation | `api/src/api/enterprise/v1/rooms/create-room.ts` | +| Enterprise mural creation (blank) | `api/src/api/enterprise/v1/murals/create.ts` | +| Template instantiation logic (shared) | `api/src/api/murals/create-from-template/index.ts` | +| API key scope definitions | `api/src/api/common/api-key/scopes.ts` | +| API key scope filtering (UI) | `api/src/api/companies/api-keys/index.ts` | +| Template permissions | `api/src/api/can/entities/template.ts` | diff --git a/enterprise-api-room-template-research/IW-1-enterprise-api-room-template-research.md b/enterprise-api-room-template-research/IW-1-enterprise-api-room-template-research.md new file mode 100644 index 0000000..348392c --- /dev/null +++ b/enterprise-api-room-template-research/IW-1-enterprise-api-room-template-research.md @@ -0,0 +1,348 @@ +# IW-1: Enterprise API Support for Room & Template CRUD — Research Findings + +**Linear issue:** [IW-1](https://linear.app/mural/issue/IW-1/research-enterprise-api-support-for-room-and-template-crud) +**Date:** 2026-03-12 + +--- + +## Executive Summary + +Room and mural CRUD endpoints **already exist** in the enterprise API codebase — `POST /enterprise/v1/rooms/` creates rooms, and `POST /enterprise/v1/murals/` creates blank murals. However, these endpoints are **not customer-accessible today**. They require the `manage:company:all` API key scope, which is restricted to MURAL global admins at key creation time. Regular enterprise customers cannot create API keys with this scope through the UI. + +This is a deliberate product architecture, not a bug. The enterprise API is split into two tiers: + +- **Customer-facing** (documented): Admin capabilities like Members, Audit Log, SCIM, eDiscovery, and Reporting — gated by narrow scopes that any company admin can assign to their API keys. +- **Internal-only** (undocumented): Provisioning capabilities like room/workspace/mural CRUD — gated by `manage:company:all`, available only to global admins. + +There are **no feature flags** controlling this split. No conditional route registration, no entitlement checks, no plan/tier gating. The scope restriction at API key creation is the sole mechanism. + +This means the customer's use case — "programmatically create rooms and add a template mural" — has **two gaps**, not one: + +1. **Access gap:** The existing room/mural CRUD endpoints require a scope customers cannot self-assign. A product decision is needed on whether to expose these endpoints to customers (via new narrow scopes) or provide the capability through a different mechanism. +2. **Feature gap:** Even with access, the enterprise API only creates blank murals. There is no "create mural from template" endpoint. The internal codebase has this capability in the public API (`POST /api/public/v1/templates/:templateId/murals`) and the internal API (`POST /api/v0/templates/:templateId/murals`), both using well-understood patterns. + +The core design concern — that rooms and templates are tightly coupled to users — is addressed by the enterprise API's established `ownerEmail` pattern. Every enterprise write endpoint already requires an `ownerEmail` field, resolves it to a real user, and uses that user as the `creatorId`/`ownerId`. This pattern would carry over to template instantiation without architectural changes. **The recommendation is (b): feasible with moderate changes**, but requires a product decision on scope strategy before implementation. + +--- + +## Detailed Findings + +### 1. Room Data Model & User Coupling + +**Model location:** `data/src/data/models/room/types.ts` + +Rooms have these user-related fields: + +| Field | Type | Required | Meaning | +|-------|------|----------|---------| +| `ownerId` | `string` | Yes | **Workspace** username — scopes the room to a workspace, not a user (code comment: `// Workspace username`) | +| `creatorId` | `string` | No | Username of the user who created the room | +| `updatedBy` | `string` | No | Username of the user who last updated the room | + +Rooms are scoped to **workspaces**, not users. `ownerId` is the workspace identifier. User association is through `creatorId` (who created it) and room membership (who has access). + +**Room creation in the API layer:** + +- **Internal API:** `POST /:workspaceId/rooms` in `api/src/api/workspaces/index.ts` (line ~885) +- **Enterprise API:** `POST /enterprise/v1/rooms/` in `api/src/api/enterprise/v1/rooms/index.ts` +- **Public API:** `POST /api/public/v1/rooms` in `api/src/api/public/v1/endpoints/rooms/index.ts` + +**Permission checks for room creation:** + +1. Workspace must be active +2. Caller must be an active workspace member +3. Member must have `createRooms` permission (legacy) or RBAC `can.createRoomIn.workspace` +4. Company entitlement check + +See `api/src/api/can/entities/workspace/index.ts` (lines ~141-183) and `business/src/business/rules/workspace/index.ts` (lines 67-73). + +**Enterprise API room creation already works.** `api/src/api/enterprise/v1/rooms/create-room.ts` resolves `ownerEmail` → user → `owner.username`, and passes it as `ownerId` to the shared `createRoomCore`. The API key provides company-level auth; the `ownerEmail` provides user-level attribution. + +### 2. Template Data Model & User Coupling + +**Model location:** `data/src/data/models/template/types.ts` + +Templates have these user-related fields: + +| Field | Type | Required | Meaning | +|-------|------|----------|---------| +| `ownerId` | `string` | Yes | Workspace username (same pattern as rooms, but no explicit comment in the template types file) | +| `creatorId` | `string` | No | Username of template creator | +| `lastUpdatedBy` | `string` | No | Username of last editor (JSDoc: "Points to `user.username`") | +| `members` | `{ [username: string]: { owner: boolean; name?: string; surname?: string; username: string } }` | Yes | Template members — inline type with ownership flag and display name fields | + +Templates follow the same scoping pattern as rooms: `ownerId` = workspace, `creatorId` = user. + +**"Add a template mural to a room"** is a single operation: `POST /api/v0/templates/:templateId/murals` with `{ targetRoomId, title }`. This creates a new mural in the target room using the template's content. It is not a two-step process. The flow: + +1. Load template by `templateId` +2. `createMuralFromTemplate()` (`api/src/api/murals/create-from-template/index.ts`) builds the mural payload and calls `createNewMural()` (`api/src/core/mural/new.ts`, line ~80) which sets: + - `creatorId: user.username` + - `ownerId: targetWorkspace.username` + - `room: targetRoom.id` + - Widget owners are reassigned to the creator via `canReassignOwner(widget)` (line ~131) +3. Dispatches async operations: `CREATE_BOARD`, `CREATE_MURAL_CONTENT`, etc. + +**Permission checks for template instantiation** (applied at the route level in `api/src/api/templates/index.ts`, line ~384): + +1. `can.createMuralIn.room` — user must be able to create murals in the target room (defined in workspace permissions module) +2. `can.createActive.mural` — active mural creation quota (defined in workspace permissions module) +3. `can.createFrom.template` or `can.createFromGlobal.template` — route branches based on `template.global`: + - **Global templates** use `can.createFromGlobal.template` (only requires `template.publicHash || template.global`) + - **Non-global templates** use `can.createFrom.template` (defined in `api/src/api/can/entities/template.ts`) which requires **any of**: workspace membership (`workspace.$me`), template `publicHash`, template `global` flag, or membership in a company template collection (`companyTemplateCollections.length > 0`) +4. MFA enforcement when required + +### 3. Enterprise API Access Architecture + +#### Authentication + +API key via `Authorization: apikey ` or `Authorization: bearer `. Implemented in `api/src/api/common/api-key/authentication.ts`. Authentication performs key decode, company lookup, and optional tenant loading. It does **not** check any company features, entitlements, or plan tiers. + +#### Actor Attribution — the `ownerEmail` Convention + +Every enterprise API write endpoint uses the same pattern: + +``` +Request body includes `ownerEmail` + → getUserByEmail(tenant, ownerEmail) + → Validate user belongs to same company as API key + → Use user as the actor for the operation +``` + +Evidence from three traced endpoints: + +1. **Room create** (`enterprise/v1/rooms/create-room.ts` line 16): `owner = getUserByEmail(tenant, roomData.ownerEmail)` → `ownerId: owner.username` +2. **Mural create** (`enterprise/v1/murals/create.ts` line 19): `owner = getUserByEmail(tenant, data.ownerEmail)` → `createMuralForUser(tenant, apiKey, owner, ...)` +3. **Workspace create** (`enterprise/v1/workspaces/create.ts`): uses `ownerEmail` in payload schema → workspace `ownerId` from resolved user + +There is **no synthetic/system user** in the enterprise API. `createdBy`/`ownerId`/`creatorId` are always from a real user resolved via `ownerEmail`. + +#### Two-Tier Scope Model + +The enterprise API has a deliberate two-tier access model enforced entirely through API key scopes. There are no feature flags, no conditional route registration, and no entitlement checks (API keys bypass entitlements entirely — see `api/src/api/can/entitlements.ts` lines 43-45). + +**Tier 1 — Customer-facing (documented):** Narrow, feature-specific scopes that any company admin can assign when creating an API key. + +| Scope | Capability | +|-------|-----------| +| `scim_integration` | SCIM identity management | +| `identify:murals` | eDiscovery — identify murals | +| `export:murals` | eDiscovery — export murals | +| `provisioning:members:get` | Member data and stats | +| `activity_logs` | Audit log access | +| `manage_content` | Content ownership transfer | +| `manage_user_types` | Manage member user types | +| `reports` | Company-level reports | +| `delete:murals` | Soft/hard delete murals | + +**Tier 2 — Internal-only (undocumented):** Restricted scopes that customers cannot self-assign. Controlled by `getEnabledApiKeyScopes()` in `api/src/api/companies/api-keys/index.ts` (lines 103-133). + +| Scope | Capability | Restriction | +|-------|-----------|-------------| +| `manage:company:all` | Room/workspace/mural CRUD, migration | **Global admin only** | +| `manage:data-retention` | Data retention policies | **MURAL company + global admin only** | +| `archive:murals` | Archive/unarchive murals | **MURAL company + global admin only** | +| `mapping_groups` | Workspace-to-group mapping | Requires `company.settings.enableMapping` | +| `contentful_integration` | Contentful template sync | Requires `company.settings.enableContentfulApiKey` | + +Authorization is handled by `apiKeyScoped(requiredScopes)` in `api/src/api/common/api-key/authorization.ts`, which performs exact scope matching (no wildcards) and key hash validation. No additional conditional logic beyond this. + +#### What This Means for the Customer + +The room/workspace/mural CRUD endpoints all require `manage:company:all`. A customer cannot create an API key with this scope — the UI's `getEnabledApiKeyScopes()` function filters it out for non-global-admin users. The routes are unconditionally registered (no feature flags, no config gates), so the only barrier is scope assignment at key creation time. + +This aligns with MURAL's [published API docs](https://developers.mural.co/public/docs/public-vs-enterprise), which describe the enterprise API as providing "admin capabilities" (Members, Audit Log, SCIM, eDiscovery, Reporting) while "product capabilities" like creating murals and rooms are in the public API. + +#### Endpoint Inventory by Access Tier + +**Customer-accessible** (documented, narrow scopes): + +| Endpoint Group | Scope | +|----------------|-------| +| Members | `provisioning:members:get`, `manage_user_types` | +| Audit Log | `activity_logs` | +| SCIM | `scim_integration` | +| eDiscovery (identify/export) | `identify:murals`, `export:murals` | +| Reporting | `reports` | +| Content Ownership Transfer | `manage_content` | +| Mural delete/remediate | `delete:murals` | + +**Internal-only** (undocumented, restricted scopes): + +| Endpoint Group | Scope | Routes | +|----------------|-------|--------| +| Rooms (all CRUD + members) | `manage:company:all` | `GET /by-external-ids`, `POST /`, `PUT /:roomExternalId`, members CRUD | +| Workspaces (CRUD + members) | `manage:company:all` | `GET /by-external-ids`, `POST /`, `PUT /:workspaceId`, `DELETE`, members CRUD | +| Murals (CRUD + members) | `manage:company:all` | `GET /by-external-ids`, `POST /`, `PUT /:muralExternalId`, members CRUD, `POST /import-files/migration` | +| Murals (archive) | `archive:murals` | `PUT /:muralId/archive` | +| Migration | `manage:company:all` | `POST /report` | +| Data Retention | `manage:data-retention` | `GET /murals` | +| Mapping | `mapping_groups` | `GET/PATCH /groups` (also requires `company.settings.enableMapping`) | + +### 4. Gap Analysis + +There are **two distinct gaps** between the current state and the customer's use case: + +#### Gap 1: Scope Access (Product Decision Required) + +The existing room and mural CRUD endpoints require `manage:company:all`, a scope that customers cannot self-assign. This is the primary barrier. The endpoints are fully functional — no feature flags, no code gates, no entitlement checks — but the scope restriction at API key creation makes them internal-only. + +Resolving this requires a product decision: + +| Option | Approach | Tradeoffs | +|--------|----------|-----------| +| A | Grant specific customers a `manage:company:all` key via internal process | Quick, no code changes; but broad scope gives access to all CRUD endpoints, not just what the customer needs | +| B | Create new narrow scopes (e.g., `manage:rooms`, `manage:murals:create`) and re-scope the endpoints | Follows existing pattern of narrow scopes; requires code changes to route middleware and `getEnabledApiKeyScopes()` | +| C | Build on the public API instead of the enterprise API | Public API already has room creation and template instantiation; but uses OAuth (user-level auth), not API keys (company-level auth) | + +Option B aligns best with the existing architecture — documented endpoints all use narrow scopes. It also provides a path to selectively expose room creation and template instantiation without exposing workspace deletion, migration, or other admin-heavy operations. + +#### Gap 2: Missing "Create Mural from Template" Endpoint + +Even with scope access resolved, the enterprise API only creates blank murals. There is no `templateId` parameter anywhere in the enterprise API surface. + +**What would a `POST /enterprise/v1/templates/:templateId/murals` endpoint need?** + +| Requirement | How Enterprise API Handles It | Difficulty | +|-------------|-------------------------------|------------| +| Authenticate request | API key auth — already exists | None | +| Attribute to a user | `ownerEmail` in body → `getUserByEmail()` | None (reuse pattern) | +| Load template | `load.template.fromParams()` — exists in internal API | Low (reuse loader) | +| Permission: create mural in room | `can.createMuralIn.room` — exists | Low (need to check if enterprise API key can satisfy this) | +| Permission: create from template | `can.createFrom.template` — requires workspace membership, public/global template, or company template collection membership | Medium (see below) | +| Business logic | `createMuralFromTemplate()` in `api/src/api/murals/create-from-template/index.ts` | Low (reuse existing) | + +**Permission barrier:** The `can.createFrom.template` check requires **any of**: workspace membership (`workspace.$me`), template `publicHash`, template `global` flag, or the template being in a company template collection. For the enterprise API, the `ownerEmail` user would need to satisfy at least one of these conditions. The most common paths are: (a) the user is a member of the target workspace (same constraint as room creation — already works), or (b) the template is in a company template collection (which may already be the case for enterprise customers). If the customer's "admin user" is a member of the relevant workspaces, or the template is in a company collection, this is not a blocker. + +**Patterns for userless operations:** The codebase has a `'WORKER'` actor concept (`workerAuditEntity()` in `api/src/core/operations/audit-operation.ts`) used for system jobs and data retention. There is also an impersonation pattern for global admins (`api/src/core/operations/impersonate-user.ts`). Neither would be needed here — the `ownerEmail` pattern is sufficient. + +### 5. Existing Solutions Check + +**What already exists in the enterprise API:** + +- Room creation: **Fully supported.** `POST /enterprise/v1/rooms/` with `ownerEmail`, `workspaceId`, `title`, etc. +- Mural creation: **Supported, but blank murals only.** `POST /enterprise/v1/murals/` with `ownerEmail`, `roomId`, `title`, dimensions, etc. No `templateId` parameter. +- Template instantiation: **Not supported.** No template-related endpoints exist in the enterprise API directory (`api/src/api/enterprise/v1/`). The only template reference is a `canPublishTemplates` permission flag in group mapping. + +**Code reuse potential:** High. The enterprise mural creation (`enterprise/v1/murals/create.ts`) already calls `createMuralForUser()` from `api/src/api/murals/create/index.ts`. The template instantiation logic in `api/src/api/murals/create-from-template/index.ts` follows the same structure. The new endpoint would combine the enterprise API's `ownerEmail` resolution with the existing template instantiation flow. + +**Published docs confirm the gap:** +- [Public vs Enterprise API comparison](https://developers.mural.co/public/docs/public-vs-enterprise): Enterprise API is for "admin capabilities" while Public API has "product capabilities" like creating murals and rooms. The scope audit confirms this distinction is enforced in code — product-level CRUD endpoints exist in the enterprise API but are gated behind `manage:company:all` to keep them internal. +- The enterprise API reference does not list template endpoints or the room/mural/workspace CRUD endpoints. + +--- + +## Architecture Diagram + +``` +CURRENT STATE: +============== + +Enterprise API Key Scopes (what customers can assign): +┌──────────────────────────────────────────────────────────┐ +│ Tier 1 — Customer-facing (9 narrow scopes) │ +│ scim_integration, identify:murals, export:murals, │ +│ provisioning:members:get, activity_logs, manage_content,│ +│ manage_user_types, reports, delete:murals │ +└──────────────────────────────────────────────────────────┘ + +Enterprise API Key Scopes (global admin only): +┌──────────────────────────────────────────────────────────┐ +│ Tier 2 — Internal-only (5 restricted scopes) │ +│ manage:company:all, manage:data-retention, │ +│ archive:murals, mapping_groups, contentful_integration │ +└──────────────────────────────────────────────────────────┘ + + +Customer Server (today) + │ + ├── Enterprise API (Tier 1 scopes) ──── Members, SCIM, Audit Log, etc. ✅ + │ + ├── Enterprise API (Tier 2 scope) ───── Room CRUD 🔒 (exists, not accessible) + │ POST /enterprise/v1/rooms/ requires manage:company:all + │ + ├── Enterprise API (Tier 2 scope) ───── Blank Mural CRUD 🔒 (exists, not accessible) + │ POST /enterprise/v1/murals/ requires manage:company:all + │ + └── Enterprise API ──────────────────── Mural from Template ❌ (does not exist) + + +PROPOSED STATE (Option B — new narrow scopes): +============================================== + +Customer Server + │ + ├── Enterprise API ──── Room CRUD ✅ (new scope, e.g. manage:rooms) + │ + └── Enterprise API ──── Mural from Template ✅ (new endpoint + scope) + POST /enterprise/v1/templates/:templateId/murals + { ownerEmail, roomId, title } + │ + ├── getUserByEmail(ownerEmail) ── reuse + ├── load template by ID ── reuse + ├── permission checks ── adapt from public API + └── createMuralFromTemplate() ── reuse +``` + +--- + +## Recommendation + +**(b) Feasible with moderate changes**, but requires a product decision on scope strategy first. + +The enterprise API has room creation, mural creation, and a well-established `ownerEmail` actor attribution pattern — all the building blocks exist. The business logic for template instantiation is already shared between the internal and public APIs. The technical work is straightforward; the prerequisite is deciding how to expose these capabilities to customers. + +**Recommended approach (Option B from Gap Analysis):** Create new narrow scopes for room and mural operations, following the established pattern where documented endpoints use feature-specific scopes. This is the cleanest path because it lets you expose room creation and template instantiation without also exposing workspace deletion, migration tools, or other internal operations that share `manage:company:all`. + +**Estimated scope:** + +1. **Scope strategy** (product decision): Define new scopes (e.g. `manage:rooms`, `create:murals:from-template`) and update `getEnabledApiKeyScopes()` in `api/src/api/companies/api-keys/index.ts` to make them available to company admins — ~15 lines +2. **Re-scope existing routes** (if exposing room CRUD): Update `apiKeyScoped()` calls in `enterprise/v1/rooms/index.ts` to accept the new scope alongside `manage:company:all` — ~10 lines +3. **New route:** `POST /enterprise/v1/templates/:templateId/murals` — ~1 new file, following the pattern of `enterprise/v1/murals/create.ts` +4. **Schema:** New request body schema with `ownerEmail`, `roomId`, `title`, and optional fields — ~20 lines +5. **Permission adaptation:** The existing `can.createFrom.template` check needs to work with an API key + resolved user, similar to how room creation already works — may need minor adjustment +6. **Tests:** Enterprise API test patterns exist for rooms and murals; follow same structure + +**Effort estimate:** Small-to-medium (2-4 days for an engineer familiar with the codebase, including the scope changes). + +--- + +## Next Steps + +1. **Product decision on scope strategy** — Choose between: (A) granting `manage:company:all` keys to specific customers via internal process, (B) creating new narrow scopes to selectively expose room/mural CRUD, or (C) directing the customer to the public API. Option B is recommended. +2. **Validate with the customer** that the `ownerEmail` pattern is acceptable — they will need a real MURAL user to attribute operations to (their "admin user" mentioned in the Slack thread) +3. **Check if the customer's admin user has appropriate workspace membership** and `createRooms` / mural creation permissions in the target workspaces +4. **Implement scope changes** — Add new scopes to `API_KEY_SCOPES` in `api/src/api/common/api-key/scopes.ts`, update `getEnabledApiKeyScopes()` to make them available, and update route middleware to accept them +5. **Prototype the template endpoint** by copying the pattern from `enterprise/v1/murals/create.ts` and adapting `createMuralFromTemplate` from `api/src/api/murals/create-from-template/index.ts` +6. **Consider also adding:** Template listing endpoint in the enterprise API so the customer can discover available templates programmatically (`GET /enterprise/v1/templates`) +7. **Documentation:** Update the enterprise API reference at developers.mural.co — both the new endpoint and any newly-exposed existing endpoints + +--- + +## Key File References + +| Purpose | Path | +|---------|------| +| Top-level route mounting | `api/src/api/endpoints.ts` | +| Enterprise API routes (sub-router registration) | `api/src/api/enterprise/v1/index.ts` | +| Enterprise room routes + middleware | `api/src/api/enterprise/v1/rooms/index.ts` | +| Enterprise room creation handler | `api/src/api/enterprise/v1/rooms/create-room.ts` | +| Enterprise mural routes + middleware | `api/src/api/enterprise/v1/murals/index.ts` | +| Enterprise mural creation handler (no template) | `api/src/api/enterprise/v1/murals/create.ts` | +| Enterprise workspace routes + middleware | `api/src/api/enterprise/v1/workspaces/index.ts` | +| Enterprise mapping routes + `can.use.mapping()` gate | `api/src/api/enterprise/v1/mapping/index.ts` | +| API key scope definitions | `api/src/api/common/api-key/scopes.ts` | +| API key authentication middleware | `api/src/api/common/api-key/authentication.ts` | +| API key authorization (`apiKeyScoped()`) | `api/src/api/common/api-key/authorization.ts` | +| API key creation + `getEnabledApiKeyScopes()` | `api/src/api/companies/api-keys/index.ts` | +| Entitlement bypass for API keys | `api/src/api/can/entitlements.ts` | +| `can.use.mapping()` definition | `api/src/api/can/index.ts` | +| Template instantiation route (internal API) | `api/src/api/templates/index.ts` (line ~384) | +| Template instantiation logic (shared) | `api/src/api/murals/create-from-template/index.ts` | +| Template instantiation route (public API) | `api/src/api/public/v1/endpoints/templates/index.ts` | +| New mural from template logic | `api/src/core/mural/new.ts` | +| Room data model | `data/src/data/models/room/types.ts` | +| Template data model | `data/src/data/models/template/types.ts` | +| Template permissions | `api/src/api/can/entities/template.ts` | +| Room permissions | `api/src/api/can/entities/workspace/index.ts` | +| Worker actor pattern | `api/src/core/operations/audit-operation.ts` |