---
phase: 40-job-infrastructure
plan: "01"
subsystem: content-jobs
tags: [db-schema, services, async-jobs, live-events]
dependency_graph:
  requires: []
  provides:
    - content_jobs DB table with queued/running/done/failed lifecycle
    - contentJobStore CRUD service
    - contentJobRunner async dispatcher with live events
    - MAX_GENERATED_ASSET_BYTES constant (500MB)
    - assets.sourceTaskId column
    - LIVE_EVENT_TYPES extended with content_job.* events
  affects:
    - packages/db/src/schema/
    - packages/shared/src/constants.ts
    - server/src/services/
    - server/src/attachment-types.ts
tech_stack:
  added: []
  patterns:
    - Drizzle ORM pgTable schema with $type<> for typed status fields
    - Fire-and-forget async job dispatch with void pattern
    - DB write before live event publish (ordered consistency)
    - renderContent stub for future renderer plug-in by jobType
key_files:
  created:
    - packages/db/src/schema/content_jobs.ts
    - server/src/services/content-job-store.ts
    - server/src/services/content-job-runner.ts
    - packages/db/src/migrations/0046_tense_randall.sql
  modified:
    - packages/db/src/schema/assets.ts
    - packages/db/src/schema/index.ts
    - packages/shared/src/constants.ts
    - server/src/attachment-types.ts
    - server/src/services/index.ts
decisions:
  - content_jobs.resultAssetId has no FK to assets — populated post-creation, circular FK avoided
  - assets.sourceTaskId is nullable text with no FK — task IDs are string identifiers, not UUIDs
  - renderContent is a stub returning placeholder.txt — phases 41-45 add real renderers keyed by jobType
  - Migration applied via drizzle-kit generate from worktree dist/ (main repo has pre-existing duplicate migration files)
metrics:
  duration: "~10 minutes"
  completed: "2026-04-04"
  tasks_completed: 2
  tasks_total: 2
  files_created: 4
  files_modified: 5
---

# Phase 40 Plan 01: Schema, constants, and services for content jobs Summary

**One-liner:** content_jobs table + async job runner with store/runner services using fire-and-forget dispatch and ordered DB-before-event lifecycle.

## What Was Built

The foundational data model and async execution engine for v1.7 content generation. Phase 40 Plan 01 establishes:

1. **DB schema** (`content_jobs` table) with full lifecycle columns and two composite indexes for efficient querying by company+status and company+createdAt.
2. **assets.sourceTaskId** — nullable text column allowing generated assets to be traced back to their originating conversation task.
3. **LIVE_EVENT_TYPES** — extended with `content_job.queued`, `content_job.running`, `content_job.done`, `content_job.failed` for real-time job progress.
4. **MAX_GENERATED_ASSET_BYTES** — 500MB constant (vs 10MB upload limit) for generated/namespace storage, configurable via `PAPERCLIP_GENERATED_ASSET_MAX_BYTES`.
5. **contentJobStore** — CRUD service with create/getById/listByCompany/transition operations against the content_jobs table.
6. **contentJobRunner** — async fire-and-forget dispatcher that transitions jobs through running→done/failed, stores generated assets with sourceTaskId, and publishes live events after each DB write.

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | Schema, constants, and migrations | 8bf4bd91 | content_jobs.ts, assets.ts, index.ts, constants.ts, attachment-types.ts, migration 0046 |
| 2 | contentJobStore and contentJobRunner | b359fec9 | content-job-store.ts, content-job-runner.ts, services/index.ts |

## Verification

- `pnpm tsc --noEmit --project packages/db/tsconfig.json` — PASS
- `pnpm tsc --noEmit --project packages/shared/tsconfig.json` — PASS
- `pnpm tsc --noEmit --project server/tsconfig.json` — PASS
- Migration file `0046_tense_randall.sql` generated with correct DDL

## Deviations from Plan

### Migration apply limitation (pre-existing blocker, out of scope)

**Found during:** Task 1

**Issue:** The main repo (`/opt/nexus`) has pre-existing duplicate migration files (`0047_nebulous_klaw.sql` and `0047_overjoyed_groot.sql`, `0048_add_chat_messages_updated_at.sql` and `0048_flashy_marrow.sql`) from parallel agent work. The `check:migrations` script prevents both `db:generate` and `db:migrate` in the main repo.

**Impact:** Migration `0046_tense_randall.sql` was generated successfully from the worktree's clean migration state but could not be applied via `pnpm db:migrate`.

**Resolution:** Migration will be applied when the worktree is merged and the duplicate migration numbering conflict is resolved. The SQL is correct and ready for execution. This is a pre-existing issue deferred to `.planning/deferred-items.md`.

**Migration file content verified:** Creates `content_jobs` table, adds `source_task_id` to `assets`, creates both compound indexes, adds FK to companies.

### Known Stubs

**renderContent stub** in `server/src/services/content-job-runner.ts`:
- Returns `{ filename: "placeholder.txt", contentType: "text/plain", buffer: Buffer.from("placeholder output") }` for all jobTypes
- This is intentional — the plan specifies this as a stub for phases 41-45 to fill in real renderers keyed by jobType
- Does NOT prevent the plan's goal (job infrastructure) from being achieved

## Self-Check: PASSED

Files verified to exist:
- `/opt/nexus/.claude/worktrees/agent-ac2e6085/packages/db/src/schema/content_jobs.ts` — FOUND
- `/opt/nexus/.claude/worktrees/agent-ac2e6085/server/src/services/content-job-store.ts` — FOUND
- `/opt/nexus/.claude/worktrees/agent-ac2e6085/server/src/services/content-job-runner.ts` — FOUND
- `/opt/nexus/.claude/worktrees/agent-ac2e6085/packages/db/src/migrations/0046_tense_randall.sql` — FOUND

Commits verified:
- `8bf4bd91` — FOUND (feat(40-01): schema, constants, and migrations for content jobs)
- `b359fec9` — FOUND (feat(40-01): contentJobStore and contentJobRunner services)
