Skip to content

OpenGraph-AI/AI-CMS

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

24 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

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