We recently rebuilt our entire editorial workflow from scratch.
Not a tweak. A complete rebuild.
And the question we kept getting was: "Why didn't you just use Payload? Or Sanity? Or any of the existing CMS products?"
The answer goes deeper than "we wanted more control." It connects to something we've been thinking about since we started Hyprnote: the belief that content should live as files, not database rows.
This is Part 7 of our publishing series, and it's probably the most philosophical one. We'll explain the "why" first, then break down exactly what we built.
The Filesystem Is the Cortex
We wrote about this in The Filesystem Is the Cortex, but the core thesis is simple:
Files are a human interface, not a technical detail.
Humans have organized information spatially for centuries. Drawers. Cabinets. Folders. We remember where something is. Location is recall. Structure is memory.
When notes—or blog posts—live as files, they feel like part of you. When they live as opaque blobs behind an API, they feel like someone else's product.
This isn't nostalgia for "old-school files." It's a belief about the future:
If AI is going to help humans think, it needs access to artifacts humans can own, inspect, move, and understand.
Markdown. Plain text. Files.
The filesystem is the cortex—and in the AI era, it becomes even more important, not less.
Why Not Payload, Sanity, or Other CMS Products?
We evaluated them all. Payload is excellent. Sanity is powerful. But every CMS we looked at had the same fundamental issue:
They want your content to live in their system.
Even the "open source" ones. Even the "self-hosted" ones. Your content becomes structured JSON in a database, mediated through schemas, accessed via APIs.
That's fine for some use cases. But for us, it violated the core principle:
Content should be files. Files in a repo. Diffable. Portable. Ownable forever.
We already wrote about this in Why Our CMS Is GitHub. GitHub gives us everything a CMS promises—versioning, collaboration, review workflows, rollback—without the lock-in.
But there was one gap: editorial workflows for non-engineers.
The Gap We Needed to Fill
Our stack was simple: MDX files in GitHub. Engineers write in IDEs. PRs are drafts, merges are publish.
But we hit friction:
- Accidental publishes — No review gate. Click merge, it's live.
- Lost work — Writers forgot to save. Changes disappeared.
- No visibility — What's pending? What's been reviewed? Nobody knew.
- Context switching — Reviewers had to check GitHub constantly.
- No unified actions — Approve in GitHub, notify in Slack, merge somewhere else.
We needed an editorial workflow. But we didn't want to abandon our file-first principles.
So we built one.
What We Built
The system has three layers:
1. Draft PRs by Default
Every change—new post, edit, unpublish—creates a draft pull request.
Draft PRs are GitHub's best-kept secret. They:
- Don't notify reviewers
- Don't show up in review queues
- Signal "I'm still working on this"
- Can be converted to "ready for review" with one API call
When a writer creates a new post, it lives on a draft PR. They can edit, save, walk away, come back. The PR stays invisible until they're ready.
2. Auto-Save with Visible Countdown
Writers shouldn't lose work. But they also shouldn't be interrupted.
We added a 60-second auto-save countdown:
[ Save (52s) ]
When the countdown reaches zero:
- Content auto-saves to the draft PR branch
- No tab opens. No notification. Silent.
- The countdown resets if more changes are made
Manual save (button or Cmd+S):
- Saves immediately
- Countdown disappears
- Still no tab opens—saving is silent
The PR URL only opens when submitting for review. This keeps writers focused on writing, not on managing PRs.
3. Slack as the Review Interface
Reviewers live in Slack. So Slack became our review interface.
When a writer clicks "Submit for Review":
- Content saves with
published: true - Draft PR converts to ready-for-review
- Reviewer is automatically assigned
- Slack receives a notification with three buttons:
┌─────────────────────────────────────────────────────────────┐
│ Article submitted for review │
│ @john please review │
│ │
│ > "Building an Editorial Workflow with GitHub and Slack" │
│ │
│ [ Preview ] [ View PR ] [ Merge ] │
└─────────────────────────────────────────────────────────────┘
- Preview — Opens the Netlify deploy preview
- View PR — Opens GitHub for detailed review
- Merge — Merges the PR directly from Slack
One click. Article goes live.
For unpublishing, we send a yellow warning message:
⚠️ @jane wants to unpublish "Article Title"
Destructive actions deserve visual distinction.
The Technical Stack
Everything is built on primitives that already exist:
| Component | Implementation |
|---|---|
| Content storage | MDX files in GitHub repo |
| Draft workflow | GitHub draft PRs |
| Auto-save | React state + interval + GitHub API |
| PR conversion | GitHub REST API (PATCH /pulls/{id}) |
| Notifications | GitHub Actions + Slack API |
| Merge from Slack | Slack interactive webhook → GitHub API |
| Preview | Netlify deploy previews (automatic) |
No database. No CMS backend. No proprietary schemas.
Just GitHub, Slack, and a few hundred lines of glue code.
Button States: Small Details Matter
Getting button states right is crucial for clarity:
| Scenario | Save | Submit for Review | Status |
|---|---|---|---|
| No unsaved changes | Disabled | Disabled | Current state |
| Has unsaved changes | Enabled + countdown | Enabled | Current state |
| Currently saving | Spinner | Disabled | Current state |
| Unpublished article | — | — | Gray "Not Published" |
| Published article | — | — | Green "Published" (hover: "Unpublish") |
Writers don't need a "Publish" button. They need to know if something is published. Make state visible; make actions secondary.
Why This Matters for Open Source
Here's where it gets interesting.
We're an open source company. Hyprnote is open source. And we believe this editorial workflow should be open source too.
Not just "source available." Actually usable by other teams.
The vision:
Bring your own repository. Bring your own S3. Get a complete editorial workflow.
Imagine a world where:
- Your content lives in your GitHub repo (not ours)
- Your images live in your S3 bucket (not ours)
- You get draft PRs, auto-save, Slack notifications, one-click merge
- Zero vendor lock-in. Zero data migration. Forever portable.
This is the opposite of what CMS products offer. They want you to put content in their system. We want to give you tools that work with the systems you already have.
We're not there yet. But the architecture is designed for it. The code is written to be extractable. And we're committed to open sourcing it when it's ready.
What We Learned
1. Draft PRs are criminally underused
Most teams create regular PRs immediately. Draft PRs are better for work-in-progress because they don't spam reviewers with notifications.
2. Auto-save needs visibility
Silent auto-save creates anxiety. "Did it save? Should I save manually?" A visible countdown removes uncertainty.
3. Slack is a legitimate review interface
For simple approve/reject workflows, Slack buttons are faster than GitHub's UI. Save GitHub for complex reviews with inline comments.
4. Status indicators matter more than action buttons
Writers don't need a "Publish" button. They need to know if something is published. Make state visible; make actions secondary.
5. Files are the foundation
Every design decision was easier because we started with files. No schema migrations. No content lake. No API rate limits. Just files.
The Philosophy Behind It
We built this because we believe:
-
Content should be owned, not rented. Your words should live in your repo, not in a SaaS database.
-
Tools should compose, not capture. GitHub + Slack + Netlify already exist. We connected them instead of replacing them.
-
Simplicity survives. Markdown files will outlive every CMS platform launched this decade.
-
Open source means open data. If the software is open, the content should be portable.
This editorial workflow is one piece of that vision. It's how we publish our blog, our docs, our changelog. And eventually, it's something we want to share with every team that believes content should be files.
This is Part 7 of our publishing series.
