Marketing mission control — a single home base where marketers run their AI-assisted and manual marketing work: campaigns, copy, scheduled sends, channel integrations.
Pre-launch. Target launch: this week. See
LAUNCH_CHECKLIST.md for the runbook and
.github/workflows/launch-readiness.yml
for the automated audit → harden → package pipeline.
A web app where a marketer can:
- brief a campaign once,
- co-write copy with an AI assistant,
- send / schedule across email and social,
- track AI token usage per workspace,
- and see what's running, what's drafted, and what's performing.
- Next.js 15 + TypeScript + Tailwind + shadcn/ui
- Postgres (Neon) +
pgwith forward-only SQL migrations (npm run db:migrate). The persistence layer sits behind a repository interface (lib/auth/repository.ts); Prisma was the original freeze choice but its engine downloads are blocked in the offline orchestrator container, so the shipped implementation is plain parameterized SQL — swapping in Prisma later is a one-file change. - Auth.js v5 — signup, login, logout, password reset (credentials + JWT sessions; one-time hashed reset tokens)
- DeepSeek v4-flash for AI assist — server-side proxy at
POST /api/ai/draft(streaming via the sharedGAPOS_LLM_API_KEY; the OpenAI-style upstream is converted to the same Anthropic-shaped SSE the client always consumed). Every request records oneai_usagerow, attributed to the caller's workspace.LLM_DEV_MODE=1serves a canned stream when no key is set. - Image generation —
POST /api/ai/image: Gemini only (DeepInfra FLUX was removed per owner direction). Default/draft tier uses Gemini 2.5 Flash Image (GOOGLE_API_KEY);hero: trueroutes to gemini-3-pro-image with a hand-safe prompt suffix and an automatic vision-QA gate (PASS/FAIL on deformed hands/artifacts, up to 3 regeneration attempts) — only QA-passed images are returned. Returns a data URL to the browser. - Resend for transactional + marketing email
- Scheduled sends — a Postgres-backed queue (
send_schedules) with a cron+queue runner:npm run worker(long-running poller) orGET /api/cron/send(cron-triggered tick). No external queue service needed; see "Scheduled sends". - Campaigns —
campaignstable +POST/GET /api/campaigns; the pilot UI's "Send to Draft" persists a real campaign row. - Sentry + pino for observability
- Railway for hosting (Dockerfile standalone build +
railway.json)
cp .env.example .env
# fill in DATABASE_URL, AUTH_SECRET, GAPOS_LLM_API_KEY, RESEND_API_KEY
# (GAPOS_LLM_API_KEY is the shared DeepSeek key; GOOGLE_API_KEY enables image
# generation — Gemini 2.5 Flash Image for drafts, gemini-3-pro-image for
# hero shots; LLM_DEV_MODE=1 + RESEND_DEV_MODE=1
# run the whole AI/send flow offline without keys)
# generate a real AUTH_SECRET: openssl rand -base64 32
# 1) database — pick one:
docker compose up -d db # Postgres on :5432, or:
npm run db:local # embedded Postgres on :5433 (no Docker needed)
# 2) apply migrations
npm run db:migrate
# 3) run the app
npm install
npm run devApp runs at http://localhost:3000. Auth flows live at /signup, /login,
/forgot-password, /reset-password; /dashboard is session-protected.
Scheduled email sends are a cron+queue setup, so they work without an external job service:
- Enqueue —
POST /api/sends/schedule(session-protected) with{ to, subject, html, scheduledFor }writes onependingrow to thesend_schedulestable (npm run db:migratecreates it — migration 0003). - Deliver — a runner tick claims rows whose
scheduled_forhas arrived and sends them through Resend, then records the outcome back on the row (sent+ Resend message id,failedwith retries, or backoff-requeued). Any scheduler drives ticks:- locally / on a VM:
npm run workerpolls the queue (docker-compose runs aworkerservice for you), - serverless: point a cron (Vercel Cron, cron-job.org, …) at
GET /api/cron/sendwith thex-cron-secretheader set toCRON_SECRET(the endpoint is otherwise 401; it returns per-tick send counts).
- locally / on a VM:
- Watch — delivery/bounce events will arrive via the webhook handler and
update
delivery_status(M3 roadmap).
For local development without a Resend account, set RESEND_DEV_MODE=1: with
no RESEND_API_KEY the runner returns synthetic message ids and the full
schedule → claim → send flow works offline. The same trick powers the AI
route: with LLM_DEV_MODE=1 and no key, POST /api/ai/draft streams a canned
draft through the real SSE pipeline.
npm run test:e2e runs Playwright against a fully offline stack: an embedded
Postgres (port 5433, all migrations) plus next dev with LLM_DEV_MODE=1 and
RESEND_DEV_MODE=1. The suite covers the M4 smoke (signup → create campaign →
AI draft → scheduled send) and axe accessibility scans (WCAG 2 A/AA, no
serious/critical violations) on the landing page and dashboard. Browsers:
npx playwright install chromium.
Every push runs three stages:
- Audit —
scripts/launch/audit.shinventories what's missing (auth, tests, health checks, docs, etc.) and writeslaunch-readiness-report.md. PRs get the report as a comment. - Harden — Gitleaks (secrets), Trivy (deps + image vulns), Semgrep
(OWASP Top 10), plus
scripts/launch/harden.shfor in-repo controls (Dockerfile non-root, pinned actions, security headers, rate limiting, etc.). Results flow into the GitHub Security tab. - Package — Builds a multi-stage Docker image, generates a CycloneDX
SBOM, scans the built image, and publishes both a container to GHCR
and a versioned
.tar.gzbundle.
The audit fails the workflow on main if any critical gap remains.
Built image is published to:
ghcr.io/mattdani21/mission-control:<version>
Release bundles are attached to the workflow run as artifacts.
See SECURITY.md.
MIT — see LICENSE.