Files
ClaudeMarketing/docs/reference/FRONTEND_ARCHITECTURE.md
T

28 KiB

Marketing Command Center — Frontend Architecture

What you want: A self-hosted web dashboard where you log in, see everything your agents produced, cherry-pick ads to push to Postiz, and drop into a Claude session to give feedback — all from the browser. Everything runs in Docker. No external services except Postiz and Claude API.


System Overview

┌─────────────────────────────────────────────────────────┐
│                    YOUR BROWSER                          │
│                                                          │
│  ┌──────────────┐  ┌──────────┐  ┌───────────────────┐  │
│  │  Campaign     │  │  Asset   │  │  Claude Chat      │  │
│  │  Dashboard    │  │  Gallery │  │  (feedback/edits)  │  │
│  └──────┬───────┘  └────┬─────┘  └────────┬──────────┘  │
└─────────┼───────────────┼─────────────────┼──────────────┘
          │               │                 │
          ▼               ▼                 ▼
┌─────────────────────────────────────────────────────────┐
│              NEXT.JS APP (Docker Container)               │
│                                                          │
│  API Routes:                                             │
│  /api/campaigns    /api/assets    /api/claude            │
│  /api/publish      /api/agents    /api/auth              │
│                                                          │
│  ┌──────────┐  ┌──────────┐  ┌───────────────────────┐  │
│  │ SQLite   │  │ Local    │  │ Claude Agent SDK (TS) │  │
│  │ Database │  │ File     │  │ Spawns Claude Code    │  │
│  │ (Prisma) │  │ Storage  │  │ as subprocess         │  │
│  └──────────┘  └──────────┘  └───────────┬───────────┘  │
└──────────────────────────────────────────┼───────────────┘
                                           │
                    ┌──────────────────────┼──────────┐
                    │                      │          │
                    ▼                      ▼          ▼
             ┌──────────┐          ┌──────────┐  ┌────────┐
             │ Postiz   │          │ Tavily   │  │Nextdoor│
             │ (Docker) │          │ API      │  │ API    │
             │ IG+TikTok│          │ Research │  │ Direct │
             │ +Media   │          └──────────┘  └────────┘
             │  Hosting │
             └──────────┘

Zero external services for core functionality. Postiz and your Next.js app both run in Docker on your machine. The only outbound calls are to Tavily (research), Nextdoor API (publishing), and Anthropic (Claude).


Tech Stack

Layer Technology Why
Framework Next.js 15+ (App Router) Full-stack React, API routes, SSR
UI shadcn/ui + Tailwind CSS Clean components, fast to build
Auth NextAuth.js (Auth.js v5) Self-contained — credentials provider, no external auth service
Database SQLite via Prisma Zero infrastructure — single file, runs in Docker volume
File Storage Local filesystem (Docker volume) Pipeline outputs stored locally, Postiz handles public hosting
Claude Claude Agent SDK (TypeScript) Spawn Claude sessions, stream responses
Publishing Postiz (@postiz/node SDK) Media upload + hosting + scheduling + IG/TikTok publishing
Realtime Server-Sent Events (SSE) Pipeline progress streaming — no Redis/WebSocket service needed
Deployment Docker Compose Everything self-hosted, single docker compose up

Database Schema (SQLite via Prisma)

// prisma/schema.prisma

datasource db {
  provider = "sqlite"
  url      = "file:./data/marketing.db"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  password  String   // bcrypt hashed
  name      String?
  createdAt DateTime @default(now())
}

model Campaign {
  id          String      @id @default(cuid())
  name        String
  status      String      @default("draft") // draft, running, review, approved, published
  prompt      String?     // the trigger prompt
  platforms   String      // JSON array: ["instagram","tiktok","nextdoor"]
  config      String?     // JSON: full campaign config from form
  outputPath  String?     // local path to outputs/
  createdAt   DateTime    @default(now())
  updatedAt   DateTime    @updatedAt
  agentRuns   AgentRun[]
  assets      Asset[]
  claudeSessions ClaudeSession[]
}

model AgentRun {
  id            String    @id @default(cuid())
  campaignId    String
  campaign      Campaign  @relation(fields: [campaignId], references: [id])
  agentName     String    // trend-scout, research, script-writer, etc.
  status        String    @default("pending") // pending, running, completed, failed
  startedAt     DateTime?
  completedAt   DateTime?
  durationMs    Int?
  outputSummary String?
  outputPath    String?
  tokenUsage    Int?
  error         String?
  assets        Asset[]
  createdAt     DateTime  @default(now())
}

model Asset {
  id            String    @id @default(cuid())
  campaignId    String
  campaign      Campaign  @relation(fields: [campaignId], references: [id])
  agentRunId    String?
  agentRun      AgentRun? @relation(fields: [agentRunId], references: [id])
  type          String    // image, video, copy, research, script
  platform      String?   // instagram, tiktok, nextdoor, all
  format        String?   // png, mp4, json, txt, html, md
  filePath      String    // local path in outputs/
  fileName      String
  dimensions    String?   // 1080x1080, 1080x1920, etc.
  metadata      String?   // JSON: caption, hashtags, hook text, scene plan
  status        String    @default("draft") // draft, approved, rejected, published
  publishedTo   String?   // JSON array of platforms published to
  postizPostId  String?   // ID from Postiz after scheduling
  postizMediaId String?   // media ID from Postiz upload
  createdAt     DateTime  @default(now())
}

model ClaudeSession {
  id          String   @id @default(cuid())
  campaignId  String
  campaign    Campaign @relation(fields: [campaignId], references: [id])
  sessionId   String?  // Claude Code session ID for --resume
  messages    String?  // JSON array of conversation history
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

model TrendReport {
  id          String   @id @default(cuid())
  name        String
  filePath    String   // path to the HTML/JSON report
  summary     String?  // brief description
  createdAt   DateTime @default(now())
}

Why SQLite over Postgres:

  • Zero config — no separate database container
  • Single file — easy to backup (just copy marketing.db)
  • Fast enough for a single-user dashboard
  • Runs anywhere Docker runs
  • Prisma makes it trivial to switch to Postgres later if needed

Authentication (NextAuth.js — Self-Contained)

// lib/auth.ts
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { PrismaAdapter } from "@auth/prisma-adapter";
import bcrypt from "bcryptjs";
import { prisma } from "./prisma";

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        const user = await prisma.user.findUnique({
          where: { email: credentials.email as string },
        });
        if (!user) return null;
        const valid = await bcrypt.compare(
          credentials.password as string,
          user.password
        );
        return valid ? user : null;
      },
    }),
  ],
  session: { strategy: "jwt" },
  secret: process.env.NEXTAUTH_SECRET,
});

First-run setup: A seed script creates your admin user. No external auth provider needed.

// prisma/seed.ts
import bcrypt from "bcryptjs";
import { prisma } from "../lib/prisma";

async function main() {
  await prisma.user.upsert({
    where: { email: process.env.ADMIN_EMAIL! },
    update: {},
    create: {
      email: process.env.ADMIN_EMAIL!,
      password: await bcrypt.hash(process.env.ADMIN_PASSWORD!, 12),
      name: "Admin",
    },
  });
}
main();

Real-Time Pipeline Progress (Server-Sent Events)

No Redis, no WebSocket server, no Supabase Realtime. Just native SSE from a Next.js API route:

// app/api/campaigns/[id]/stream/route.ts
export async function GET(req: Request, { params }: { params: { id: string } }) {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      // Subscribe to pipeline events for this campaign
      const unsubscribe = pipelineEvents.subscribe(params.id, (event) => {
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify(event)}\n\n`)
        );
      });

      // Keep connection alive
      const keepAlive = setInterval(() => {
        controller.enqueue(encoder.encode(`: keepalive\n\n`));
      }, 15000);

      req.signal.addEventListener("abort", () => {
        unsubscribe();
        clearInterval(keepAlive);
        controller.close();
      });
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

Frontend hook:

// hooks/usePipelineProgress.ts
export function usePipelineProgress(campaignId: string) {
  const [agents, setAgents] = useState<AgentRun[]>([]);

  useEffect(() => {
    const source = new EventSource(`/api/campaigns/${campaignId}/stream`);
    source.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setAgents((prev) => updateAgentStatus(prev, data));
    };
    return () => source.close();
  }, [campaignId]);

  return agents;
}

Claude Integration

// lib/claude.ts
import { spawn } from "child_process";
import { EventEmitter } from "events";

export const pipelineEvents = new EventEmitter();

export async function launchPipeline(campaignId: string, prompt: string, cwd: string) {
  // Update campaign status
  await prisma.campaign.update({
    where: { id: campaignId },
    data: { status: "running" },
  });

  const claude = spawn("claude", [
    "-p", prompt,
    "--output-format", "stream-json",
    "--verbose",
    "--allowedTools", "Read,Edit,Write,Bash,Grep,Glob",
  ], { cwd, env: { ...process.env } });

  claude.stdout.on("data", async (chunk) => {
    const lines = chunk.toString().split("\n").filter(Boolean);
    for (const line of lines) {
      try {
        const event = JSON.parse(line);

        // Detect agent starts/completions from tool use events
        const agentEvent = parseAgentEvent(event);
        if (agentEvent) {
          // Save to database
          await upsertAgentRun(campaignId, agentEvent);
          // Broadcast to SSE clients
          pipelineEvents.emit(campaignId, agentEvent);
        }

        // Detect new files created
        const fileEvent = parseFileCreatedEvent(event);
        if (fileEvent) {
          await createAsset(campaignId, fileEvent);
          pipelineEvents.emit(campaignId, { type: "asset_created", ...fileEvent });
        }
      } catch {}
    }
  });

  claude.on("close", async (code) => {
    const status = code === 0 ? "review" : "failed";
    await prisma.campaign.update({
      where: { id: campaignId },
      data: { status },
    });
    pipelineEvents.emit(campaignId, { type: "pipeline_complete", status });
  });
}

// Claude chat session for feedback
export async function sendChatMessage(
  sessionId: string | null,
  message: string,
  cwd: string
): AsyncGenerator<string> {
  const args = ["-p", message, "--output-format", "stream-json", "--verbose"];
  if (sessionId) args.push("--resume", sessionId);
  args.push("--allowedTools", "Read,Edit,Write,Bash,Grep,Glob");

  const claude = spawn("claude", args, { cwd, env: { ...process.env } });

  // Stream text deltas back
  for await (const chunk of claude.stdout) {
    yield chunk.toString();
  }
}

Postiz Integration (Media Upload + Publishing)

// lib/postiz.ts
import { Postiz } from "@postiz/node";
import fs from "fs";
import path from "path";
import FormData from "form-data";

const postiz = new Postiz({
  apiKey: process.env.POSTIZ_API_KEY!,
  baseUrl: process.env.POSTIZ_URL!,
});

// Upload a local file to Postiz and get back a hosted URL + media ID
export async function uploadToPostiz(filePath: string) {
  const form = new FormData();
  form.append("file", fs.createReadStream(filePath));

  const res = await fetch(`${process.env.POSTIZ_URL}/public/v1/upload`, {
    method: "POST",
    headers: { Authorization: process.env.POSTIZ_API_KEY! },
    body: form,
  });

  const { id, path: publicUrl } = await res.json();
  return { mediaId: id, publicUrl };
}

// Push an approved asset to Postiz for scheduling
export async function pushToPostiz(asset: Asset, scheduledAt: string) {
  // Upload media first
  const { mediaId, publicUrl } = await uploadToPostiz(asset.filePath);

  // Get the right Postiz integration (channel) ID
  const integrations = await postiz.integrations.list();
  const integration = integrations.find(
    (i) => i.providerIdentifier === asset.platform
  );

  if (!integration) throw new Error(`No Postiz channel for ${asset.platform}`);

  // Parse caption from asset metadata
  const metadata = JSON.parse(asset.metadata || "{}");

  // Create the scheduled post
  const post = await postiz.posts.create({
    type: "schedule",
    date: scheduledAt,
    integration: integration.id,
    content: metadata.caption || "",
    image: [{ id: mediaId, path: publicUrl }],
  });

  return post;
}

File Serving (Local Assets → Browser Preview)

Your pipeline outputs (images, videos) live on the local filesystem. The dashboard needs to serve them for preview:

// app/api/files/[...path]/route.ts
import { readFile } from "fs/promises";
import path from "path";
import { auth } from "@/lib/auth";

const PIPELINE_ROOT = process.env.PIPELINE_ROOT || "/app/pipeline";

export async function GET(req: Request, { params }: { params: { path: string[] } }) {
  // Auth check — only logged-in users can view files
  const session = await auth();
  if (!session) return new Response("Unauthorized", { status: 401 });

  const filePath = path.join(PIPELINE_ROOT, ...params.path);

  // Security: prevent path traversal
  if (!filePath.startsWith(PIPELINE_ROOT)) {
    return new Response("Forbidden", { status: 403 });
  }

  try {
    const buffer = await readFile(filePath);
    const ext = path.extname(filePath).toLowerCase();
    const contentType =
      { ".png": "image/png", ".jpg": "image/jpeg", ".mp4": "video/mp4",
        ".html": "text/html", ".json": "application/json", ".md": "text/markdown",
        ".txt": "text/plain", ".css": "text/css" }[ext] || "application/octet-stream";

    return new Response(buffer, { headers: { "Content-Type": contentType } });
  } catch {
    return new Response("Not found", { status: 404 });
  }
}

Asset gallery uses: <img src="/api/files/outputs/campaign_123/ads/instagram_feed.png" />

Video preview uses: <video src="/api/files/outputs/campaign_123/video/tiktok_ad.mp4" />


Docker Setup

Dockerfile (Next.js App)

# Dockerfile
FROM node:20-alpine AS base

# Install Claude Code CLI
RUN npm install -g @anthropic-ai/claude-code

# Install Playwright browsers (for ad creative generation)
RUN npx playwright install chromium --with-deps

FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma

# Copy the marketing pipeline
COPY pipeline/ ./pipeline/

EXPOSE 3000
CMD ["node", "server.js"]

Docker Compose (Full Stack)

# docker-compose.yml
version: "3.8"

services:
  # Your marketing dashboard + pipeline
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NEXTAUTH_URL=http://localhost:3000
      - NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
      - ADMIN_EMAIL=${ADMIN_EMAIL}
      - ADMIN_PASSWORD=${ADMIN_PASSWORD}
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
      - TAVILY_API_KEY=${TAVILY_API_KEY}
      - POSTIZ_URL=http://postiz:5000
      - POSTIZ_API_KEY=${POSTIZ_API_KEY}
      - NEXTDOOR_API_TOKEN=${NEXTDOOR_API_TOKEN}
      - NEXTDOOR_ADVERTISER_ID=${NEXTDOOR_ADVERTISER_ID}
      - PIPELINE_ROOT=/app/pipeline
    volumes:
      - app-data:/app/prisma/data       # SQLite database
      - pipeline-outputs:/app/pipeline/outputs  # Generated content
      - pipeline-knowledge:/app/pipeline/knowledge  # Brand files (editable)
    depends_on:
      - postiz

  # Self-hosted Postiz (social media scheduling + media hosting)
  postiz:
    image: ghcr.io/gitroomhq/postiz-app:latest
    ports:
      - "5000:5000"
    environment:
      - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postiz-db:5432/postiz
      - REDIS_URL=redis://redis:6379
      - NEXT_PUBLIC_BACKEND_URL=http://postiz:5000
      - STORAGE_PROVIDER=local
      - UPLOAD_DIRECTORY=/uploads
    volumes:
      - postiz-uploads:/uploads
      - postiz-config:/config
    depends_on:
      - postiz-db
      - redis

  # Postiz database (Postgres — required by Postiz, not by your app)
  postiz-db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=postiz
    volumes:
      - postiz-pgdata:/var/lib/postgresql/data

  # Postiz cache (Redis — required by Postiz, not by your app)
  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data

volumes:
  app-data:          # Your SQLite database
  pipeline-outputs:  # All generated content (images, videos, reports)
  pipeline-knowledge: # Brand identity, platform guidelines, campaign docs
  postiz-uploads:    # Postiz media storage
  postiz-config:     # Postiz configuration
  postiz-pgdata:     # Postiz database
  redis-data:        # Postiz cache

Environment File

# .env

# --- Your App ---
NEXTAUTH_SECRET=generate-a-random-32-char-string-here
ADMIN_EMAIL=you@yourdomain.com
ADMIN_PASSWORD=your-secure-password

# --- Claude (pipeline engine) ---
ANTHROPIC_API_KEY=sk-ant-your-key

# --- Research ---
TAVILY_API_KEY=tvly-your-key

# --- Postiz (publishing + media hosting) ---
POSTIZ_API_KEY=your-postiz-key
POSTGRES_PASSWORD=a-secure-password-for-postiz-db

# --- Nextdoor (direct API — Postiz doesn't support it) ---
NEXTDOOR_API_TOKEN=your-token
NEXTDOOR_ADVERTISER_ID=your-id

Startup

# First run
docker compose up -d
docker compose exec app npx prisma db push    # Create tables
docker compose exec app npx prisma db seed    # Create admin user

# Open in browser
open http://localhost:3000

After first run, just docker compose up -d and everything starts.


Project Folder Structure

marketing-command-center/
├── app/                              # Next.js App Router
│   ├── (auth)/
│   │   ├── login/page.tsx
│   │   └── layout.tsx
│   ├── (dashboard)/
│   │   ├── layout.tsx                # Sidebar + header
│   │   ├── page.tsx                  # Dashboard home
│   │   ├── campaigns/
│   │   │   ├── page.tsx              # Campaign list
│   │   │   ├── new/page.tsx          # New campaign form
│   │   │   └── [id]/
│   │   │       ├── page.tsx          # Pipeline progress tab
│   │   │       ├── assets/page.tsx   # Asset gallery tab
│   │   │       └── chat/page.tsx     # Claude feedback tab
│   │   ├── assets/page.tsx           # Global asset library
│   │   ├── trends/page.tsx           # Trend reports
│   │   └── queue/page.tsx            # Postiz queue / calendar
│   └── api/
│       ├── auth/[...nextauth]/route.ts
│       ├── campaigns/
│       │   ├── route.ts              # CRUD
│       │   ├── [id]/
│       │   │   ├── launch/route.ts   # Trigger pipeline
│       │   │   └── stream/route.ts   # SSE pipeline progress
│       ├── assets/
│       │   └── route.ts              # Asset CRUD + filtering
│       ├── claude/
│       │   └── route.ts              # Chat sessions (stream)
│       ├── postiz/
│       │   └── route.ts              # Upload + schedule to Postiz
│       └── files/
│           └── [...path]/route.ts    # Serve local pipeline files
├── components/
│   ├── ui/                           # shadcn/ui
│   ├── campaign-card.tsx
│   ├── asset-gallery.tsx
│   ├── asset-card.tsx
│   ├── pipeline-progress.tsx
│   ├── claude-chat.tsx
│   ├── postiz-push-modal.tsx
│   ├── trend-report-viewer.tsx
│   └── campaign-form.tsx
├── hooks/
│   ├── usePipelineProgress.ts        # SSE hook
│   └── useClaudeChat.ts             # Chat streaming hook
├── lib/
│   ├── auth.ts                       # NextAuth config
│   ├── prisma.ts                     # Prisma client
│   ├── claude.ts                     # Claude SDK wrapper
│   ├── postiz.ts                     # Postiz SDK wrapper
│   └── utils.ts
├── prisma/
│   ├── schema.prisma                 # Database schema
│   ├── seed.ts                       # Admin user creation
│   └── data/                         # SQLite file lives here
│       └── marketing.db
├── pipeline/                         # Your marketing pipeline
│   ├── .claude/
│   ├── skills/
│   │   ├── trend-scout/SKILL.md
│   │   ├── marketing-research-agent/SKILL.md
│   │   ├── script-writer/SKILL.md
│   │   ├── ad-creative-designer/SKILL.md
│   │   ├── video-ad-producer/SKILL.md
│   │   ├── copywriter-agent/SKILL.md
│   │   └── distribution-agent/SKILL.md
│   ├── knowledge/
│   │   ├── brand_identity.md
│   │   ├── platform_guidelines.md
│   │   └── product_campaign.md
│   ├── assets/                       # Brand images, logos
│   ├── outputs/                      # All generated content
│   ├── remotion-ad/                  # Remotion video project
│   ├── CLAUDE.md
│   └── package.json
├── public/
├── .env
├── docker-compose.yml
├── Dockerfile
├── next.config.ts
├── tailwind.config.ts
├── tsconfig.json
└── package.json

Pages & Features

1. Dashboard Home (/)

  • Active campaigns with status badges (Running / Review Needed / Published)
  • Recent trend reports from Trend Scout
  • Quick stats: assets created, published this week, pending review
  • "New Campaign" button

2. Campaign View (/campaigns/[id])

Pipeline Tab — real-time agent execution progress via SSE:

✅ Trend Scout        12s    "5 trending hooks found"
✅ Research Agent      2m     "Market report generated"
✅ Script Writer       3m     "15 scripts written"
🔄 Ad Creative        1m...  "Generating static ads..."
⏳ Video Producer     —      waiting
⏳ Copywriter         —      waiting
⏳ Distribution       —      waiting (pending approval)

Assets Tab — gallery of everything produced:

  • Grid of image/video previews with platform badges
  • Filter by: platform, type (image/video/copy), status
  • Click to expand: full preview + caption + metadata
  • Approve / Reject buttons per asset
  • "Push to Postiz" on approved assets

Claude Tab — live feedback chat:

  • Streaming chat interface connected to Claude Code
  • Session persists via --resume
  • Type: "make it snarkier", "add competitor X to research", "regenerate hook B"
  • New/updated assets auto-appear in the gallery

3. Asset Library (/assets)

  • All assets across all campaigns
  • Search by hook text, caption, platform
  • Bulk approve / reject / push to Postiz
  • Auto-generated trend reports (iframe HTML viewer)
  • "Create Campaign from This Report" button

5. Postiz Queue (/queue)

  • Everything pushed to Postiz with status (scheduled / published / failed)
  • Calendar view of upcoming posts
  • Link to Postiz dashboard at localhost:5000 for visual calendar

What Runs Where

Component Runs In External Calls
Next.js dashboard Docker (your machine) None — fully self-contained
SQLite database Docker volume None
Auth (NextAuth) Docker (your machine) None
File serving Docker (your machine) None
Pipeline progress (SSE) Docker (your machine) None
Claude Code sessions Docker (spawns subprocess) → Anthropic API
Research agent Claude subprocess → Tavily API
Image generation Claude subprocess → NanoBanana (Gemini API)
Video rendering Claude subprocess → Local Remotion render
Postiz Docker (your machine) → Instagram API, TikTok API
Nextdoor publishing Your app API route → Nextdoor API

Outbound network calls: Anthropic, Tavily, Gemini (NanoBanana), Instagram (via Postiz), TikTok (via Postiz), Nextdoor. Everything else is local.


Build Phases

Phase 1: Scaffold (Week 1)

  • Next.js + shadcn/ui + Tailwind setup
  • Prisma + SQLite schema
  • NextAuth credentials provider
  • Docker Compose with Postiz
  • Basic dashboard layout with sidebar nav

Phase 2: Pipeline Integration (Week 2)

  • Campaign CRUD pages
  • Campaign launch form → spawns Claude Code subprocess
  • SSE pipeline progress streaming
  • File serving API route for asset preview
  • Auto-scan outputs folder → populate assets table
  • Asset gallery with grid view + filtering
  • Image/video inline preview
  • Approve / reject workflow
  • Claude chat component with streaming
  • Session persistence (--resume)

Phase 4: Postiz Publishing (Week 4)

  • Postiz upload integration
  • "Push to Postiz" modal (select → schedule → confirm)
  • Queue page with Postiz status sync
  • Nextdoor direct API publishing
  • Trend report viewer

Phase 5: Polish (Week 5)

  • Bulk actions (approve all, reject all, push batch)
  • Asset search
  • Mobile-responsive
  • Trend Scout auto-scheduling (cron in Docker)
  • Error handling + retry logic