In beta

Halyard

Posts about your product to social media on a schedule, without you watching it, and is built so that it can never accidentally post the same thing twice.

Social media infrastructure built around one rule: never post the same thing twice.

Platform
Web
Status
In beta
Demonstrates
Production engineering

02

What it is for

For a builder who has a product and no marketing function.

Every tool in this category starts from a brief you write about your business, and every prompt downstream inherits whatever you typed. Halyard starts from the product. It reads the website, the store listing, the code, the interface, and an MCP server if one exists, and stores facts that cite the evidence behind them. Copy is then written from real product output rather than from a description of it.

03

Why it exists

Publishing to social platforms on a schedule, unattended, is easy to do badly and dangerous to do wrong.

A retry that double-posts to a real account is not a bug you apologise for privately: the post is live and public before anyone notices. Every scheduling tool has this problem and most of them solve it with a comment saying not to retry. The people with the problem are anyone shipping content from a queue they are not watching.

04

What it does

  • Job queue

    One table, claimed by a Postgres function with FOR UPDATE SKIP LOCKED, polled every two seconds.

  • Per-kind job policy

    Fourteen job kinds, each with its own timeout, attempt count and backoff, and a reason for each.

  • Duplicate protection

    Four independent layers, the load-bearing one a unique index the claim row hits before any network call.

  • Kill switch

    A single global flag checked before anything else on every publish attempt.

  • Failure policy

    Five failure kinds, each with a decided answer — because an undecided one becomes an outage at three in the morning.

  • Quality gates

    Copy, claims, visual, audio, destination, proof and coherence. Failures never reach the approval queue.

  • Reschedule policy

    A pure function deciding publish, reschedule, wait or expire — never publishing four-day-old approval as fresh.

  • Stale lock reaper

    Requeues anything running whose lock is older than thirty minutes, or marks it dead.

05

What it runs on

Client

01
Next.js 15
02
React 19
03
Tailwind v4
04
Supabase SSR auth

Backend

01
Node worker
02
Supabase Postgres
03
40 SQL migrations
04
pnpm + Turborepo

Services

01
OpenAI
02
Anthropic
03
Whisper
04
ElevenLabs
05
Sentry
06
Railway

06

How it's built

A pnpm monorepo: a Next.js dashboard, a long-running Node worker, and about 42,000 lines of shared domain logic. Supabase Postgres underneath, with forty hand-written SQL migrations.

The queue is one table. The claim is a database function using FOR UPDATE SKIP LOCKED, so two workers are safe because the correctness lives in Postgres rather than in the polling loop — which keeps only the responsibilities it can get right: per-kind timeouts, backoff, reaping stale locks, and a heartbeat. Every job kind has its own timeout, attempt count and backoff, and each unusual number carries its reasoning: a capture gets twenty minutes because it drives a real browser through a real product flow twice, once to verify and once to record.

The worker runs on Railway with an always-restart policy. Sentry is wired for errors, with expected states — a paused kill switch, a duplicate abort — deliberately excluded from alerting, because those are the system working.

07

Architecture

One figure, read left to right. Border treatment carries the node type; the color is the project's.

signalsone jobby kindreview_mediafailures never arriveapprovedclaim before callconnectors/GitHub releases, product APIsjobspriority, run_after, attemptsclaim_next_job()FOR UPDATE SKIP LOCKEDpoller.tsTimeouts, backoff, reapergenerate, tts, renderContent and mediaqc/ gatesCopy, claims, coherence, audioApproval queueSurvivors onlypublish.tsKill switch checked firstpublicationsunique (item, account)
Fig. 1worker and scheduled-job architecture
  • Service
  • Store
  • External
  • Model

08

A piece of the code

supabase/migrations/0009_functions.sql

create or replace function public.claim_next_job(p_worker_id text, p_kinds text[] default null)returns setof jobslanguage sqlvolatileas $$  update jobs     set status    = 'running',         locked_at = now(),         locked_by = p_worker_id,         attempts  = attempts + 1   where id = (     select id       from jobs      where status = 'queued'        and run_after <= now()        and (p_kinds is null or kind = any(p_kinds))      order by priority, created_at        for update skip locked      limit 1   )  returning *;$$;
Twelve lines that make a multi-worker queue safe. The atomicity is put where the transaction already is, so the polling loop above it never has to be correct about concurrency — it never gets the chance to be wrong. SKIP LOCKED means a second worker steps over the locked row rather than blocking on it.

Excerpt — trimmed for reading, not a full file.

09

The hard parts

01Problem

Publishing is a network call with retries, run by a worker that can be killed mid-flight, possibly alongside a second worker. Every one of those is a chance to post the same thing twice to a real public account.

Approach

Four independent mechanisms, only one of which is application logic. A pre-flight check for an existing publication. A claim row inserted before the network call, guarded by a unique index on the item and account pair, so two racing workers hit the index and exactly one wins. A second unique index on platform and post id. And a malformed response classified as success-with-unknown-id, marked for reconciliation rather than retried.

Tradeoff

A crash between the claim and the call leaves a claim row for a post that never happened, and someone has to confirm it manually. The system chooses occasional manual reconciliation over occasional double-posting.

02Problem

The failure policy already returned "do not retry" for authentication failures, malformed responses and duplicates. The publish handler already acted on that for the item and the account. Then it threw an ordinary error, and the poller — which had no way to hear the decision — retried anyway.

Approach

A permanent-failure error class carrying a human-readable reason. The decision stays where it already lived; this is only the channel it was missing. Deliberately an error subclass rather than a return value, because a handler signals permanence by how it fails, which is how it signals everything else, and every existing handler keeps working unchanged.

Tradeoff

Control flow through exception types is easy to misuse, so the rule is written next to it: a transient failure must still throw an ordinary error and take its retries. This is not a shortcut for a flaky provider.

03Problem

A scheduled slot arrives and the item is not ready. There are at least six reasons why, each needing a different answer, and left undecided each becomes an incident in the middle of the night.

Approach

Two pure functions with no I/O. One maps five publish-failure kinds to a full policy — retry or not, backoff, whether to pause the account queue, whether to notify, whether to mark for reconciliation. The other decides publish now, reschedule, wait, or expire, with a cap of three reschedules and a twenty-minute grace window for a render still running.

Tradeoff

Separating policy from execution means two places to look, and the seam between them is exactly where the retry bug lived.

04Problem

Every text-level quality check passes on a video whose voiceover describes a feature the footage never shows. Text cannot see the artifact.

Approach

Gates that run against the rendered file in their own job: coherence, which asks whether the artifact shows what the post claims; a claim verifier that checks each factual claim against the artifact; and audio checks measuring word error rate, pace and loudness from a transcription. Failures never reach the approval queue; warnings are shown but do not block.

Tradeoff

That job samples frames and makes a describer call per frame. It is the slowest and most expensive step in the pipeline, and it is worth waiting for because it is the only thing that looks at the finished media.

Ask about Halyard

Answers are drawn from the notes and source behind this case study, and cite what they read.