Herald/Documentation
← All documentation
Reference

Herald — Phased Build Plan

Each phase ends shippable and testable with a concrete acceptance check. Do them in order. Turn each phase into a task-by-task plan with the superpowers:writing-plans skill, then implement TDD. Do not mark a phase done until its acceptance check passes with output you actually saw.

The matching kickoff prompt for each phase is in ../prompts/.


Phase 0 — Repo, infra, CI, health

Build: monorepo skeleton (backend/, frontend/); FastAPI app with /health; Neon connection via DATABASE_URL; first migration (enable pgcrypto, citext; create tenants, users); UV project; Railway web service deploys; Vercel project links frontend/; env var wiring documented.

Acceptance: GET /health returns 200 on the deployed Railway URL; the migration runs clean against Neon; the empty Next.js app renders on Vercel.

Phase 1 — Tenancy + auth (API keys + dashboard login)

Build: tenant + user models; dashboard email/password session login; API-key create/list/revoke (hash stored, full key shown once); the Bearer hld_… auth dependency that resolves a request to a tenant; the tenant-scoping data layer.

Acceptance: unit test — a key for tenant A cannot read tenant B's rows; an API call with a revoked key → 403; login works on the deployed dashboard.

Phase 2 — OAuth connect flow + connected_accounts

Build: connectors/base.py Protocol; oauth_states (signed, single-use); GET /api/oauth/{network}/connect + /callback; encrypted token storage; resolve_identity; account status. Implement the Instagram and LinkedIn OAuth halves (authorize URL, code exchange, identity resolution, IG Business-account detection + rejection of personal accounts).

Acceptance: against each platform's sandbox/test app (or mocked HTTP for unit tests), a full connect round-trip creates an encrypted connected_accounts row with the right platform_account_id/page_id; a personal IG account is rejected with a clear message. Token ciphertext is never plaintext in the DB.

Phase 3 — Media pipeline

Build: object-store client (R2/Blob/S3); POST /v1/media (file + source_url); public-URL return; media_assets. Helper used by the publisher to guarantee a public image URL before an IG publish.

Acceptance: upload an image → the returned URL is publicly GET-able (fetch it with no auth and get the bytes).

Phase 4 — Publishing engine + connectors

Build: PublishPayload/PublishResult; connectors/instagram.py (2-step Graph publish + permalink) and connectors/linkedin.py (image upload + /rest/posts + permalink); token-refresh (ensure_fresh) before publish; per-target result mapping; error taxonomy (reauth_required, rate_limited, invalid_media, …).

Acceptance: unit tests with httpx MockTransport prove the exact request shapes for both networks and the permalink parsing; a real manual publish to one IG Business account and one LinkedIn company Page returns a permalink you open in a browser.

Phase 5 — Public API: posts + targets + status

Build: posts + post_targets; POST /v1/posts (validate, fan out, publish inline when scheduled_at null); GET /v1/posts/{id}, GET /v1/posts, POST /v1/posts/{id}/retry; idempotency via external_ref; per-network validation (caption length, IG needs media).

Acceptance: TestClient — POST /v1/posts to two accounts creates two targets, publishes (mocked connectors), and GET shows partial when one target is forced to fail; idempotent repeat returns the same post id.

Phase 6 — Scheduler + token-refresh worker

Build: the Railway worker process: due-target claim (FOR UPDATE SKIP LOCKED), publish, bounded retry/backoff; proactive token-refresh loop flipping accounts to expired on failure.

Acceptance: a post with scheduled_at a minute out publishes via the worker (observed in logs + GET shows published); two worker instances don't double-publish a target (claim test).

Phase 7 — Dashboard (Vercel)

Build: Next.js dashboard — login; Connect Instagram / Connect LinkedIn buttons (OAuth), connected-account list with status + reconnect/disconnect; a composer (text + media + account multi-select + schedule) that calls the BFF; post history with per-target status + "View post" permalink links; API-keys management screen. i18n keys, light mode, shadcn/ui.

Acceptance: on the deployed dashboard, connect a real account, compose + publish a post, and see the live permalink — all through the UI. Read the screens back at desktop + mobile width; "would I demo this to a paying customer tomorrow?" is yes.

Phase 8 — tars integration (first consumer)

Build: in tars2, a thin HeraldClient (see INTEGRATION-tars.md): on draft approve, POST /v1/posts to Herald with the tenant API key + the brand's connected account_id; capture the returned target permalink into the draft; reuse the existing "View post" UI. Add per-campaign network/account selection + auto-publish switch, mirroring the WordPress card.

Acceptance: approving a social draft in tars publishes it live via Herald and shows the IG/LinkedIn permalink in the cockpit.


Cross-cutting requirements (every phase)

  • Multi-tenant scoping enforced + tested.
  • Tokens encrypted at rest; never logged/returned.
  • /health stays green; migrations forward-only.
  • TDD; a phase isn't done until its acceptance check passes for real.
  • Secrets from env; no hardcoded credentials.

Suggested order rationale

Auth/tenancy (1) underpins everything; OAuth (2) must exist before you can publish; media (3) before IG publish (4); the engine (4) before the API (5); the API before the worker (6) and dashboard (7); tars (8) last, once the API is real.