Architecture, in one picture
is_current_approval() is
computed at call time, not cached from generation time, so revoking an approval or
letting one expire stops a publish that was already queued.
changes_requested is terminal on purpose. Generated content is
append-only and versioned, so a reviewer never edits model output in place. They
reject it and re-run generation, which produces a new lineage and keeps the audit
trail honest about what the system actually produced.
What it does
Social marketing is mostly repetitive work. Someone reads the brand guidelines, decides what to post this week, writes it four different ways for four platforms, finds an image, and schedules it. Every week, forever.
This does that work. It ingests the brand’s own material and keeps it in a vector store, so what it writes comes from real source documents rather than a general impression of the brand. It proposes a plan, drafts the copy, adapts it per platform, generates branded images, and queues everything for review.
Then it stops. Nothing reaches the public until a person approves it.
How it is built
A modular monolith on FastAPI, not microservices and not a chat wrapper. One deployable with clear module boundaries: identity, brand knowledge, strategy, content, approvals, publishing, analytics.
| Layer | Choice | Why |
|---|---|---|
| Backend | FastAPI, Python | One deployable, clear module bounds |
| Client | Expo, React Native, TypeScript | Web, iOS and Android from one codebase |
| Database | PostgreSQL via Supabase | Transactional source of truth |
| Vectors | pgvector, in the same Postgres | Retrieval without another system to run |
| Text | Gemini, with a Cloudflare Workers AI adapter | Both behind one llm.generate port |
| Images | Cloudflare Workers AI, FLUX.1 | Behind an image.generate port |
| Tools | MCP | Scoped access that cannot bypass app authorization |
| Orchestration | n8n, self-hosted | Publish and schedule jobs |
| Delivery | GitHub Actions, Render, Cloudflare Pages | Lint, types, tests, security scans, eval gates |
The design rule behind all of it: every provider is replaceable. Text generation, embeddings, image generation, storage and each social platform sit behind a port. Swapping Gemini for something else is a config change, not a rewrite. Nothing is a permanent bet on one vendor.
The rules it will not break
This is the part I actually care about, and the part most AI products skip.
It fails closed. Publishing, rescheduling and deleting all require a live, scoped, unexpired human approval. When that approval is missing or has expired, the system does nothing. It never decides an action is probably fine.
It does not fabricate success. A provider action is not reported as done until it has been confirmed and reconciled against what the provider actually returned. A queued post that silently failed is worse than one that never sent.
Every claim carries a citation. Brand profile extraction and generated copy trace back to the source chunk they came from, so a person reviewing a draft can check where a claim about the brand actually came from.
Prompt injection is treated as a real threat, not a footnote, anywhere user-supplied or web-scraped content reaches a model call.
Everything is logged. Every state change, approval decision and publish attempt records who did it and when.
CI is a gate, not a formality. Lint, type check, the full test suite and AI eval gates all run before anything merges, including a pass that stops the commit gate itself from failing open.
Where it is now
Built and tested: brand ingestion and RAG, brand profile extraction with citations, strategy and weekly planning, copy generation with platform adaptation, branded image generation, and the approval inbox with risk flags, citations and expiry.
In progress: real social connectors starting with Threads, scheduled publishing, and the n8n orchestration layer, which is architected but not yet integrated.
I am deliberately precise about that split. The interesting claim here is not that it works, it is that it refuses to act when it should not, and that is only worth anything if I am honest about what is finished.