A ground-up redesign of the Sentinel architecture. Instead of writing TSX into one website
repo, every post is authored as canonical markdown in a content repo, reviewed through the
same PR loop, and on merge fanned out through platform adapters to the owned website,
Dev.to, Hashnode, Medium, Substack, and LinkedIn — own site first, syndication with canonical links after.
ported reused from Sentinelgeneralized reworked from Sentinelnew built for RelayAPIemailbrowser transport reliability
00
Triggers & channel config
generalizedscheduler + config
APScheduler cron and PR polling port straight over. The new piece is a channel registry: each
configured channel defines a niche/audience, a style profile, target platforms, and posting cadence —
so one deployment can run several content properties, not one hardcoded brand.
Same engine — Claude Code with web search, dedupe against DB + published archive, GitHub issue per
brief. Now parameterized by the channel's niche instead of hardcoded GPU topics, and briefs include a
platform plan: which platforms this piece targets and how the angle differs per audience
(a Substack read is not a Dev.to read).
claude · WebSearch WebFetchGitHub issues · briefsdedupe · DB + archive
brief issue created
02
Writing — canonical markdown
newreplaces TSX writer
The biggest break from Sentinel. Posts are written as markdown + frontmatter in a dedicated
content repo — the single source of truth all platforms render from. The branch/PR mechanics and
Claude CLI spawning port over; the TSX/POSTS-array/sitemap coupling is gone.
content/{channel}/{slug}/index.md — the post body, platform-agnostic
assets/ — hero + OG images generated alongside (media pipeline)
claude · Read Write Edit WebSearchcontent repo · PR labeled post
PR opened on content repo
03
Review loop
portedfrom agents/reviewer.py
Sentinel's most valuable machinery, kept almost intact: per-PR worktrees, resolvable line comments,
fix-and-push iteration, score gate. One change — the hardcoded pricing check becomes a
pluggable validator slot, so each channel registers its own domain checks.
iterates until pass · max 5 · reviews markdown, not TSX
Review pass
Editorial scoring · fact-check via web search · link targets WebFetched before suggesting ·
channel validators (pricing, code snippets, claims)
→
Gatescore ≥ threshold and zero open issues?
fix pushed → next iteration · fail 5× → PR closed
claude · Read Grep WebFetch BashGitHub · line commentsvalidator plugins per channel
batch settles · winner PR merged to main
04
Canonical publish — own site first
generalizedfrom auto-publish + deploy poller
Merge enqueues publish jobs, but the owned website always goes first. Its adapter renders
markdown into the site's format (TSX/MDX page, or a CMS API call for WordPress/Ghost), the ported
deployment poller confirms the page is live, and that URL becomes the post's canonical URL.
Why the ordering matters: syndicated copies on Medium, Dev.to and Hashnode
carry rel=canonical back to this URL. Publishing everywhere simultaneously would make the
platforms compete with your own site in search; canonical-first means syndication adds reach without
cannibalizing SEO.
Every adapter implements the same interface — render() transforms canonical markdown to
the platform's format, publish() ships it, verify() confirms it's live. Jobs run
from a publish queue with retries and per-platform status, so one flaky platform never blocks the rest.
Dev.toAPI
Official REST API, markdown-native, first-class canonical_url field. The easiest win.
forem API · api key
HashnodeAPI
Official GraphQL API with originalArticleURL for canonical attribution.
gql publishPost · pat token
Mediumbrowser
Official API is closed to new tokens. Playwright drives the import-story flow, which preserves the
canonical link natively.
playwright · saved session
Substackemailbrowser
No official API. Email-to-draft creates the draft reliably; Playwright polishes formatting and
hits publish/send.
smtp draft + playwright
LinkedInbrowser
Articles require automation; fallback is an API share-post linking to the canonical URL with a
generated teaser.
playwright · saved session
Own website(s)git/API
The Sentinel model, now one adapter among many — git commit for static sites, REST for
WordPress/Ghost. Supports multiple sites.
already shipped in stage 04
all jobs terminal · published or failed-after-retries
06
Verify, report, learn
generalizedfrom Slack notify + dashboard
Each adapter's verify() re-fetches its published URL. Slack gets one digest per post —
a URL per platform, failures flagged with retry state. The dashboard grows a post × platform matrix
so the whole fan-out reads at a glance. Later: pull per-platform stats back in to inform discovery.
Slack digestdashboard matrixanalytics loop · later
Deployment topology — what runs where
Relay hostone always-on server · VM (Hetzner/EC2) or a machine you own · Docker or run.sh · everything below is one deployable unit
API server + dashboard
FastAPI on uvicorn serves the REST API and the built React SPA from one process.
:8500 · single port exposed
Scheduler
APScheduler runs in-process with the API — per-channel crons and the PR poll loop. No separate service.
in-process · asyncio
Agent runner
Spawns Claude Code CLI subprocesses for discovery / writing / review / fixes with per-task tool whitelists.
subprocess · stream-json
Adapter workers
Async tasks draining the publish queue; browser adapters run headless Chromium via Playwright on this same box.
asyncio tasks + playwright
SQLite databasedata/relay.db on local disk (WAL mode). Holds pipeline state and telemetry only — the content itself lives in GitHub, so the DB is rebuildable.
local disk · nightly backup to object storage
Workspace + vaultworkspace/ git clones and per-PR worktrees; encrypted credential vault (API keys, Playwright session state) beside it.
local disk · vault encrypted at rest
Network edges — everything else is someone else's server
GitHubHTTPS + gitContent repo (markdown source of truth) and website repo. Issues, PRs, line comments, merges via REST/GraphQL; branch push/pull via git. PR poll every 120s.
AnthropicHTTPSThe Claude Code CLI subprocess calls the Claude API (or reuses the host's logged-in session). No model runs locally.
Website hostingHTTPSVercel / Cloudflare builds the site on merge — Relay never deploys it directly, it just polls the production URL until the post is live (canonical established).
Dev.to · HashnodeHTTPS APIDirect REST / GraphQL publish calls with API keys from the vault. Verify by re-fetching the returned URL.
Medium · LinkedInbrowserHeadless Chromium sessions (Playwright storage_state from the vault) drive the real web editors over HTTPS from the Relay host.
SubstackSMTP + browserDraft created by emailing the publication's secret post address via an SMTP relay (Resend/SES); Playwright then formats and publishes.
Scale path: this single-host shape (the Sentinel model) is right until
multiple channels publish daily. The seams are pre-cut for the next step: swap SQLite → Postgres, move adapter
workers to a separate process consuming the queue (Redis), and keep the API/scheduler node stateless.
publish_jobs: post × platform, states queued → rendering → publishing → verifying →
published / failed, attempt counts, exponential retry.
Credential vaultnew
API keys (Dev.to, Hashnode), Playwright storage-state sessions (Medium, Substack, LinkedIn),
git tokens. Encrypted at rest; Slack alert when a session expires and needs re-login.
Relay works like a small, very disciplined content team that never sleeps. Five roles
hand work to each other in a fixed order. You mostly just read the finished result — and get a message
when something is published.
The Scout finds ideas
Every morning, the Scout searches the internet for topics your audience cares about right now.
It checks what you've already published so it never suggests the same idea twice, then writes a
one-page brief for each good idea: what to write, for whom, and why now.
tech tab: stage 01 · Discovery
The Writer drafts the article
The Writer takes a brief and writes the full article — researched, structured, in your voice.
It writes one master copy in a simple universal format, so the same article can later be
dressed up correctly for every different platform.
tech tab: stage 02 · canonical markdown in the content repo
The Editor checks and pushes back
Nothing publishes on a first draft. The Editor scores the article, fact-checks every claim
against the live web, and tests every link. If anything falls short, it sends the draft back to
the Writer with margin notes — up to five rounds. Only an article that scores at least
7.5 out of 10 with zero open issues moves forward. If several articles were written the same
day, only the best one ships; the rest are politely shelved.
The winning article goes on your own website first, and the Publisher waits until it can
see the page live. Why the order matters: every copy posted elsewhere carries a small signpost
saying "the original lives here." Google then credits your site — not Medium — as the source.
Think of it as selling the original in your own shop and putting posters everywhere else.
tech tab: stage 04 · canonical-first publish
The Town Crier spreads it everywhere
Then the article fans out to every other platform, each copy reformatted the way that platform
likes it. Some platforms have a proper "front door" we can use directly; others don't, so the system
literally opens a browser, logs in, and posts it the way a person would. If one platform is
having a bad day, the others aren't held up — failed posts retry on their own.
Your article
your website · firstMediumSubstackDev.toHashnodeLinkedIn
tech tab: stage 05 · syndication fan-out
The Messenger tells you it's done
Once everything is out, the system double-checks each copy is actually visible on each platform,
then sends you one Slack message per article: every link in one place, and a clear flag if any
platform failed and is being retried.
tech tab: stage 06 · verify & report
Where does everything live?
Three places, and only the first one is yours to look after.
The office — one computer
The whole team runs on a single rented server. Its notebook (a small database) tracks what's in
progress; if the notebook were lost, it could be rebuilt from the archive.
The filing cabinet — GitHub
Every article's master copy lives here, with the full history of every draft and every edit the
Editor ever requested. This is the permanent record.
The stages — other people's websites
Medium, Substack, Dev.to, Hashnode, LinkedIn and your site's hosting are all out on the internet.
Relay just delivers to them; it doesn't run them.
When do humans get involved?
Almost never, by design. Ideas, writing, editing, publishing and announcements are all automatic, end to end.
When a login expires. Platforms that need a real browser login (Medium, Substack, LinkedIn) occasionally sign the robot out. You get a Slack nudge, log in once, and it carries on.
When you want a say. Every draft is reviewed in the open (as a GitHub pull request), so you can read along, comment, or veto before anything ships — you're just never required to.