DevOps Operations Releases Portfolio

Why we ship the marketing site, the docs, and the binary in the same release

A deep dive on the operational pattern we use across the Cryptuon portfolio: one tagged release publishes the marketing site, the docs subpath, the binary, and a /version.json hash, with the CDN cache purged per host and the storage backend pruned on schedule.

DS
Dipankar Sarkar
11 min read 2,075 words

Every project in the Cryptuon portfolio — all twenty of them — has the same release shape. You tag a release, and four things happen in lockstep:

  1. The marketing site at <project>.cryptuon.com rebuilds and republishes.
  2. The docs path at docs.cryptuon.com/<project>/ rebuilds and republishes.
  3. The binary or library publishes to its registry (crates.io for the Rust ones, PyPI for the Python ones, GitHub Releases for everything else).
  4. A small /version.json file uploads to object storage with the release tag and the commit SHA.

Then the CDN cache is purged for the host, the storage backend’s lifecycle policy auto-prunes old releases, and any monitoring system pointing at /version.json sees the version flip. By the time the release pipeline finishes, the version is consistent across the marketing site, the docs, the binary, and the version probe. That consistency is the whole point.

This post explains why we settled on that pattern, what it cost to get there, and what every project in the portfolio gets for free because of it.

TL;DR

  • The four release artefacts — site, docs, binary, version probe — are co-released or they’re inconsistent. Inconsistency at release time is the most common cause of “the docs say one thing, the binary does another” tickets.
  • Path-based docs (docs.cryptuon.com/<project>/) beat subdomain-based docs because it’s one TLS cert, one CDN config, and one consolidated search index across the portfolio.
  • Every release uploads /version.json with the tag, the commit SHA, and the build time. This is the cheapest, most reliable post-deploy validation you can write. It is read by the smoke test, the monitoring dashboard, and the support tooling.
  • The CDN cache is purged per host, not globally, so a release of dfpn.cryptuon.com doesn’t invalidate the cache for tesseract.cryptuon.com. This matters more than it sounds.
  • A scheduled lifecycle policy on the object-storage backend auto-prunes old release artefacts so the storage bill doesn’t grow linearly with release count.

The problem: release-time inconsistency

The thing we kept hitting, before we standardised this pattern, was a class of bug that’s hard to file because it’s structurally about time, not about code. The binary would publish to crates.io at 14:02. The marketing site would rebuild at 14:05. The docs would rebuild at 14:14 because mkdocs-material is slower than Astro. The /version.json probe would update at 14:17 because the release script ran sequentially. For fifteen minutes after a release, a user who installed the new binary, read the marketing site for installation instructions, and then went to the docs would see three different versions of the truth.

That’s not a hypothetical. It happened across the portfolio multiple times before we put proper interlocks in. Specifically: a SolScript release where the marketing site cached an example that used the old syntax. An nklave release where the docs were updated but the CDN cache hadn’t been purged on the docs host. An evmore release where the binary published but the marketing site’s “Latest release” widget showed the previous tag for half an hour.

The conclusion was: release isn’t a single moment in time. It’s a coordinated state transition across at least four systems. And it has to be treated that way.

The pattern: one tag, four artefacts, one cache purge

Every project in the portfolio now has the same release pipeline shape, parameterised by the project name:

  1. Build runs in CI: marketing site (Astro for most projects), docs (MkDocs or Astro depending on the project), binary (cargo publish --dry-run / poetry build).
  2. Tag is created on the git repo with the release version.
  3. Upload runs in parallel for all four artefacts:
    • Site to object storage at s3://<bucket>/<project>/site/.
    • Docs to object storage at s3://<bucket>/docs/<project>/.
    • Binary to its registry.
    • version.json to s3://<bucket>/<project>/site/version.json with { "version": "<tag>", "sha": "<commit>", "built_at": "<ISO8601>" }.
  4. Purge runs once all four uploads complete: CDN cache purged for <project>.cryptuon.com and for docs.cryptuon.com/<project>/*.
  5. Verify fetches https://<project>.cryptuon.com/version.json and asserts the version matches the tag that was just released. If it doesn’t, the release is marked failed in GitHub Releases.
  6. Lifecycle policy runs on a schedule (daily) and auto-prunes anything in s3://<bucket>/<project>/site/ older than the configured retention window.

The whole shape is project-agnostic. You parameterise it by <project> and the same pipeline runs for SolScript, Zig-EVM, nklave, blockchain-compression, commit-reveal, and every other binary-shipping project in the portfolio. The Solana-coordination projects (DFPN, Njord, Mentat, etc.) follow the same shape minus the binary registry publish — their “binary” is the deployed Anchor program, which has its own on-chain release process, but the site, docs, and version probe still co-release.

Why path-based docs beat subdomains

Every project in the portfolio publishes its docs at docs.cryptuon.com/<project>/. Not <project>-docs.cryptuon.com. Not docs-<project>.cryptuon.com. Path-based, always.

The reasons matter:

  • One TLS certificate for docs.cryptuon.com covers all twenty projects’ docs. With subdomain-based docs you’d need a wildcard cert or twenty individual certs, plus the operational discipline to renew them.
  • One CDN configuration. The cache rules, the security headers, the rate limits, the redirect map — one config file covers all twenty projects. With subdomains you’d have twenty CDN configs that almost match.
  • One consolidated search index. The docs.cryptuon.com instance of Algolia indexes the whole tree as one site, so a user searching “anchor” can find both the DFPN and Mentat docs without having to know which project to search first.
  • Cleaner cross-linking. The Solana lessons post in this series links from www.cryptuon.com/blog/... to docs.cryptuon.com/dfpn/ and docs.cryptuon.com/streamsync/ without a CORS or a referrer-policy footgun.

The cost of path-based docs is one: the per-project docs need to be built into the right subpath. MkDocs handles this with a site_url and a small build-script wrapper. Astro handles it with a base config option. Either way, the project’s mkdocs.yml or astro.config.mjs declares its subpath, the CI pipeline publishes to that subpath, and the consolidated docs.cryptuon.com site stitches the twenty subpaths together.

We deliberately chose this over the more fashionable “every project gets its own subdomain” pattern because the portfolio is what we care about, not any single project. The user who installs SolScript today is probably going to look at Zig-EVM tomorrow. Keeping the docs in one place keeps that path frictionless.

What /version.json buys you

The /version.json probe is the most boring, most useful piece of post-deploy validation we have. Every site in the portfolio publishes it at the root, and it looks like this:

{
  "version": "v0.4.2",
  "sha": "a3f9c12...",
  "built_at": "2026-06-05T16:05:00Z"
}

Three fields. That’s it. But it powers:

  • Post-deploy smoke tests: the release pipeline fetches it immediately after the CDN purge and asserts the version matches the tag. If it doesn’t, the release is marked failed and the previous version is restored.
  • Uptime monitoring: the monitoring system polls /version.json every minute. If the response stops parsing as JSON, the site is broken; if the version changes unexpectedly, someone did an out-of-band deploy.
  • Support tooling: when a user files an issue, the support template asks them to paste their <project>.cryptuon.com/version.json. We know immediately whether they’re on the version they think they’re on.
  • Internal dashboards: a single page in the internal dashboard fetches /version.json from all twenty marketing sites and all twenty docs subpaths. At a glance, we see which versions are deployed where.

The reason this works is that it’s part of the build, not an afterthought. The CI pipeline writes version.json at the same step it writes the rest of the static site. It can’t drift from the rest of the deploy because it’s a deploy artefact, not a runtime endpoint.

We have had at least one near-miss caught by this. A docs republish failed silently on the CDN’s side — the upload to object storage succeeded, the purge fired, but a stale edge node kept serving the previous version. The version probe caught it within a minute because the dashboard turned amber for that project. We re-purged, and the version went green. Without /version.json, that bug would have shipped to users and we would have noticed it from a confused support ticket twelve hours later.

Per-host cache purges matter

A subtle thing: when a release ships, the CDN cache is purged only for that project’s host. A release of dfpn.cryptuon.com purges:

  • dfpn.cryptuon.com/*
  • docs.cryptuon.com/dfpn/*

And nothing else. It does not purge tesseract.cryptuon.com or docs.cryptuon.com/tesseract/*, even though they share the same CDN account.

The reason: cache purges have a real cost (CDN quota, edge-node revalidation traffic), and a global purge across twenty projects on every release would be wasteful. More importantly, it would couple the projects in a way that’s operationally bad. A Tesseract release should not invalidate the DFPN cache, even by accident, because if the Tesseract release pipeline has a bug, the DFPN cache state should be untouched.

This sounds obvious in writing. It is not always obvious in practice, because many CDN client libraries default to “purge by tag” or “purge by host pattern” in a way that’s easy to misuse. The pattern we settled on is: the release pipeline accepts an explicit --host flag, the host is computed from the project name, and the purge is scoped to exactly that host. No globbing, no shared tags across projects.

Lifecycle policies prevent linear bill growth

Every release of every project leaves an artefact in object storage. Twenty projects, releasing roughly monthly, leaves 240 release artefacts per year. After three years, that’s 720. The marketing-site artefacts are small (~5MB each, Astro is tiny), but the docs artefacts aren’t (MkDocs material themes are 30-80MB compiled). At 80MB × 720 docs releases, that’s ~58GB of object storage growth per year for the docs alone, plus the same again for site artefacts, plus binary release tarballs.

The fix is unglamorous: a lifecycle policy on the object-storage bucket auto-deletes artefacts older than 90 days (or whatever the retention is for that project — for production-leaning projects like nklave and Tesseract, we keep longer; for active-development projects we keep shorter). The policy is set once when the project is bootstrapped and never touched again.

The reason this matters is that the absence of this discipline is the most common failure mode for a portfolio of projects. Each project shipping each release independently is fine. Twenty projects shipping releases independently with no cleanup is a quietly-growing operational debt that nobody owns until the storage bill triples.

What this pattern doesn’t do

It deliberately doesn’t:

  • Auto-deploy on every push to main. Deploys happen on tagged releases. Pushes to main update the staging environment.
  • Couple cross-project releases. A Tesseract release does not trigger a DFPN release, even if Tesseract’s TESS staking docs reference DFPN.
  • Provide rollback by re-deploying an old artefact. Rollback re-tags the previous version and re-runs the release pipeline. The artefact store is append-only; rollback is a forward operation.

The first two are by design. The third is a deliberate choice — re-running the pipeline takes ~3 minutes and produces a verifiable artefact with its own /version.json and its own audit trail. Re-deploying an old object would skip those guarantees.

The takeaway

A portfolio of twenty open-source projects has a multiplier effect on operational discipline. Anything that’s slightly sloppy at the single-project level becomes badly sloppy at the portfolio level. The four-artefact co-release with /version.json post-deploy validation, path-based docs at docs.cryptuon.com/<project>/, per-host cache purges, and scheduled lifecycle pruning is the minimum viable shape of “deploy means ship and observe.”

The shape is project-agnostic. It is parameterised by the project name and almost nothing else. That’s why every project in the portfolio uses it, and why every new project we start gets the same pipeline on day one.

Three concrete examples from the portfolio:

The pattern is not novel. It is just disciplined. Across twenty projects, that discipline compounds.

DS

Dipankar Sarkar

Founder, Cryptuon

Blockchain researcher and systems engineer. Author of 5 published papers on cross-chain composability, MEV mitigation, and DePIN protocols. Building production blockchain infrastructure in Rust and Zig.

Build on research-grade infrastructure

Ready to deploy smart contracts across chains, execute atomic cross-rollup swaps, or protect your validators from slashing?