Pulpit Engine

Fully automated sermon-to-social pipeline for churches. Sermon recording in, scheduled vertical video clips out — zero manual steps, no agency staff, no human in the loop between recording and published Reel.

6 Clips per service run
15 Ordered pipeline steps
278 Passing tests
4 Railway services deployed

What I Built

Most churches produce a 45-60 minute sermon every week and do nothing with it afterward. The pastor doesn't have a social media team, the AV volunteer is already stretched thin, and clipping video for Instagram and Facebook requires skills and time the church doesn't have. Pulpit Engine solves that end to end.

I built a 15-step TypeScript runner that takes a raw sermon recording and outputs 6 production-ready vertical video clips scheduled directly to the church's Facebook Page — with no human in the loop except the pastor's one-time approval in the review app. The pipeline handles source intake from multiple streaming platforms (BoxCast, Manual Source, Cloudflare RTMP), downloads and trims raw video on Railway, transcribes with AssemblyAI, runs Claude Sonnet to detect sermon boundaries and score clip candidates by virality potential, generates animated-caption vertical clips via Submagic Magic Clips, writes captions with Claude Haiku, stores clips in a dated Google Drive folder, and schedules posts to Facebook Reels via the Pages API — then sends the pastor a SendGrid briefing email with a summary of what's going out and when.

The runner is a checkpoint-resume system: every step persists its output to Supabase before moving to the next, so a mid-run failure restarts from the last completed step without re-spending on vendor APIs. The bounded manifest enforces which external calls each step is allowed to make at runtime — a step that shouldn't touch Facebook cannot touch Facebook, even if someone adds code to try. The QC flow gates scheduling behind a review session: the pastor sees 6 clip slots in a mobile-optimized review app, can approve or reject each one, and scheduling only runs after explicit approval.

The system is architected for multi-church scale: all church-specific config lives in Supabase (Facebook page ID, Drive folder IDs, pastor email, voice profile, service windows), and the runner is church-agnostic. Adding a new church means inserting a config row and pointing the source adapter at their streaming platform — no code changes required.

Pastor Review Interface

The mobile-optimized QC app deployed to Railway staging. Pastors review 6 clip slots and 3 backups before anything gets scheduled. Approve All triggers the schedule-claim and lock-to-schedule steps; nothing posts until this gate clears.

Pastor Review App — Mobile View (375px)
Review App — Mobile View
375px viewport smoke test of the pastor-facing QC interface deployed to Railway staging. Shows the clip grid, slot numbering, and approval controls.
Pastor Review App — CDP Screenshot
Review App — Full Page Capture
Full-page CDP screenshot of the review session UI. Covers the complete review flow: clip previews, caption display, approve/reject per slot, and the Approve All confirmation gate.

15-Step Runner Architecture

The TypeScript runner that owns the full pipeline. Each step is independently versioned, has a typed input/output contract, and persists state to Supabase before proceeding. The bounded manifest enforces which external systems each step can touch at runtime.

// 03_BUILD/pulpit-runner — 15 ordered steps, 278 passing tests

 1  source-intake       // normalize recording from streaming platform adapter
 2  queue-claim         // atomic claim from worker_jobs; idempotent lease
 3  railway-download    // download raw sermon video from Cloudflare Stream
 4  railway-trim        // FFmpeg service on Railway: trim to sermon window
 5  assemblyai-transcribe // transcribe sermon; swappable provider seam
 6  sonnet-boundaries  // Claude Sonnet: detect sermon start/end, score clips
 7  submagic-upload    // upload trimmed video to Submagic Magic Clips
 8  submagic-poll       // checkpoint/resume poll until clips are ready
 9  ingest-candidates  // persist 9 candidate clips to Supabase (6 primary + 3 backup)
10  host-previews       // generate signed preview URLs for review app
11  haiku-captions     // Claude Haiku: write captions for each clip
12  open-review        // create review session; halt until pastor approves
13  schedule-claim     // claim posting slots from weekly schedule window
14  lock-to-schedule   // lock approved clips to slots; DB-unique wall prevents doubles
15  briefing            // SendGrid email to pastor; Slack ops alert
Runner Step Manifest
Complete ordered step list from 03_BUILD/pulpit-runner/src/. Each step has a typed contract enforced at compile time and a bounded allowance list enforced at runtime. Steps 12–15 only run after the review session reaches status = locked. The step list is the same for every church; only the config row changes.

Supabase Schema — System of Record

Supabase is the only system of record. All pipeline state, QC decisions, schedule claims, and metrics live here. n8n was retired; the runner owns all business logic and writes directly to these tables.

-- Durable queue tables
worker_jobs          -- job intake from Cloudflare webhook; atomic claim lease
processing_queue     -- per-sermon processing state; checkpoint/resume anchor

-- Execution state
runner_runs          -- one row per sermon; links to all steps
runner_steps         -- one row per step execution; persists typed output JSON
runner_side_effect_attempts  -- idempotency log for every external call
source_runs          -- normalized recording intake from source adapter

-- Content tables
candidate_clips      -- 9 clip candidates per run (6 primary, 3 backup)
review_sessions      -- QC session per run; status: open → locked
review_slots         -- 9 slots per session; approved/rejected per slot
sermon_transcripts   -- lean provenance row; full transcript in Google Drive

-- Scheduling tables
scheduled_posts      -- Facebook Reels schedule; unique (slot, platform) wall
cloudflare_webhook_events  -- signed webhook intake log; duplicate suppression

-- Church config
churches             -- per-church config: FB page, Drive folders, voice profile
operational_events   -- structured error log; pipeline halt events
Database Schema Overview
Canonical migration chain: 0001_runner_execution_state.sql, 0002_processing_queue_foundation.sql, 0002_runner_side_effect_idempotency.sql, 0003_production_webhook_worker.sql. All schema applied via ordered single-transaction migrations — no loose repair SQL. The lock_review_session_v1 RPC enforces the review gate at the database level.

Source Adapter Pattern

The intake layer is designed around a normalized source recording object so the rest of the pipeline is platform-agnostic. BoxCast and Manual Source are built; Subsplash, Resi, YouTube Live, and Facebook Live are planned adapters. Cloudflare Stream RTMP is the default recording intake path (PERC).

// Normalized source recording object — output shape every adapter must produce
{
  "church_slug":          "first-baptist-sandbox",
  "source_platform":      "boxcast",
  "source_external_id":   "boxcast_broadcast_id",
  "source_recording_id":  "boxcast_recording_id",
  "source_title":         "Sunday Morning Service",
  "source_video_url":     "https://...",
  "source_url_type":      "direct_mp4",
  "source_started_at":    "2026-05-18T10:00:00Z",
  "source_ended_at":      "2026-05-18T11:30:00Z",
  "church_timezone":      "America/Chicago",
  "sermon_type":          "Sunday",
  "sermon_date":          "2026-05-18"
}

// Adapters built:   Manual Source (permanent fallback), BoxCast
// Trigger path:     Cloudflare Stream RTMP webhook → pulpit-runner-webhook → worker_jobs
// Future adapters:  Subsplash, Resi, YouTube Live, Facebook Live, ChurchStreaming.tv
Source Adapter Interface
Every streaming platform adapter outputs the same normalized object. The processing pipeline only needs church_slug, source_video_url, sermon_type, sermon_date, and church_timezone to run. All per-platform fields are for idempotency, logging, and future reporting. Adding a new church means inserting a churches config row — no code changes to the runner.

Technologies

Recording Intake
Cloudflare Stream (PERC) RTMP / RTMPS BoxCast
Infrastructure
Railway Supabase TypeScript Vercel
AI
Claude Sonnet Claude Haiku AssemblyAI
Video
Submagic Magic Clips FFmpeg Google Drive
Distribution
Facebook Pages API SendGrid Slack

Build Progress

Phase 5 is complete. The no-spend worker proof passed on Railway Staging under commit d5325b7. Phase 6 is the first normal-mode sandbox run gated on a separate approval.

Complete
Phase 1–3: Runner + Review App
15-step TypeScript runner, 278 passing tests, review app deployed to Railway staging, Supabase schema applied and verified, sandbox church (First Baptist) configured.
Complete
Phase 4: Webhook Intake Proof
Signed Cloudflare webhook received by staging webhook service, idempotency verified (duplicate delivery produced zero extra rows), one worker_jobs row per unique event.
Complete
Phase 5: Worker No-Spend Proof
Railway Staging worker deployed (commit d5325b7), booted in no-spend mode, claimed a fixture job, completed with result_json.status = no_spend_proof_halt. No vendor spend occurred.
Next Gate
Phase 6: Normal-Mode Sandbox Run
Full end-to-end run against First Baptist Sandbox. Sunnylane PERC recording source in; 6 clips to sandbox Facebook page out. Stops at review gate for pastor approval before any posts are scheduled.
Back to jakegandara.com ↗ GitHub Repo ✉ jake@jakegandara.com