I expected the hard part to be prompt engineering. It wasn't. The hard part is what happens after generation: enforcing structural invariants so the output is actually usable -- every section present, no "coming soon" placeholders, no layout that falls apart at export time.
I ended up treating LLM output the way a compiler treats source code: intent goes in, validated artifacts come out, deterministic rules sit between the model and the user.
Live at https://gixo.ai -- sample outputs on the homepage, no signup required. This post is about the engineering, not the product.
--
STYLE GUIDES AS COMPILER RULES
Every content type has a typed style guide -- required sections, structural constraints, quality checklists, which elements are mandatory vs. negotiable. Transformations between types (blog post to video script, guide to checklist) carry a Confidence score and explicit LostElements so the user knows what the conversion costs.
The LLM is the code generator. The style guide is the type system. Output that doesn't satisfy the constraints fails -- a presentation deck missing its title slide or section dividers triggers bounded regeneration rather than silent acceptance.
WHY BLAZOR SERVER?
SignalR gives me real-time push for free, and server rendering means I access the database and message bus directly -- no separate API gateway.
The tradeoff: every user holds an open WebSocket. For a content tool where users are actively editing, the connection is already there and I use it for everything -- progress updates, collaboration, AI streaming.
The non-obvious gotcha: when a Blazor Server component calls its own /api/ endpoint, the request doesn't carry the browser's cookies. I had to write a DelegatingHandler that forwards the auth cookie from the inbound HttpContext onto the outgoing request. Without it, every internal API call returns 401. I struggled to find clear documentation for this pattern.
AUTHORIZATION WITHOUT [AUTHORIZE] SPRAWL
Once you have dozens of controllers, policy decorators become invisible. I replaced them with a single middleware scoped to API routes that separates two concepts:
1. Permission (role-based): "Is this user allowed to do this?" 2. Entitlement (subscription-based): "Has this user paid for this?"
If the entitlement service isn't registered in production, the middleware denies access rather than silently making paid routes free. Fail-closed on billing -- you only add that after you discover the alternative.
THE OPS LESSON THAT COST ME A WEEKEND
Azure App Service recycles containers. If your DataProtection keys live in memory, every restart invalidates every auth cookie. The symptom: infinite login redirect loop plus a SignalR disconnect storm. I lost a weekend to this. Fix: persist keys to durable storage outside the container.
Separately: I applied rate limiting globally and immediately broke every real-time feature. It now excludes WebSocket paths.
HONEST TRADEOFFS I HAVEN'T FIXED
Auth is overengineered. Multiple generations of authentication coexist -- cookie sessions, JWT flows, two identity enrichment paths. An internal audit calls it "overengineered in practice." I haven't consolidated because the current system works and touching auth is terrifying.
CI doesn't gate on tests. Build-and-deploy, not build-test-deploy. There are multiple test projects, but they don't gate deployment yet.
Observability is incomplete. Structured logging is solid. Distributed tracing is on the list, not in the code.
--
Happy to answer questions about the architecture, the tradeoffs, or any of the things I got wrong.