# Architecture Research

**Domain:** Content generation integration — Nexus v1.7
**Researched:** 2026-04-04
**Confidence:** HIGH (based on direct codebase inspection of /opt/nexus)

## Standard Architecture

### System Overview

```
+---------------------------------------------------------------------------------+
|                           UI Layer (React/Vite)                                 |
|  +------------------+ +------------------+ +--------------+ +----------------+  |
|  | ChatPanel        | | ContentJobViewer | | ThemePreview | | DiagramRenderer|  |
|  | (existing,       | | (new)            | | (new)        | | (new, wraps    |  |
|  |  minor extension)| | progress+result  | | CSS vars     | |  mermaid dep)  |  |
|  +--------+---------+ +--------+---------+ +------+-------+ +-------+--------+  |
|           |                    |                   |                 |           |
+-----------|--------------------|--------------------|-----------------|------------+
            | HTTP/SSE           | HTTP/SSE          | HTTP           | (client-side)
+-----------|--------------------|--------------------|-------------------------+
|           |       API Layer (Express)               |                          |
|  +--------v----------------------------------------------v------------------+  |
|  | /api/companies/:id/content-jobs   (new)                                   |  |
|  | /api/content-jobs/:id             (new)                                   |  |
|  | /api/companies/:id/themes/generate (new)                                  |  |
|  +-------------------------------------------------------------------------+  |
+---------------------------------------------------------------------------------+
|                       Service Layer (Node.js)                                   |
|  +-------------------+  +------------------+  +---------------------------------+|
|  | contentJobService | | themeEngineService| | renderPipelineService            ||
|  | (new)             | | (new)             | | (new)                            ||
|  | enqueue, status,  | | palette gen,      | | routes jobs to renderer adapters ||
|  | list              | | WCAG check,export | |                                  ||
|  +--------+----------+  +------------------+  +---------------+-----------------+|
|           |                                                    |                  |
|  +--------v----------------------------------------------------v--------------+  |
|  |                   Renderer Adapters (new, behind interface)                 |  |
|  |  +-------------+ +------------+ +----------+ +-----------+ +----------+    |  |
|  |  | Mermaid     | | SVG        | | Remotion | | PDF       | | Image    |    |  |
|  |  | (isomorphic)| | (generator)| | (CLI)    | | (Puppeteer| | (Sharp)  |    |  |
|  |  +-------------+ +------------+ +----------+ +-----------+ +----------+    |  |
|  +-------------------------------------------------------------------------+  |
+---------------------------------------------------------------------------------+
|               Storage + Events Layer (existing, minimally extended)             |
|  +------------------+  +--------------------+  +-----------------------------+  |
|  | StorageService   |  | publishLiveEvent   |  | assets table (existing)     |  |
|  | (existing)       |  | (existing +3 types)|  | content_jobs table (new)    |  |
|  +------------------+  +--------------------+  +-----------------------------+  |
+---------------------------------------------------------------------------------+
```

### Component Responsibilities

| Component | Responsibility | Status | Notes |
|-----------|----------------|--------|-------|
| `contentJobService` | Queue and track async render jobs; emit live events on status change | New | Factory function, matches `chatService` pattern |
| `renderPipelineService` | Route render requests to the correct renderer adapter | New | Strategy pattern over adapters |
| `themeEngineService` | Palette generation, WCAG AA validation, CSS/JSON/Tailwind exports | New | Pure computation, no DB, deterministic |
| `mermaidRendererAdapter` | Mermaid DSL string to SVG buffer, server-side | New | Uses `@mermaid-js/mermaid-isomorphic`; no Chromium needed |
| `remotionRendererAdapter` | Invoke Remotion CLI subprocess, return MP4/WebM path | New | Subprocess; outputs go to storage namespace `generated/videos` |
| `svgGeneratorAdapter` | Template-based SVG generation (icons, banners, placeholders) | New | No binary deps; pure string construction + existing sanitizer |
| `pdfRendererAdapter` | HTML to PDF via Puppeteer (arm64 Chromium on M4) | New | Subprocess; Puppeteer arm64 works on Apple Silicon |
| `imageProcessorAdapter` | Composite and resize via Sharp | Modified | Sharp already in `server/package.json`; extend for content use |
| `placeholderService` | Manifest tracking for draft assets | Existing | Already implemented; optionally extend PlaceholderEntry with `contentJobId` |
| `assetService` | CRUD for the `assets` table | Existing | Already handles `createdByAgentId`; use as-is |
| `StorageService` | Provider-agnostic blob storage | Existing | Use `generated/` namespace prefix for all new content |
| `publishLiveEvent` | SSE fan-out to UI subscribers | Existing | Extend `LIVE_EVENT_TYPES` with 3 new content job event types |
| `ContentJobViewer` (UI) | Poll/stream job status; show progress, render result inline | New | Subscribes to SSE live events |
| `DiagramRenderer` (UI) | Client-side Mermaid render using existing `mermaid` dep | New | `mermaid ^11.12.0` already in `ui/package.json` |
| `ThemePreview` (UI) | Live palette preview via CSS custom properties | New | No server round-trip for preview |
| `ContentGallery` (UI) | Workspace page showing all generated assets | New | Pagination via `assetService.list` |

## Recommended Project Structure

New files follow existing monorepo conventions: factory functions, co-located types, no class syntax.

```
server/src/
├── services/
│   ├── content-job.ts           # contentJobService factory
│   ├── render-pipeline.ts       # renderPipelineService — adapter dispatch
│   ├── theme-engine.ts          # themeEngineService — pure palette computation
│   └── renderers/
│       ├── index.ts             # RendererAdapter interface + barrel
│       ├── mermaid-renderer.ts  # Mermaid DSL -> SVG (server-side isomorphic)
│       ├── remotion-renderer.ts # Remotion CLI subprocess wrapper
│       ├── svg-generator.ts     # Template SVG (icons, placeholders, banners)
│       └── pdf-renderer.ts      # HTML -> PDF via Puppeteer
├── routes/
│   ├── content-jobs.ts          # GET/POST /companies/:id/content-jobs
│   └── themes.ts                # POST /companies/:id/themes/generate
└── types/
    └── content.ts               # Server-internal ContentJobType, ContentJobStatus

packages/db/src/
├── schema/
│   └── content_jobs.ts          # NEW table (upstream-safe, no upstream equivalent)
└── migrations/
    └── NNNN_add_content_jobs.sql

packages/shared/src/
├── types/
│   └── content.ts               # ContentJob, ContentJobStatus shared types
└── constants.ts                 # LIVE_EVENT_TYPES extended (+3 content.job.* types)

packages/
└── remotion-compositions/       # NEW workspace package
    ├── package.json
    └── src/
        └── index.ts             # Remotion composition definitions

ui/src/
├── components/
│   ├── ContentJobViewer.tsx     # Job progress + result display
│   ├── ContentJobCard.tsx       # Compact job status card
│   ├── DiagramRenderer.tsx      # Mermaid client-side wrapper
│   ├── ThemePreview.tsx         # Live palette preview
│   └── GeneratedAssetCard.tsx   # Thumbnail + download + metadata
└── pages/
    └── ContentGallery.tsx       # Gallery of generated assets per workspace
```

### Structure Rationale

- **`server/src/services/renderers/`**: Isolates binary-dependent adapters behind a shared `RendererAdapter` interface. New renderers plug in without touching the core job service.
- **`content_jobs` table**: Separate from `assets`. A job tracks render lifecycle (queued to running to done/failed); on success it writes an `assets` row and records the `assetId`. This mirrors how `heartbeat_runs` tracks execution separately from its outputs.
- **`packages/remotion-compositions/`**: Remotion compositions must be bundled ahead of time. Keeping them in a dedicated workspace package lets the bundle step run once at startup, not on every render request.
- **Content as skills**: Skills (`company_skills`) are Markdown instruction files. Content type skills tell agents which `/api/companies/:id/content-jobs` endpoint to call and with what parameters. No new schema needed.

## Architectural Patterns

### Pattern 1: Async Job with SSE Progress

**What:** Long-running renders (Remotion, Puppeteer PDF) run asynchronously. The service creates a `content_jobs` row with `status: "queued"`, immediately returns the job record, then spawns the renderer. Live events push progress to the UI over the existing SSE stream.

**When to use:** Any render taking more than ~200ms: Remotion, PDF, large Mermaid diagrams. Fast operations (SVG generation, theme palette) can be synchronous HTTP.

**Trade-offs:** One DB row per render. Adds durable history of what was generated. Acceptable at solo-user scale.

**Example:**
```typescript
// server/src/services/content-job.ts
export function contentJobService(db: Db, storage: StorageService) {
  return {
    async enqueue(companyId: string, input: ContentJobInput): Promise<ContentJob> {
      const [row] = await db
        .insert(contentJobs)
        .values({ companyId, type: input.type, params: input.params, status: "queued" })
        .returning();
      publishLiveEvent({ companyId, type: "content.job.started", payload: { jobId: row.id } });
      // Non-blocking — kick off render
      renderPipelineService(storage).render(row)
        .then(async (result) => {
          await db.update(contentJobs)
            .set({ status: "done", assetId: result.assetId, completedAt: new Date() })
            .where(eq(contentJobs.id, row.id));
          publishLiveEvent({ companyId, type: "content.job.done", payload: { jobId: row.id, assetId: result.assetId } });
        })
        .catch(async (err) => {
          await db.update(contentJobs)
            .set({ status: "failed", errorMessage: String(err) })
            .where(eq(contentJobs.id, row.id));
          publishLiveEvent({ companyId, type: "content.job.failed", payload: { jobId: row.id } });
        });
      return toContentJob(row);
    },
  };
}
```

### Pattern 2: RendererAdapter Interface

**What:** Each renderer implements a shared interface. `renderPipelineService` selects the adapter based on `ContentJobType`. Adding a new renderer requires only: (a) implement the interface, (b) register in the dispatch table.

**When to use:** Every new content type.

**Trade-offs:** Thin abstraction, no framework needed. Appropriate for the codebase size and single-user scale.

**Example:**
```typescript
// server/src/services/renderers/index.ts
export interface RendererAdapter {
  type: ContentJobType;
  render(
    params: Record<string, unknown>,
    storage: StorageService,
    companyId: string
  ): Promise<{ objectKey: string; contentType: string; byteSize: number }>;
}
```

### Pattern 3: Content Types as Skill Files

**What:** A Mermaid-generation "skill" is a Markdown file in `company_skills` that instructs agents: "When asked for a diagram, call `POST /api/companies/:id/content-jobs` with `{type: 'mermaid', params: {dsl: '...'}}` and wait for `content.job.done` event." No new schema required.

**When to use:** All content types — this is how they become installable skills.

**Trade-offs:** Agent must know the API contract, included in the skill markdown. Works with all adapters (Claude Code, Hermes, Ollama) since skills are plain text.

### Pattern 4: Theme Engine as Pure Function, Preview Client-Side

**What:** Theme generation is a pure computation: seed hex color in, palette object out. Preview injects CSS custom properties directly into the DOM — no server round-trip. Saving a theme stores the palette JSON via `StorageService`.

**When to use:** Theme generation and live preview.

**Trade-offs:** No server latency for preview feedback. The JSON is reusable for all export formats (CSS, Tailwind config, design tokens).

## Data Flow

### Async Content Job Request Flow

```
Agent / UI
    |
    v
POST /api/companies/:id/content-jobs
    { type: "mermaid", params: { dsl: "graph TD..." } }
    |
    v
contentJobService.enqueue()
    INSERT content_jobs WHERE status = "queued"
    publishLiveEvent("content.job.started")
    (non-blocking) -> renderPipelineService.render()
    |
    v (async)
renderPipelineService
    selects MermaidRendererAdapter
    |
    v
MermaidRendererAdapter.render()
    Mermaid DSL -> SVG Buffer (via @mermaid-js/mermaid-isomorphic)
    |
    v
StorageService.putFile()
    objectKey: "{companyId}/generated/diagrams/2026/04/04/{uuid}-diagram.svg"
    |
    v
assetService.create()
    INSERT assets (objectKey, contentType, byteSize, createdByAgentId)
    |
    v
contentJobService callback
    UPDATE content_jobs SET status = "done", asset_id = ...
    publishLiveEvent("content.job.done", { jobId, assetId })
    |
    v
UI (SSE subscriber)
    ContentJobViewer receives event -> fetches asset URL -> renders preview inline
```

### Theme Generation Flow (Synchronous)

```
User picks seed color
    |
    v
ThemePreview component
    (no server round-trip — CSS custom properties injected directly into DOM)
    |
    v (on "Save Theme")
POST /api/companies/:id/themes/generate
    { seedColor: "#4a90d9" }
    |
    v
themeEngineService.generate()
    Compute palette (tints, shades, semantic tokens, WCAG AA checks)
    Returns palette JSON
    |
    v
StorageService.putFile()
    objectKey: "{companyId}/themes/{uuid}-theme.json"
    contentType: "application/json"
    |
    v
assetService.create()  ->  201 { assetId, downloadUrl }
```

### Mermaid Client-Side Fast Path

```
Agent sends message with ```mermaid code block
    |
    v
ChatMarkdownMessage (existing component, minor extension)
    Detects ```mermaid fence
    |
    v
DiagramRenderer (new component, wraps existing mermaid dep)
    Calls mermaid.render() client-side (mermaid ^11.12 already in ui/package.json)
    Displays SVG inline
    "Save as asset" button -> POST /api/companies/:id/content-jobs (server path)
```

### State Transitions: content_jobs

```
queued -> running (renderPipelineService picks up job)
running -> done   (renderer returns, asset created)
running -> failed (renderer throws, error recorded)
```

## New vs Modified: Explicit Breakdown

### New (does not exist)

| Artifact | Type | Purpose |
|----------|------|---------|
| `content_jobs` table | DB schema + migration | Track async render job lifecycle |
| `contentJobService` | Server service | Enqueue, status, list jobs |
| `renderPipelineService` | Server service | Route jobs to renderer adapters |
| `themeEngineService` | Server service | Palette generation + WCAG validation |
| `mermaidRendererAdapter` | Server renderer | Server-side Mermaid to SVG |
| `remotionRendererAdapter` | Server renderer | Remotion CLI to MP4/WebM |
| `svgGeneratorAdapter` | Server renderer | Template SVG generation |
| `pdfRendererAdapter` | Server renderer | HTML to PDF via Puppeteer |
| `content-jobs.ts` route | API route | Create and list content jobs |
| `themes.ts` route | API route | Synchronous theme generation |
| `packages/remotion-compositions/` | Workspace package | Remotion composition definitions |
| `packages/shared/src/types/content.ts` | Shared type | `ContentJob`, `ContentJobStatus` |
| `ContentJobViewer` | UI component | Job progress + result display |
| `DiagramRenderer` | UI component | Client-side Mermaid wrapper |
| `ThemePreview` | UI component | Live palette preview |
| `GeneratedAssetCard` | UI component | Asset thumbnail + actions |
| `ContentGallery` | UI page | Workspace content library |

### Modified (exists, needs extension)

| Artifact | Change | Risk |
|----------|--------|------|
| `packages/shared/src/constants.ts` | Add 3 new `LIVE_EVENT_TYPES`: `content.job.started`, `content.job.done`, `content.job.failed` | LOW — additive only |
| `server/src/app.ts` | Mount `contentJobRoutes` and `themeRoutes` | LOW — two lines |
| `ChatMarkdownMessage` | Detect triple-backtick mermaid fence; render via `DiagramRenderer` | MEDIUM — existing component, test carefully |
| `assetService` | Add `list(companyId, opts)` method for gallery pagination | LOW — new method, no schema change |
| `packages/db/src/schema/index.ts` | Export `contentJobs` table | LOW |
| `packages/db/src/index.ts` | Export `contentJobs` from db package | LOW |

## Data Model

### New Table: `content_jobs`

No changes to existing tables. Standalone new table; upstream-safe because Paperclip has no content generation system.

```sql
CREATE TABLE content_jobs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  company_id UUID NOT NULL REFERENCES companies(id),
  type TEXT NOT NULL,                       -- 'mermaid' | 'remotion' | 'pdf' | 'svg' | 'theme' | 'image'
  status TEXT NOT NULL DEFAULT 'queued',    -- 'queued' | 'running' | 'done' | 'failed'
  params JSONB NOT NULL DEFAULT '{}',       -- renderer-specific input params
  asset_id UUID REFERENCES assets(id),     -- set on success
  error_message TEXT,                      -- set on failure
  created_by_agent_id UUID REFERENCES agents(id),
  created_by_user_id TEXT,
  started_at TIMESTAMPTZ,
  completed_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX content_jobs_company_status_idx ON content_jobs(company_id, status);
CREATE INDEX content_jobs_company_created_idx ON content_jobs(company_id, created_at DESC);
```

### Storage Namespaces (extends existing StorageService path conventions)

```
{companyId}/generated/diagrams/YYYY/MM/DD/{uuid}-{name}.svg
{companyId}/generated/videos/YYYY/MM/DD/{uuid}-{name}.mp4
{companyId}/generated/pdfs/YYYY/MM/DD/{uuid}-{name}.pdf
{companyId}/generated/images/YYYY/MM/DD/{uuid}-{name}.png
{companyId}/generated/icons/YYYY/MM/DD/{uuid}-{name}.svg
{companyId}/themes/{uuid}-theme.json
{companyId}/placeholders/{uuid}-placeholder.svg
```

The `StorageService.putFile()` method already handles path construction from `namespace` + `originalFilename` + timestamp. Pass `namespace: "generated/diagrams"` etc.

## Integration Points

### Existing System Touch Points

| Integration Point | How Content Gen Connects | Notes |
|-------------------|--------------------------|-------|
| `assetService` + `assets` table | Every rendered output creates an asset row | `createdByAgentId` already supported; agents get credit |
| `StorageService` | All rendered blobs stored via existing `putFile()` | Use `generated/` namespace prefix; no service changes |
| `publishLiveEvent` | Job lifecycle events push to SSE stream | Extend `LIVE_EVENT_TYPES` in `packages/shared/src/constants.ts` |
| `ChatMarkdownMessage` | Inline diagram rendering; "save as asset" button | Mermaid already a UI dep; add `DiagramRenderer` wrapper |
| `companySkills` + `skill-registry` | Content types as installable skill markdown files | No schema change; skills are text files agents read as context |
| `placeholderService` | Placeholder assets tracked in PLACEHOLDERS.md manifest | Optionally extend `PlaceholderEntry` with `contentJobId` |
| `hardwareService` | Detect if Remotion/Puppeteer can run | M4 Mac Mini: arm64 Chromium available, 24GB unified memory sufficient |
| `companyId` scoping | All content jobs scoped to `companyId` | Consistent with every other resource in the system |
| Agent task sessions | Agents invoke content APIs during task execution | Use `createdByAgentId`; same pattern as `documents`, `work-products` |

### External Dependencies (new server deps)

| Dependency | Purpose | Platform Notes |
|------------|---------|---------------|
| `@mermaid-js/mermaid-isomorphic` | Server-side Mermaid to SVG | No Chromium needed; fast; preferred over Puppeteer for Mermaid |
| `puppeteer` | HTML to PDF | ~300MB install; bundled arm64 Chromium works on M4; only add if PDF is a phase priority |
| `remotion` (CLI) | Video/presentation render | Add as devDep in `remotion-compositions` package; CLI called via subprocess |

Mermaid client-side and Sharp are already present. No changes needed for those paths.

## Scaling Considerations

This is a Mac Mini M4 single-user deployment. Analysis focuses on resource contention, not user count.

| Concern | Approach |
|---------|----------|
| Concurrent render jobs | Node.js event loop is safe for I/O. CPU-bound renders (Remotion, Puppeteer) spawn subprocesses, keeping the event loop responsive. |
| Remotion render duration | Renders can take minutes. Never synchronous HTTP. Async job pattern + SSE progress is mandatory. |
| Chromium memory (PDF/Puppeteer) | Puppeteer can use 500MB+ per render. Serialize PDF renders via an in-memory queue (one at a time). |
| Storage growth | Generated content accumulates. Add `retention_days` field to `content_jobs`; implement a cleanup cron using the existing `cron.ts` service. |
| Remotion bundle step | Bundle compositions once at server startup (or on demand). Never bundle on each render request — it takes 30-60s. |

## Build Order (Phase Dependencies)

Dependencies flow from infrastructure upward to content types upward to UI.

```
Phase A: Core Infrastructure (unblocks everything)
  - Add content_jobs schema + migration (db package)
  - Extend LIVE_EVENT_TYPES with content.job.* (shared package)
  - Implement contentJobService (server)
  - Implement renderPipelineService stub (server)
  - Add API routes + app.ts mounts (server)

Phase B: Fast Content Types (no heavy binary deps; validates pipeline end-to-end)
  - svgGeneratorAdapter (pure TypeScript; icons, placeholders)
  - mermaidRendererAdapter (@mermaid-js/mermaid-isomorphic; no Chromium)
  - themeEngineService (pure computation)
  - UI: DiagramRenderer, ThemePreview, ContentJobViewer

Phase C: Client-Side Mermaid + Content Gallery
  - ChatMarkdownMessage extension (detect mermaid fence)
  - DiagramRenderer client-side component
  - ContentGallery page + assetService.list()
  - GeneratedAssetCard

Phase D: Document Generation (introduces Puppeteer)
  - Add puppeteer to server deps
  - pdfRendererAdapter
  - PDF download flow in UI

Phase E: Video / Presentations (introduces Remotion)
  - packages/remotion-compositions/ workspace package
  - remotionRendererAdapter (CLI subprocess)
  - Video playback in UI

Phase F: Image Generation
  - imageProcessorAdapter using Sharp (banners, OG images, social cards)
  - imageGenerationAdapter interface (Stable Diffusion / cloud APIs — future)
  - Social media content generation

Phase G: Content as Skills (no code, pure skill markdown)
  - Skill markdown files for each content type in company_skills
  - Agent-callable via existing skill system
```

## Anti-Patterns

### Anti-Pattern 1: Rendering inside chatService

**What people do:** Add Mermaid rendering to `chatService` or `documentService` because content requests arrive from chat.

**Why it's wrong:** Couples unrelated concerns. Future content types (video, PDF) would bloat chatService and block upstream rebases.

**Do this instead:** `chatService` calls `contentJobService.enqueue()`. Rendering is entirely separate. Chat is a trigger, not an owner.

### Anti-Pattern 2: Synchronous HTTP response for long renders

**What people do:** `POST /render/remotion` holds the connection open for 2+ minutes while rendering.

**Why it's wrong:** HTTP timeout (30s default on most proxies). No progress feedback. Retry hell.

**Do this instead:** Return a `contentJobId` immediately with `202 Accepted`. Client subscribes to SSE `content.job.done` event.

### Anti-Pattern 3: One DB table per content type

**What people do:** Add separate `diagrams`, `presentations`, `themes` tables.

**Why it's wrong:** The existing `assets` table already handles typed binary blobs. The `content_jobs` table handles any render job regardless of output type. Fragmented schema multiplies migration surface.

**Do this instead:** Use `content_jobs.type` to discriminate job types. Use `assets.content_type` to discriminate output format. One jobs table, one assets table.

### Anti-Pattern 4: Bypassing StorageService for renderer output

**What people do:** Remotion adapter writes to `/tmp` and returns a filesystem path.

**Why it's wrong:** Bypasses the provider abstraction (local disk vs S3), deduplication (sha256), the `assets` table, and download URL generation.

**Do this instead:** Renderer writes output to a `Buffer`, passes to `StorageService.putFile()`, returns `objectKey`. Asset serving goes through the existing `/api` asset download route.

### Anti-Pattern 5: Modifying upstream DB tables

**What people do:** Add a `generated_content_type` column to the existing `assets` table.

**Why it's wrong:** Modifies upstream schema — migration conflict on next `git rebase upstream/master`. Violates the display-only fork constraint.

**Do this instead:** Use the `content_jobs.asset_id` FK as the signal that an asset is generated. Query `content_jobs JOIN assets` to distinguish generated from uploaded. Keep `assets` table untouched.

### Anti-Pattern 6: Remotion bundle on every render request

**What people do:** Call `bundle()` inside the render adapter on each job.

**Why it's wrong:** Bundling takes 30-60s. Renders that should take 5s take 90s.

**Do this instead:** Bundle once at server startup (or lazily on first render, cached). The `remotionRendererAdapter` calls `renderMedia()` against the pre-built bundle path.

## Sources

- Direct inspection of `/opt/nexus` codebase (2026-04-04):
  - `server/src/services/` — factory function service patterns
  - `server/src/storage/` — `StorageService` / `StorageProvider` interfaces
  - `server/src/storage/service.ts` — `buildObjectKey()` namespace + path conventions
  - `server/src/services/live-events.ts` — SSE event bus (`publishLiveEvent`, `subscribeCompanyLiveEvents`)
  - `server/src/services/voice-pipeline.ts` — async subprocess service pattern
  - `server/src/services/placeholder-service.ts` — existing `PlaceholderEntry` manifest service
  - `server/src/services/assets.ts` — `assetService` factory (minimal; extend for listing)
  - `server/src/services/work-products.ts` — job/output separation pattern
  - `packages/db/src/schema/assets.ts` — existing `assets` table
  - `packages/db/src/schema/documents.ts` — document + revision pattern
  - `packages/shared/src/constants.ts` — `LIVE_EVENT_TYPES` (currently 9 types)
  - `server/src/app.ts` — route mounting conventions
  - `server/src/routes/voice.ts` — SSE streaming response pattern
  - `ui/package.json` — confirms `mermaid ^11.12.0` already installed
  - `server/package.json` — confirms `sharp`, `ffmpeg-static` already installed
- Mermaid v11 isomorphic: https://mermaid.js.org/config/usage.html
- Remotion CLI rendering: https://www.remotion.dev/docs/cli/render
- `@mermaid-js/mermaid-isomorphic` for server-side rendering without a browser

---
*Architecture research for: Nexus v1.7 Content Generation*
*Researched: 2026-04-04*
