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/<slug> from a published-rows view. Aggressive HTTP cache headers; no admin imports anywhere in the dependency tree.published bool is the contract. Public reads with that filter; admin writes with policy. Nothing else cross-cuts.Operating principles
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.