Why I Deleted a Publish Pipeline Before Launch
Problem
Content publishing creates an attractive infrastructure problem. A webhook arrives, several edits can arrive together, and a deployment must use trusted Content. I designed a dedicated Publish Gate to solve all three concerns before the site used the system in production.
The Publish Gate was a separate Cloudflare Worker. It accepted Sanity webhooks, grouped nearby events, created a Content Snapshot, stored that Snapshot in R2, and started GitHub Actions. R2 is Cloudflare object storage. The design also offered an authenticated refresh route for code-triggered builds.
Each part answered a real question. The problem was the combined operating cost. Before activation, repository evidence showed that Content Publishes were rare, at only a few each week. The system optimized a burst pattern that the portfolio did not have.
Decision
I removed the Worker before production activation. Sanity sends a trigger directly to GitHub Actions. Each Content Publish starts one Site Redeploy, and that job creates its own validated Content Snapshot. A weekly Site Redeploy recovers missed webhook events.
The portfolio-publish-gate package remains, but its role changed. It is the home of Snapshot Query, the GROQ projector that reads published Sanity data. It is not a webhook receiver, an R2 publisher, or a coalescer. Keeping the package avoided a rename and code move that did not improve the active architecture.
This decision retired infrastructure, not validation. Production builds still require a Snapshot envelope, and Snapshot Query still validates the Content Contract.
Implementation
The original design had two routes. Sanity called /webhook. GitHub Actions called /refresh to request a current Snapshot. The webhook route was public because Sanity needed to reach it. The refresh route sat behind Cloudflare Access, which uses service credentials to control access to an application.
The webhook used Sanity HMAC authentication. HMAC is a keyed hash that proves that a sender knows a shared secret. The Worker needed the raw request body for signature verification before JSON parsing. It also enforced a five-minute timestamp limit because the Sanity webhook toolkit did not enforce that limit.
After authentication, a Durable Object tracked a Coalesce Window. A Coalesce Window is a quiet period that groups nearby Content Publishes. Each event reset a five-minute alarm. When the alarm fired, the Worker queried Sanity and validated the result. It wrote the Snapshot to R2 and dispatched GitHub Actions with a reference to that object.
The design separated credentials. Sanity had the HMAC secret. GitHub had Cloudflare Access credentials for /refresh. Signed URLs let GitHub download an R2 object without general R2 read credentials. The webhook body remained a wake-up signal rather than a trusted Content payload.
That implementation reached the repository in the portfolio-publish-gate workspace package. It included Worker routes, authentication, Durable Object logic, R2 code, dispatch code, provisioning scripts, Wrangler configuration, and tests. Commit f6b9196 later removed the Worker surface. A structural test makes sure that the package contains no Worker, R2, HMAC, Durable Object, or Wrangler runtime code.
The replacement path uses fewer owners. A Sanity webhook sends repository_dispatch with a GitHub token in its HTTP headers. The trigger projection contains only the event type and optional trigger details. GitHub generates the Snapshot inside the redeploy job and builds from that local file.
The deployment workflow handles overlap instead of grouping events before CI. Its production job uses cancel-in-progress: true. A newer start cancels an older run, so the latest publish request controls the remaining deployment.
Rejected Options
I rejected finishing the Publish Gate activation. It had stronger webhook-specific authentication and better burst grouping. Those features did not justify Worker deployment, R2 lifecycle management, Access policy, several secrets, and a second production service at the measured publish rate.
I rejected moving Snapshot Query into the Astro site after deleting the Worker. The package name reflects history, but the export already serves the site and local development. A package shuffle changes ownership without reducing runtime infrastructure.
I also rejected live GROQ inside Astro prerender. Removing the Gate did not remove the need for a stable build input. GitHub Actions still creates the Snapshot before Astro starts.
Direct deployment hooks were not a full replacement because the project keeps the build and Cloudflare deployment in GitHub Actions. Scheduled Dataset polling was also unnecessary. A weekly run through the normal redeploy path provides recovery without a new polling service.
Accepted Tradeoffs
The smaller system lost burst coalescing. Several publishes can start several Actions runs. Production cancellation removes outdated in-flight work, but GitHub still starts the runs.
It also lost R2 Snapshot history. The current design creates a file inside each job and does not keep a central object for later download. Debugging relies on workflow logs and reproducible queries instead of a stored series of Snapshot objects.
The direct webhook path also lost the Gate acknowledgment and its HMAC boundary. Authenticity comes from the GitHub dispatch token in Sanity webhook headers. If dispatch fails beyond Sanity retry behavior, the weekly redeploy is the catch-up path.
These are real losses. The accepted gain is a smaller operational surface. No Gate Worker needs deployment. No Durable Object alarm needs observation. No R2 bucket or lifecycle rule needs maintenance. No Access application protects /refresh.
Lesson
A complete implementation is not proof that infrastructure belongs in production. The Publish Gate solved plausible future pressure, but the active workload did not supply that pressure. Deleting it before activation cost less than operating it until a later cleanup.
The exercise also separated enduring logic from delivery machinery. Snapshot Query and Contract validation remained useful. Webhook routes, coalescing, storage, and authentication belonged only to the retired delivery design.
Next-Project Rule
For my next project, I will match infrastructure to observed event volume and recovery needs. I will keep a direct path until measurements show a burst or reliability problem. If that problem appears, I will add the smallest stateful layer that addresses it. I will also design components so durable domain logic can survive the removal of transport infrastructure.