Insights

Archiving Threads Drafts vs. Published Posts with a Hybrid Supabase and File Pipeline

8 min read#threads#supabase#launchd#git#automation#writing-process

Who this is forSolo creators and developers who publish on Threads and want to track how their drafts change before posting, using a Mac mini, Supabase, and launchd.

Every time I post to Threads, the published version differs from the draft I wrote. The tone shifts, the text gets shorter, tables become bullet lists, and numbers get rounded. These edits are the clearest signal of my writing style, but nothing recorded them, so I had no reference to give an AI when asking it to write in my voice. This article describes the pipeline I built on April 23, 2026 to fix that. Supabase stores time-series metrics, the file system archives the post text, and a Mac mini running 24/7 ties the two together. You will get the architecture, the component list, the diff fields it records, and the operating rules that came from real failures.

The problem: drafts and published posts diverge

Each time a post goes to Threads, the final text differs from the draft in predictable ways:

  • Tone: casual speech (banmal) changes to polite speech (jondaetmal).
  • Length compression: 947 characters became 331 characters, a -65% change.
  • Table flattening: | Header | Value | tables become bullet points.
  • Number rounding: 272 became 300, and 155,587 became 150,000.
  • Emoji changes: 4 emoji became 1 emoji.

These edit patterns are the core signal of my writing style, yet none of them were stored anywhere. I also had no reference to include when asking an AI to write a draft in my style.

Storage options compared

I compared three ways to store the drafts alongside the published posts.

Option Storage location User effort Analysis convenience Time-series metrics Verdict
A. DB only Add draft_text and edit_meta columns to Supabase threads_posts Add post_id: to frontmatter SQL queries Handled together Clean centralization, files not greppable
B. Files only Save draft, published text, and diff together in published/threads/*.md Same, plus a sync command git, grep, diff Managed separately High analysis freedom, separate from the DB
C. Hybrid DB = time-series metrics, files = manuscript archive Same Strengths of both Mostly DB Role split keeps schema changes minimal

I chose option C. Supabase was already running 4-hour insights snapshots, and changing the DB schema for draft-published analysis would add overhead. Files, meanwhile, come with git version control, grep, and diff tools at no extra cost.

Final architecture

The data flows through the following stages:

[MacBook] write drafts/*.md
   ↓ (publish)
[Threads] post goes live → Threads API aggregation
   ↓ (snapshot_cron.py, 4h interval, Mac mini launchd)
[Supabase] threads_posts + threads_snapshots (time-series metrics, single source of truth)
   ↓ (archive_published.py, runs back-to-back in the same cron)
[Files] published/threads/{date}-{pid_short}-{slug}.md generated automatically
   ↓ (user: add post_id to the draft frontmatter, run link_draft.py)
[Files] draft and diff sections appended to the same published md; draft moved to _archive/
   ↓ (Mac mini: git commit is automatic, push is manual — review gate)
[GitHub origin main] analysis and backup

The key design decision is that the snapshot job runs on a schedule, while linking a draft to its published post is a manual step that happens after posting. The archive script runs inside the same cron cycle, so published posts appear as files without any action from me.

Components

Five files make up the pipeline:

File Role Trigger
scripts/snapshot_cron.py Collects Threads API insights and upserts them to Supabase; auto-refreshes the token when it expires within 7 days Every 4h via launchd
scripts/archive_published.py Queries the last 7 days from threads_posts and creates only the missing files in published/threads/ (idempotent) Every 4h (wrapper)
scripts/link_draft.py Appends a diff section to the published file for the draft’s post_id and moves the draft to _archive/ Manual (after posting)
deploy/run_threads_cron.sh snapshot → archive → git add + git commit (no push) Called by launchd
deploy/xyz.ggplab.threads-snapshot.plist 4h interval, RunAtLoad, log path set System boot + every 4h

The shell script deliberately stops at commit. Pushing is a separate decision, covered in the operating rules below.

Diff fields recorded automatically

link_draft.py appends these quantitative fields to the published markdown file:

  • char_delta (signed) and len(draft) → len(published)
  • line_delta (signed)
  • Numbers added and removed, extracted with the regex \d[\d,.]*%? and compared as set differences
  • Emoji count delta, plus the list of emoji in the published version
  • Unified diff (first 200 lines)

Because every edit is recorded in the same format, the files can be compared across posts with ordinary command-line tools.

Operating rules

These rules came from problems that actually happened in production.

Principle Reason (actual incident)
Never put the main push in cron (commit only) An auto-push straight to main would pollute the production branch without review. Commits pile up locally on the Mac mini, and a person checks git log origin/main..HEAD before pushing.
Stash untracked files first when pulling on the Mac mini Scripts copied over by hand with scp in an earlier session remained untracked and blocked a pull of a file with the same name. Preserve them with git stash -u before pulling.
Set an explicit User-Agent for Discord and Threads requests Python-urllib’s default User-Agent gets a 403 (1010) block from Cloudflare. Every HTTP call sets User-Agent: ggplab-threads-analytics/1.0.
Add a fallback for timestamp parsing Threads returns timestamps like 2026-04-23T04:44:05+0000 (no colon in the offset), and Python 3.9’s fromisoformat fails silently. A strptime fallback plus a parse_errors counter detects the problem.
Use a Z suffix in PostgREST URLs +00:00 decodes to a space in the URL. Force the format with .isoformat().replace('+00:00', 'Z').

Each rule addresses a specific failure. None of them were added as precautions.

Insights: design lessons

1. Metric definitions determine most of the pipeline design

This morning I had Claude tag my Threads posts. The top result was a post recommending 16GB of RAM for MacBooks, with 272 likes and 155,587 views. But its ER (engagement rate) was 0.23%, near the bottom of the list. Same data, different metric, completely different conclusion.

I built that lesson into the pipeline:

  • The DB stores raw counters (views, likes, replies, reposts).
  • The enriched view calculates ER (threads_snapshots_enriched.engagement_rate).
  • Analysis scripts always apply filter(views >= 500) + order by ER desc.

An analysis that does not choose its metrics in advance converges on “the most visible thing is the most important.”

2. Separating the SSOT protects the Supabase schema

When the need for draft-published analysis appeared, I could have changed the DB schema. But:

  • The DB is already written reliably by snapshot_cron.
  • Adding columns means a chain of migration, RLS redesign, and edge function updates.
  • The file system is a free single source of truth, with git, grep, diff, and Python parsers all available at no cost.

The rule I took from this: if a new feature would break the schema of an existing source of truth, consider a different store. The DB holds only time series, and the files hold text and edit history.

3. Resisting auto-push keeps the system running longer

Letting the Mac mini run git push origin main automatically would be convenient. But a contaminated main branch costs far more to recover than the automation saves. The note estimates that 15% of auto-push pipelines eventually let a “strange commit” slip in and break production, a figure based on personal experience and industry common sense.

Instead, the pipeline accumulates local commits on the Mac mini, a person reviews them, and then a manual push happens. A daily check with git log origin/main..HEAD before pushing adds about 30 seconds, yet it preserves security, readability, and accountability.

4. Why the snapshot interval is 4 hours

Threads API insights have a 10–30 minute update delay. Hourly snapshots would barely change the numbers while quadrupling the rows in Supabase. Four hours is the shortest interval that still captures most peak engagement within 72 hours.

Because a T+0 snapshot also runs automatically right after posting, the workflow “post just went up, check ER three hours later” is covered.

5. File names determine how easy analysis is

The file naming pattern is:

{YYYY-MM-DD}-{post_id_short}-{slug}.md

  • The date prefix sorts files chronologically, which makes reviewing the timeline natural.
  • post_id_short (the last 6 characters of the post ID) prevents duplicates and serves as the join key to the database.
  • The slug uses the first 30 characters of the first line, keeping letters and digits, so a plain ls listing shows what each post is about.

Draft social posts

The note includes two drafts for announcing the work.

LinkedIn version

I write a draft before posting to Threads every day, and I often edit it heavily when I publish. The tone changes, the length drops by 65%, tables become bullets, and numbers get rounded.

That editing pattern is effectively my writing style DNA, and nothing stored it.

So yesterday I built one pipeline on a 24/7 Mac mini server:

  1. Supabase: collects views, likes, and reply counts from the Threads API every four hours (time series).
  2. File system: archives the published post text automatically as published/threads/*.md.
  3. Diff recording: enter only the post_id in the draft file, and the character delta, metric changes, emoji counts, and a unified diff get appended to the published file automatically.

The key is separating roles: the DB holds metrics, and the files hold text. I did not build the new feature by altering the existing schema of the source of truth. Each tool does what it does best.

I deliberately left out automatic git push. The Mac mini only accumulates commits. I review git log origin/main..HEAD once a day and push manually. It is a balance between automation and a review gate.

After a few months, the data will show how I change drafts, and that becomes a real reference when I ask an AI to write in my style.

Threads version

My draft and my published post are different.

The tone changes, the length drops 65%, numbers get rounded, and emoji go from 4 to 1.

But nothing stored this editing pattern anywhere.

So I built a pipeline.

Supabase records only metrics every 4h. The file system keeps the text, draft, and diff as md files. The Mac mini runs it on its own. Automatic push is deliberately off.

I did not change the DB schema. Files get git, grep, and diff for free.

Separating roles is the cleanest approach.

Sources

  • Live build session, sns-content-hub repository, April 23, 2026
  • belle_epoque7 Threads analysis of 160 posts (the ER vs. raw likes reversal case)
  • Mac mini home server setup (reference: 2026-04-20-macbook-macmini-ssh-homeserver.md)
  • Supabase RLS (row-level security) policy and PostgREST URL encoding debugging
  • sns-content-hub commits: ee49193 (initial pipeline), 3fba232 (first automatic archive)

Bottom line

The evidence supports one conclusion: separating metrics from text keeps the database schema stable while preserving every edit. Supabase stores the time series, and the files store the draft, the published text, and the diff, which git, grep, and diff handle at no cost. Automation covers collection, archiving, and commits, while a person makes the final push after reviewing the log. Whether the archived edits become a useful style reference for AI drafting depends on months of accumulated data, and that outcome has not been demonstrated yet.

Frequently asked questions

Why keep metrics in Supabase and drafts in files instead of storing everything in one place?
Supabase already held the 4-hour metrics snapshots, and adding draft columns would mean migrations, RLS redesign, and edge function changes. Files get git, grep, and diff for free, so the split keeps schema changes minimal.
Why doesn't the pipeline push to main automatically?
Auto-push would put unreviewed commits straight onto main, and recovering from a bad commit costs more than the automation saves. The Mac mini only commits, and the author reviews git log origin/main..HEAD once a day before pushing manually.

Want the full system? The Claude Code & Codex Skills guidebook collects the skills and subagents behind this blog, from $19.