Back to portfolio
SERVICE PAIR · ports 8006 + 8007 · live

Blog

Two services, one story. Blog Public (port 8006) renders fast, cached, anonymous. Blog Admin (port 8007) is authenticated, drives an AI authoring pipeline, and only writes published content via an explicit publish gate. They share a database; they don't share a service surface.

Read the blog →

From idea to published post

The author drives; the AI assists; the publish gate enforces the contract between admin and public.

Why two services, not one

A single Flask app would be smaller. It would also leak: the admin surface (drafts, AI prompts, cost ledger) would always be one auth-bypass bug away from the public. Splitting into two services means the public service has zero code paths that touch drafts. It can't accidentally serve them. It doesn't even know they exist.

Same database, different lanes. The admin writes; the public reads WHERE published = TRUE. The publish step is the only way bytes cross the line.

Components

Blog Public — port 8006
read-only · cached · anonymous
Pure read service. Serves /blog/<slug> from a published-rows view. Aggressive HTTP cache headers; no admin imports anywhere in the dependency tree.
Flask cache read-only
Blog Admin — port 8007
auth · drafts · AI authoring
Authenticated dashboard. Lists drafts, exposes the AI authoring pipeline, owns publish/unpublish. Sees everything; the public service can't reach it.
Flask portal-auth drafts
AI authoring pipeline
outline → draft → polish
Three-stage pipeline gated by author approval at each step. Outline first (Sonnet, fast iteration). Draft (Opus, depth). Polish (Sonnet, restraint). Author owns the cursor at every gate.
Sonnet Opus approval gates
PostgreSQL — shared
posts · drafts · published flag
Single table for posts; published bool is the contract. Public reads with that filter; admin writes with policy. Nothing else cross-cuts.
PostgreSQL shared single source of truth
Publish gate
explicit · audited · reversible
A button, not a side effect. Logs who, when, and what state the post was in. Unpublish is one click and idempotent. The gate is the only place where unpublished bytes become public.
explicit audit log reversible
Memory Archive context
grounded authoring
When a post talks about a service or a decision, the AI pipeline pulls the corresponding KG node and recent memories so claims are grounded in reality, not training data.
MCP kg_query

Operating principles

Author > AI
Every AI output is a suggestion. The author owns the cursor — outline approved, draft approved, polish approved. Skip a gate? The post doesn't ship.
Read service stays boring
No clever middleware on the public side. Static-y, cacheable, fast. If it ever has to do anything interesting, that's a sign the boundary is wrong.
Cost ledger applies
Every authoring-pipeline LLM call writes a cost row. FinOps Agent flags an outlier post the same way it flags an outlier resume.
Drafts can't escape
There is exactly one code path that flips published to true. It lives in the admin service. The public service can't reach it.

More of the system

Two services for one product is a deliberate boundary, not duplication. See how Memory Archive grounds the authoring pipeline, or how the deploy pipeline ships both services on every change.