Broadside
Multi-brand editorial desk seeded from your own project docs
Writing in my own voice doesn't scale — but writing drafts in my own voice can, if the inputs are specific enough. Broadside ingests my project docs and writes posts in the dialect the docs taught it.
I kept promising myself I’d start publishing more — on LinkedIn, Substack, Bluesky, X. The promise never held because writing doesn’t scale: every draft needs my attention, my voice, my specific angle. Broadside is the attempt to make drafts that already sound like me by feeding the drafting system the thing that knows my voice best — my own project docs. Git commits, READMEs, CLAUDE.md, CHANGES.md, migration narratives. The retrieval scoring is tilted toward the files where specifics live — CHANGES.md weighted at 5.0, CLAUDE.md at 4.0, README.md at 2.5 — so the drafts come out citing real commits, real numbers, and real decisions instead of generic industry-blog filler. Currently pivoting from a single-brand direct-publishing tool into a multi-brand editorial desk — Phase 1 of 4.
Next.js 15 + React 19 on port 7720 on Furnace, Postgres 16 with pgvector 0.6, and five drafting LLMs (Anthropic Claude Opus 4.6, OpenAI GPT-4o, Google Gemini 2.5 Pro, DeepSeek V3.1, xAI Grok 3), each chosen per request for its voice flavor. Embeddings come from Forge’s qwen3-embed-8b at 768 dimensions, with OpenAI’s text-embedding-3-small as the fallback when Forge is in Creative Mode. Publishing routes through Blotato for nine-platform distribution, or direct to platform APIs when the post calls for something Blotato doesn’t handle — Twitter threads, Bluesky chained replies, Dev.to articles, Resend newsletter sends. Read-only access to Nexus via a scoped broadside_reader role so it can mine the agent data warehouse for additional context.
The multi-brand pivot is the hardest part right now. For the first six months Broadside was single-brand; brand became a column on content_groups in a 2026-04 migration and started showing up on every new INSERT path — and most of them silently didn’t set the column, so every post hit the publish gate with brand_id=NULL and failed in ways that looked like unrelated errors. Migration 019 backfilled the orphans via parent-inherit loops, then a niclydon-com default, flipped brand_id to NOT NULL, and made every INSERT INTO content_groups call site explicit about which brand. The pattern I’ll keep: when adding a column that becomes load-bearing for a downstream gate, NOT NULL it on the migration that makes it load-bearing, not the one that introduced the column. Nullable columns silently accept the missing data and the bug reappears somewhere else.