Skip to content

OpenGraph-AI/AI-CMS

Repository files navigation

1AI X CMS

Agentic CMS that turns a free-text brief into luxury-grade product imagery or video, then routes the artefacts through an approval + project-review workflow.

Pipeline: Brief → OpenAI Intent → OpenAI Planner (registry-constrained) → OpenRouter execution (Vercel AI Gateway image fallback) → asset uploaded to Supabase Storage → approval workflow → projects → reviewer sign-off. Deployed end-to-end on Render as two services from one render.yaml Blueprint.

Stack

Layer Tech
Backend Python 3.11+, FastAPI, OpenAI SDK, httpx (OpenRouter + Vercel AI Gateway over OpenAI-compatible REST), Pydantic v2 — runs as a Docker web service on Render (or any Docker host)
Database Supabase Postgres (via PostgREST + direct pooler for migrations)
Asset storage Supabase Storage (public bucket)
Frontend Next.js 15 (app router), Tailwind, TypeScript, framer-motion — Render (Node web service)

The generation pipeline is a streaming state machine: POST /generate is a Server-Sent Events endpoint that yields a full JobStatus JSON after every transition (queued → intent → brand_context → planning → awaiting_confirm | executing → done | error). The frontend renders each frame as it arrives, so the chat journey fills in step by step in real time instead of waiting 30–50s for a single response. Video jobs pause at awaiting_confirm for a cost-gate confirmation, then GET /jobs/{id} polling drives the provider's queue to completion. No background workers, no Redis, no long-lived sockets — just streamed HTTP and short polls, both of which work cleanly behind Render's edge.

Provider routing: Each registry entry has a primary slot (OpenRouter) and a fallback slot (Vercel AI Gateway, for images only — the gateway's video API is AI-SDK-only and not addressable over plain REST). The executor in backend/provider_executor.py transparently retries the fallback on any primary error.

Local development

# 1. backend
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
cp .env.example .env   # fill in the secrets below
python -m backend.migrate              # one-time: apply CMS schema to Supabase
uvicorn backend.main:app --reload --port 8000

# 2. frontend (new terminal)
cd frontend
cp .env.example .env.local              # set BACKEND_URL=http://localhost:8000
npm install
npm run dev                             # http://localhost:3000

Required env vars

Key Where Notes
OPENAI_API_KEY backend gpt-4.1 (intent + planner), gpt-4o (vision)
OPENROUTER_API_KEY backend primary image + video provider (sk-or-...)
AI_GATEWAY_API_KEY backend fallback for images. Optional in dev — without it, an OpenRouter error surfaces directly to the user instead of falling back
AUTH_USERNAME / AUTH_PASSWORD backend single admin login; defaults to admin / 1601admin
AUTH_SESSION_SECRET backend signs the session cookie. Generate one with python -c "import secrets; print(secrets.token_urlsafe(48))"; without it sessions reset on every restart
AUTH_COOKIE_HTTPS_ONLY backend (prod) set to 1 behind HTTPS so the session cookie is Secure
SUPABASE_URL backend https://<ref>.supabase.co
SUPABASE_SERVICE_ROLE_KEY backend service-role JWT, never expose to browser
SUPABASE_ASSETS_BUCKET backend public bucket name (e.g. luxury)
DATABASE_URL backend (migrations only) Supabase session-pooler URL; percent-encode @ in password as %40
FRONTEND_ORIGINS backend (prod) comma-separated allowed origins for CORS
BACKEND_URL frontend server-only; used by the proxy route

Upstash Redis is no longer required. The confirm-gate that used to block on Redis is now a direct DB state transition driven by the client (POST /jobs/{id}/confirm).

Model registry

The planner picks model_id from the primary slot of each entry in backend/registry.py — it cannot invent IDs. Fallbacks are executor-internal and hidden from the planner prompt.

Job OpenRouter primary Vercel AI Gateway fallback Why
Hero still (no ref) black-forest-labs/flux.2-max bfl/flux-2-max Most photoreal Flux on both gateways
Hero still (with product ref) google/gemini-3-pro-image-preview (Nano Banana Pro) google/gemini-3-pro-image Preserves product identity for catalog
Branded composition openai/gpt-5.4-image-2 recraft/recraft-v4.1-pro Real typography + composition control (gateway fallback restores Recraft parity)
Image → video kwaivgi/kling-v3.0-pro bytedance/seedance-2.0 (OpenRouter, same provider) Best fabric/metal/liquid motion
Text → video google/veo-3.1-fast openai/sora-2-pro (OpenRouter, same provider) Native audio + strongest narrative

Video has no cross-provider fallback — Vercel AI Gateway only exposes video through its AI SDK v6 (Node.js); there's no REST endpoint for video that we can call from Python. If OpenRouter video fails, the job ends in status=error with a clear message rather than hanging. Image fallback to the gateway still works because images go through /v1/images/generations (REST).

CMS workflow

brief → generation → done → ✓ Approve  →  Approved sheet  →  bundle into Project  →  assign → reviewer approves

Five personas (Souvik, Sara, Marco, Léa, Devon) are seeded on first boot; the active persona is stored in app_state.current_user and switched via the topbar avatar.

API surface

Generation

  • POST /generate — multipart form (brief, optional product_image, brand_guidelines, mood_board[])
  • GET /jobs/{id} / GET /jobs?limit=N
  • POST /jobs/{id}/confirm — required before video jobs run (cost guard)

Catalogues (filtered view over jobs)

  • GET /catalogues?status=approved|pending|rejected|all
  • POST /catalogues/{id}/approve|reject|unapprove

Projects

  • POST /projects (empty or with catalogue_ids[])
  • GET /projects / GET /projects/{id}
  • PATCH /projects/{id} — name, description, assignee, due_date
  • POST /projects/{id}/catalogues (bulk add) / DELETE /projects/{id}/catalogues/{cid}
  • POST /projects/{id}/assign|approve|reject|reopen|comments
  • DELETE /projects/{id}

Users

  • GET /users / GET /users/me / POST /users/me

System

  • GET /health / GET /registry / GET /assets/{filename} (local-fallback)

Production deployment (end-to-end on Render)

Both halves of the app deploy as two services from a single render.yaml Blueprint: the FastAPI backend on Docker, the Next.js frontend on Node. The frontend's BACKEND_URL is auto-wired to the backend service's hostname via Render's fromService syntax, so there's no manual copy-paste between deploys.

1. Supabase

  • Create a project, copy Project URL + service_role JWT (Settings → API).
  • Create a public Storage bucket (e.g. luxury).
  • Note the session pooler URL from Settings → Database → "Session pooler (IPv4 compatible)". Percent-encode any @ in the password as %40. This is DATABASE_URL — used only by the migration runner.

2. Deploy the Blueprint

  1. Push the repo to GitHub.
  2. Render → New → Blueprint → connect the repo. Render reads render.yaml and provisions both services.
  3. When prompted, paste every sync: false secret listed in render.yaml:
    • OPENAI_API_KEY
    • OPENROUTER_API_KEY (primary image + video provider)
    • AI_GATEWAY_API_KEY (image fallback only; optional but recommended)
    • SUPABASE_URL
    • SUPABASE_SERVICE_ROLE_KEY
    • SUPABASE_ASSETS_BUCKET
    • DATABASE_URL (migration runner only)
    • FRONTEND_ORIGINS — leave blank for first deploy; set after step 3 once Render hands you the frontend URL.
  4. Render builds Dockerfile for the backend, runs python -m backend.migrate as the pre-deploy step (idempotent — schema_migrations table tracks applied files), then starts uvicorn on $PORT. It separately runs npm run build + npm start for the frontend out of frontend/.

3. Wire CORS once

After the first deploy, copy the frontend's Render URL (e.g. https://1ai-x-cms-web.onrender.com) and set it as FRONTEND_ORIGINS on the backend service. Trigger one redeploy to pick it up.

Deploy verification

curl https://<backend>.onrender.com/health
# { "ok": true, "db": "supabase", "asset_storage": "supabase" }

curl https://<frontend>.onrender.com/api/proxy/health
# same — confirms the Next.js proxy can reach the backend

curl https://<frontend>.onrender.com/api/proxy/users | jq length   # → 5

Open the frontend URL, submit a brief, watch the streamed pipeline fill in step by step — intent → brand → plan → result — with each frame arriving over Server-Sent Events.

Why a long-lived host instead of serverless

The streaming /generate endpoint and the FAL-queue-style video polling both run longer than typical serverless function caps. Render's Web Service has no per-request cap, which keeps the architecture simple — no background queue, no SSE-disconnect-then-reconnect dance. If you ever want to deploy elsewhere, the same Dockerfile runs on Fly, Cloud Run, ECS, Railway, etc. — point the frontend's BACKEND_URL at the new host and the proxy route handles the rest.


Out of scope (v1)

Real auth (Clerk / Supabase Auth), notifications, per-asset comments, asset versioning, multi-assignee review, project export.

Security

  • .env is gitignored. Never commit it.
  • Service-role keys and OpenRouter / OpenAI / Vercel AI Gateway keys authenticate as your account — treat them like credit-card numbers. Rotate any key that's been shared in chat.
  • The service_role JWT bypasses Supabase row-level security; that's why it stays server-side.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors