There’s a pattern I keep seeing in platform work: systems that do a lot but know very little about themselves. The logic is spread across files, the decisions that shaped those files exist only in someone’s memory, and the data the system generates never feeds back into what the system does next.

I built FABRIC/SOCIAL to be the opposite of that. This is the story of what that took — and what I learned doing it.

Start With the Decision Log

Before writing a single line of new code, the right move is to document what already exists. Not the what (the code already shows that) — the why.

Architecture Decision Records are the tool for this. An ADR is a short document that captures a single decision: what the context was, what the options were, what was chosen, and what constraints that choice creates going forward. They’re immutable — you don’t update them, you write a new one that supersedes the old one. That immutability is the point. It gives you a timestamped trail of how a system evolved and why.

Over the last few months, FABRIC/SOCIAL accumulated 16 ADRs. ADR-001 documents the zero-footprint architecture decision (state in Redis and Qdrant, no Kubernetes, no Docker). ADR-009 documents the Cloudflare Workers subrequest budget constraint — 50 subrequests per invocation on the free tier — and the batching rules that follow from it. ADR-011 documents the end-to-end publishing pipeline for YouTube Shorts. ADR-016 documents the full API surface and how it maps to the platform’s two-tier navigation model.

The discipline of writing ADRs before building forces a specific kind of thinking. You have to articulate the constraint before the code can react to it. ADR-009, for example, exists because I’d already blown the subrequest budget twice with per-item Redis loops. Writing the ADR turned “don’t do that again” into a named constraint with explicit rules: batch reads with MGET, cap dispatch loops, offload bulk work to GitHub Actions. Those rules are now in CLAUDE.md and get enforced on every new route.

Document Everything Before You Wire It

The platform had a full intelligence layer — AEO engine, newsletter digest, podcast script generator, whitepaper detector, Forge content pipeline, shortlinks — that was fully implemented but had no HTTP routes and no documentation. It worked in isolation (MCP tools, GitHub Actions) but wasn’t accessible to anything else.

Before adding routes, I did a documentation pass. Read every source file. Map every function. Find every gap.

The gaps I found:

  • clickCount() existed in the shortlink module but had no HTTP route — you could register a shortlink but couldn’t query how many times it had been clicked
  • generateAeoProfile() and scoreAeoContent() were MCP-only — the AEO engine had no HTTP surface at all
  • generateEpisodeScript() generated podcast episode scripts but immediately discarded them — there was no storage path, so every generate was a one-way Claude call with no retrieval
  • The newsletter dispatcher sent to a single address rather than a Resend audience — the gap between “working” and “production-ready” right there in one function

Documentation first. Then routes.

Wire Everything as an API

Once the gaps are visible, the work is straightforward: close them. Fourteen new endpoints, all following the same pattern: Hono router, named export, registered in index.ts, TypeScript types updated in both the backend and the frontend API client simultaneously.

The new routes break into five categories:

Intelligence layer exposure. GET /api/aeo/profile/:id, POST /api/aeo/profile, POST /api/aeo/score. GET /api/shortlink/:code/stats. GET /api/egress/route/:postId — which queues contain this post ID and at what score. GET /api/egress/newsletter/status, GET /api/egress/whitepaper/status. POST /api/egress/podcast/episode now stores the generated script; GET /api/egress/podcast/episode/:id retrieves it.

Operational visibility. GET /api/health/deep — all external dependency health in one call (Redis latency, Qdrant reachability, LinkedIn token expiry, YouTube token status, whether each API key is configured). GET /api/platform/pulse — live state of every queue, every token, every channel. One request, full picture.

Content graph traversal. GET /api/content/map — every campaign linked to its shortlinks, AEO profile, egress channel presence, and YouTube brief in one traversal. No client-side stitching. This is the bonus API that makes the others useful together.

Attribution. GET /api/campaign/:id/shortlinks — short codes and click counts for a campaign’s associated URLs. You publish a Short, it generates a shortlink, the carousel QR code encodes that link, and now you can see how many clicks came from the physical QR code versus the YouTube description. That’s a closed attribution loop.

Ecosystem. GET /api/forge/ecosystem — exposes the ry-ops GitHub repo index that the Forge grounding prompts already used internally. Now it’s callable.

The pattern for each: read the existing function, understand what it already does, expose it. No new logic in the route. The route is a thin HTTP shell around existing service code.

Analytics That Drive the Content

Here’s where it gets interesting. Every platform has analytics. Most platforms let you look at them. This one uses them.

YouTube Studio generates natural language summaries of channel and video performance. When you ask “how did viewers find my content?” or “summarize my latest video performance,” it returns a narrative — traffic source breakdowns, retention metrics, comparative performance vs. typical. That text is a structured signal dressed up as prose.

The YouTube Insight Engine ingests that signal. It classifies the input type — Studio channel summary, Studio video summary, or raw transcript — and runs Claude extraction to produce structured intelligence:

For channel summaries: dominant_discovery, traffic_sources, search_strength, shorts_dominant, key_findings, recommendations. When I pasted the channel analytics showing 80% of views from the Shorts feed, the engine extracted shorts_dominant: true, search_strength: emerging, and generated two actionable recommendations specific to ry-ops.

For video summaries: hook_quality, rewatch_rate, avg_pct_viewed, topic_signals, follow_up_suggested, campaign_angle. When I pasted the performance card for “Securing Cortex Against Prompt Injection Attacks” — 75% stayed to watch, 137.9% average percentage viewed — it extracted hook_quality: exceptional, rewatch_rate: high, follow_up_suggested: true, campaign_angle: "deep-dive Short on prompt injection mitigations in Cortex".

For transcripts: topics, entities, key_claims, content_gaps, suggested_post_angles, aeo_keywords. A transcript becomes a content brief.

These extracted insights are stored in Redis (90-day TTL) and become Aiana’s context source #10 — joined alongside Cloudflare traffic data, LinkedIn analytics, YouTube upload stats, archive backlog, and campaign state. Every conversation Aiana has from this point forward is informed by what the analytics say.

The detection is heuristic-based: phrase matching on Studio-specific language (“stayed to watch,” “vs. typical,” “traffic source,” “Shorts feed”), timestamp patterns for transcripts, length thresholds for filtering conversational messages from paste events. It’s not ML — it’s pattern matching on known vocabulary. For a system that only ingests one person’s YouTube Studio data, that’s the right level of complexity.

Aiana Closes the Loop

The insight pipeline needed a front door. The API endpoint (POST /api/insights/ingest) handles programmatic intake — MCP tools, scripts, CI workflows. But the natural interaction pattern is conversational: you’re looking at Studio data, you copy it, you paste it into the chat.

The chat route detects this automatically. Any message over 300 characters that contains Studio-specific phrases gets silently routed through the insight engine before reaching Aiana. The insight is stored. The context is rebuilt. Aiana responds already knowing what was just ingested.

The result in practice: I pasted the performance card for the Cortex Short into Aiana. She responded with the rewatch analysis, connected it to the LinkedIn CI/CD security post performing simultaneously, and offered to search the archive for prompt injection adjacent content. That cross-channel connection — “security content is working on both YouTube and LinkedIn right now” — wasn’t in any single data source. It came from Aiana having both the YouTube insight and the LinkedIn snapshot in context at the same time.

The UI surfaces a confirmation badge when an insight is stored — a small Database icon with “Video insight saved” and the insight ID. Subtle, but it closes a feedback loop: you know the paste registered, you know it has a retrievable ID, you know Aiana will have it in every subsequent conversation.

What the Architecture Actually Teaches

The specific features are less important than the pattern they embody.

Document decisions before code. ADRs create named constraints. Named constraints become rules. Rules become the CLAUDE.md that gets enforced automatically. The discipline of writing the ADR is what makes the constraint durable.

Expose everything as an API. If logic exists in your codebase and has no HTTP route, it doesn’t exist for other systems. The AEO engine, the shortlink stats, the egress routing — all of it was real, working code that was invisible until it had a route. Visibility is prerequisite to integration.

Close feedback loops. A platform that generates content but doesn’t learn from how that content performs is running open-loop. The insight engine closes that loop: analytics → structured signal → context → generation. What performs well shapes what gets made next.

Let the AI handle the ambiguity. The insight detection doesn’t try to parse Studio data formats — it hands the text to Claude with a structured extraction prompt. The prompt knows what ry-ops cares about (security content, Shorts performance, search strength, follow-up opportunities). The model handles the variability in how Studio formats its output. That division of labor — deterministic routing, AI-powered extraction — is the right one.

Conversational intake lowers the activation energy. The difference between “go to /api/insights/ingest, write a curl command, paste your Studio data as JSON” and “paste it into the chat” is enormous in practice. The second one gets done. The first one doesn’t. Aiana as the intake point isn’t a UX nicety — it’s the difference between a system that gets used and one that doesn’t.

The platform still has gaps. The newsletter needs to send to an audience, not a single address. The podcast voice IDs need to be cast. The AEO profiles need to surface in blog post API responses. Those are known, documented, named gaps in MEMORY.md — which means they’re in scope for the next session, not lost in some backlog no one reads.

That’s the meta-pattern: document it, expose it, close the loop, repeat.

Frequently Asked Questions

What is the difference between an ADR and regular technical documentation?

Regular technical documentation describes what a system does, while an ADR captures why a specific decision was made — including the context, alternatives considered, and constraints the decision creates. ADRs are immutable by design, so they are never updated; instead, a new ADR is written to supersede an old one, preserving a timestamped audit trail of how a system evolved.

How do you handle the Cloudflare Workers 50 subrequest limit in a content platform?

The Cloudflare Workers free tier limits invocations to 50 subrequests. To stay within this budget, the recommended approach is to batch Redis reads using MGET instead of per-item loops, cap dispatch loop iterations, and offload bulk processing work to GitHub Actions rather than handling it inline within the Worker invocation.

When should you write an ADR in a software project?

An ADR should be written before writing the code that implements the decision, not after. Writing the ADR first forces you to articulate the constraint or tradeoff explicitly, which turns implicit knowledge into a named rule that can be referenced in code, automation, and team onboarding.

What makes a content platform ‘self-documenting’?

A self-documenting content platform is one where the decisions that shaped the system are captured in structured records (like ADRs), the system exposes a well-defined API surface, and the data the system generates feeds back into how the system behaves — rather than existing only in code comments or team memory.