logicspike/docs

Content Engine

Phase 2: Architecture Decision Record — Content Engine

Last Updated: 2026-03-15 Status: Proposed


ADR-1: Service Deployment Model

Context

The Content Engine needs both synchronous API handling (CRUD, calendar queries) and asynchronous background processing (scheduled publishing, retries, token refresh).

Decision

Deploy as a single Cloudflare Worker (apps/content-engine) with:

  • Hono Router for REST API (consistent with blog-service, media-service)
  • Cloudflare Queues for asynchronous publish jobs
  • Cloudflare Cron Triggers for periodic scheduling scans
  • Durable Objects for reliable, exactly-once publish guarantees (optional — evaluate during Phase 2)

Rationale

  • Keeps alignment with the existing LogicSpike Workers-based architecture
  • Cloudflare Queues provide native dead-letter queue support for failed publishes
  • Cron Triggers are built into Workers — no external scheduler needed
  • Durable Objects prevent duplicate publishes if the Worker cold-starts mid-job

Alternatives Considered

Option Rejected Because
Separate Worker for publisher Adds operational overhead; single worker with queues achieves the same isolation
External scheduler (e.g., AWS EventBridge) Vendor mixing; Cloudflare Cron Triggers are free and integrated
Long-running Node.js process Doesn't fit Cloudflare Workers model; no persistent processes

ADR-2: Database Strategy

Context

Need to store content slots, social connections, campaigns, and publishing logs.

Decision

Use Neon PostgreSQL with Drizzle ORM — consistent with blog-service (which uses @neondatabase/serverless + drizzle-orm/neon-http) and manager (which uses @repo/core-database).

Dedicated database: content-engine-db with tenant isolation via tenantId column on every table (shared-schema multi-tenancy — same pattern as blog-service).

Rationale

  • Follows the existing LogicSpike database strategy — both blog-service and manager use Neon PostgreSQL
  • Enables reuse of the shared @repo/core-database package (or a dedicated createDb() factory like blog-service)
  • PostgreSQL supports jsonb, full-text search, and advanced indexing needed for calendar queries
  • Neon's serverless driver (@neondatabase/serverless) is optimized for Cloudflare Workers
  • Drizzle ORM provides type-safe schema definitions and query building (same pattern across all services)

ADR-3: OAuth Token Management

Context

Social platform connectors require storing OAuth2 access/refresh tokens. These are high-sensitivity credentials that grant posting access to user accounts.

Decision

  • Store tokens in the social_connections table
  • Encrypt accessToken and refreshToken at rest using AES-256-GCM (matching the Communication Service pattern for provider credentials)
  • Encryption key stored as a Worker environment secret (SOCIAL_TOKEN_KEY)
  • Proactive token refresh: a daily cron job checks all tokens and refreshes those expiring within 48 hours

Rationale

  • A stale token at publish time = failed post + bad UX
  • Proactive refresh avoids the "publish failed because token expired 5 minutes ago" scenario
  • AES-256-GCM provides authenticated encryption — tamper detection is built-in

ADR-4: Content Slot State Machine

Context

A content slot moves through multiple states. We need a well-defined state machine to prevent invalid transitions and ensure data consistency.

Decision

                    ┌─────────────────────────────────────────┐
                    │                                         │
                    ▼                                         │
            ┌──────────┐     ┌───────────────┐     ┌─────────┴─┐
   Create──▶│  draft   │────▶│pending_review │────▶│ approved  │
            └──────────┘     └───────┬───────┘     └──────┬────┘
                 ▲                   │                    │
                 │              (reject)            (schedule)
                 │                   │                    │
                 └───────────────────┘                    ▼
                                                  ┌──────────┐
                                                  │scheduled │
                                                  └────┬─────┘

                                                  (due time)


                                                  ┌──────────┐
                                            ┌────▶│publishing│
                                            │     └──┬───┬───┘
                                            │        │   │
                                       (retry)       │   │
                                            │        │   │
                                            │   (fail)   (success)
                                            │        │   │
                                            │        ▼   ▼
                                            │  ┌──────┐ ┌──────────┐
                                            └──│failed│ │published │
                                               └──────┘ └──────────┘

Transitions:

From To Trigger Permission
draft pending_review User submits for review content:slots.write
draft scheduled Direct schedule (no approval) content:slots.write
pending_review approved Reviewer approves content:slots.approve
pending_review draft Reviewer rejects content:slots.approve
approved scheduled Auto-transition on approval System
scheduled publishing Cron trigger at due time System
publishing published All platforms succeed System
publishing failed Platform API error (after retries) System
failed draft User edits and retries content:slots.write

Rationale

  • Explicit states prevent ambiguity ("Is this post going out or not?")
  • Permission-gated transitions integrate with LogicSpike's PBAC system
  • Mirrors real agency workflows where content is created by juniors and approved by managers

ADR-5: Platform Adapter Pattern

Context

Each social platform has a unique API, media requirements, and character limits. We need a clean abstraction.

Decision

Use a Strategy Pattern with a common PlatformAdapter interface:

interface PlatformAdapter {
  platform: SocialPlatform;
  validateContent(slot: ContentSlot): ValidationResult;
  uploadMedia(media: MediaAsset, token: DecryptedToken): Promise<string>; // returns platform media ID
  publish(slot: ContentSlot, token: DecryptedToken): Promise<PublishResult>;
  refreshToken(connection: SocialConnection): Promise<TokenPair>;
}

Each platform gets its own adapter file: twitter.adapter.ts, linkedin.adapter.ts, meta.adapter.ts.

Rationale

  • Adding YouTube support later = creating one new file implementing PlatformAdapter
  • Content validation (e.g., "Twitter max 280 chars") is co-located with the platform logic
  • Consistent with Communication Service's ProviderRegistry pattern

ADR-6: Cross-Service Integration via Event Bus

Context

The Content Engine needs to react to events from other services (e.g., blog.published) and emit its own events.

Decision

  • Consumer (optional): Listen for blog.published on the Event Bus → auto-create content slots (only when Blog Engine is active)
  • Producer: Emit content.scheduled, content.published, content.failed events
  • Use Cloudflare Queues as the event bus mechanism (consistent with platform direction)

Rationale

  • Decoupled architecture — the Blog Engine doesn't know the Content Engine exists
  • Future services (Newsletter, Automation Engine) can subscribe to content events without any changes to this service
  • Aligns with LogicSpike Platform Section 4: "The Central Event Bus"

ADR-7: MCP Server for Agent Control

Context

The Content Engine should be operable by AI agents (Claude, Gemini, custom automation agents) — not just via a human dashboard. We need a protocol that allows agents to semantically interact with the service.

Decision

Expose the Content Engine as a Model Context Protocol (MCP) server with:

  • 15+ MCP tools mapping 1:1 to REST API actions (schedule, reschedule, cancel, etc.)
  • MCP Resources for read-only context (calendar state, connection health, stats)
  • Bulk operation tools (bulk_schedule, bulk_action) optimized for agent workflows
  • Dual transport: SSE (for remote agents) + stdio (for local agent frameworks like Claude Desktop)
  • Deployed as: packages/content-engine-mcp/ — a standalone Node.js process wrapping the REST API

Architecture

AI Agent → MCP Protocol (SSE/stdio) → Content Engine MCP Server → REST API → Content Engine Worker

The MCP server is a thin wrapper — it translates MCP tool calls into REST API calls with proper authentication. This means:

  • The REST API remains the single source of truth
  • MCP server can be deployed independently
  • Adding new MCP tools = adding new REST routes + MCP tool definitions

Auth Strategy

  • Agents authenticate via API Keys (ls_ prefix secret keys)
  • Each API key has PBAC-scoped permissions — an agent can only do what its key allows
  • Every agent action includes an agentId in the audit trail

Safety Guards

  • autoApprove defaults to false — agents must explicitly opt into bypassing review
  • Rate limits: max 50 slots per bulk_schedule, max 200 slots/hour per agent
  • Optional dryRun: true flag for previewing actions without executing

Rationale

  • MCP is an open standard — works with Claude, Gemini, and any custom agent framework
  • Semantic tools are richer than raw REST — agents get descriptions, validation, and structured responses
  • Aligns with LogicSpike Phase 3 vision — the Automation Engine will orchestrate multiple MCP servers
  • Thin wrapper pattern keeps the architecture simple — no duplicate business logic

Alternatives Considered

Option Rejected Because
Direct REST API for agents Agents need semantic tool descriptions, not raw endpoint docs
GraphQL for agents MCP is purpose-built for agent interaction; GraphQL adds query complexity
Custom WebSocket protocol Non-standard; MCP is an industry-standard protocol
Embedding MCP in the Worker Workers have limited execution time; MCP needs a persistent process for SSE

Full Specification

See mcp_server_spec.md for complete tool definitions, resources, agent personas, and rate limits.

Content Engine