# How Instatic's Publish Versioning and Cache Invalidation Work: A Deep Dive into the Three-Layer Pipeline

> Understand Instatic's publish versioning and cache invalidation within its three-layer pipeline. Learn how it keeps artifacts, caches, and helpers synchronized. Explore the CoreBunch/Instatic repository.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: deep-dive
- Published: 2026-07-03

---

**Instatic uses a monotonic publish version counter combined with an in-process publish lock and version-keyed single-flight memo caches to ensure static artefacts, in-memory render caches, and snapshot helpers stay synchronized across every publish operation.**

Instatic, an open-source static site generator, implements a deterministic three-layer publishing pipeline that guarantees content consistency across full-site and per-row publishes. Understanding how Instatic's publish versioning and cache invalidation work is essential for developers extending the platform or debugging cache-related issues. The architecture relies on a global version counter stamped onto every artefact and a serialization mechanism that prevents race conditions during the read-transaction-bake cycle.

## The Monotonic Publish Version Counter

The foundation of Instatic's cache coherence lies in a global integer that tracks every successful publish operation. In [`server/publish/publishState.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishState.ts), the module maintains a `publishVersion` state that increments monotonically using `bumpPublishVersion()`.

```typescript
// server/publish/publishState.ts
let publishVersion = 0

export function bumpPublishVersion(): number {
  return ++publishVersion        // called after a publish commit
}

export function getPublishVersion(): number {
  return publishVersion
}

```

Every successful publish—whether full-site, per-row, or unpublish—invokes either `bumpPublishVersion()` or `bumpPublishVersionSerialized()`. The latter runs under the publish lock to avoid race conditions during concurrent operations. When baking static artefacts, the system calculates `nextPublishVersion = getPublishVersion() + 1` to stamp the forthcoming version onto placeholders, as seen in [`server/publish/publishSite.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishSite.ts) at lines 199-203.

## The In-Process Publish Lock

Publishing spans three distinct phases: reading data, committing a database transaction, and writing disk artefacts. The window between reading the current version and bumping the version must never interleave with another publish, otherwise `<instatic-hole>` shells could be stamped with stale version numbers.

The `withPublishLock()` function in [`server/publish/publishState.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishState.ts) serializes every publish operation using a promise chain:

```typescript
// server/publish/publishState.ts
let publishChain = Promise.resolve()

export function withPublishLock<T>(fn: () => Promise<T>): Promise<T> {
  const run = () => fn()
  const result = publishChain.then(run, run)
  publishChain = result.then(() => undefined, () => undefined)
  return result
}

```

Both `publishDraftSite` and `publishDataRow` wrap their core logic with this lock. You can see the implementation in [`server/publish/publishSite.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishSite.ts) at lines 83-86 and in [`server/publish/publishRow.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishRow.ts) at lines 50-53, ensuring that only one publish operation mutates global state at any time.

## Version-Keyed Single-Flight Memo Caches

Some expensive, version-specific computations—such as generating the site-wide CSS bundle or snapshot memos used by the hole endpoint—must be cached per publish version. The `createVersionedSingleFlight()` utility constructs a memo that only retains values when the **publish version matches** the version at load time.

```typescript
// server/publish/publishState.ts (excerpt)
export function createVersionedSingleFlight<T>(): VersionedSingleFlight<T> {
  let cache: { version: number; value: T } | null = null
  let inFlight: { version: number; promise: Promise<T | null> } | null = null
  // ...
}

```

When `bumpPublishVersion()` increments the global counter, any memo created via this function automatically invalidates its cached value because the stored version no longer matches `getPublishVersion()`. All version-keyed caches register reset callbacks via `registerVersionedCacheReset()`, and test harnesses can clear the entire state using `resetPublishStateForTests()`.

## The Three-Layer Cache Invalidation Flow

Instatic's cache invalidation operates across three distinct layers, each using the publish version to determine freshness:

### Layer A: Static Artefacts

When baking HTML, CSS, or JavaScript, Instatic writes to an inactive slot before swapping it active. During this process, the `nextPublishVersion` is embedded into every `<instatic-hole>` placeholder via the `data-instatic-version` attribute. This occurs in [`server/publish/publishSite.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishSite.ts) where `nextPublishVersion = getPublishVersion() + 1` prepares the artefacts for the upcoming version.

### Layer B: In-Memory Render Cache

Immediately after the slot swap completes, the system calls `bumpPublishVersion()` synchronously—without awaiting between the swap and the bump—to invalidate the LRU cache and any version-keyed snapshots. This happens at line 301 in [`server/publish/publishSite.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishSite.ts) and line 78 in [`server/publish/publishRow.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishRow.ts).

### Layer C: Hole Runtime

The hole endpoint reads the `data-instatic-version` attribute from incoming requests and compares it against the current global version via `getPublishVersion()`. If the versions differ, the fragment is considered stale and re-fetched, ensuring visitors never receive cached content from a previous publish.

## Architecture Enforcement

To prevent regressions, the architecture test at [`src/__tests__/architecture/publish-bumps-cache-version.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/architecture/publish-bumps-cache-version.test.ts) enforces that every publish entry point imports and calls the bump functions from the `publishState` module. This automated gate verifies that no code path can publish without incrementing the version, as documented in lines 31-35 of the test file.

## End-to-End Example

The following demonstrates how a full-site publish increments the version and affects subsequent requests:

```typescript
import { publishDraftSite } from '@/server/publish/publishSite'
import { getPublishVersion } from '@/server/publish/publishState'

// Trigger a full site publish
await publishDraftSite(db, adminUserId, '/tmp/uploads')

// The version is now incremented
console.log('Current publish version:', getPublishVersion()) // → 1

// Subsequent requests that hit a hole placeholder will see
// data-instatic-version="1" and will be considered fresh.

```

## Summary

- **Global version counter**: [`server/publish/publishState.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishState.ts) maintains a monotonic integer that increments via `bumpPublishVersion()` after every successful publish.
- **Race-condition prevention**: `withPublishLock()` serializes publish operations using a promise chain, ensuring version consistency across the read-transaction-bake lifecycle.
- **Automatic cache invalidation**: Version-keyed single-flight memos created via `createVersionedSingleFlight()` automatically discard stale data when the publish version changes.
- **Three-layer validation**: Static artefacts embed the version in HTML attributes, the in-memory cache invalidates immediately after slot swaps, and the hole runtime validates request versions against the global state.
- **Test enforcement**: Architecture tests ensure every publish path bumps the version, preventing silent cache coherency bugs.

## Frequently Asked Questions

### What triggers a publish version bump in Instatic?

Any successful publish operation—whether `publishDraftSite` for full-site publishes, `publishDataRow` for single-row updates, or unpublish operations—calls `bumpPublishVersion()` or `bumpPublishVersionSerialized()` in [`server/publish/publishState.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishState.ts). The bump occurs immediately after the atomic slot swap, ensuring the new version number reflects the freshly baked artefacts.

### How does Instatic prevent race conditions during concurrent publishes?

The `withPublishLock()` function in [`server/publish/publishState.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishState.ts) implements an in-process promise chain that serializes all publish operations. By wrapping the three-phase publish logic (read → transaction → bake) in this lock, Instatic guarantees that no two publishes can interleave, preventing scenarios where hole placeholders might receive incorrect version stamps.

### What happens to cached data when the publish version increments?

All version-keyed caches created via `createVersionedSingleFlight()` compare their stored version against the global `publishVersion` on every access. When `bumpPublishVersion()` increments the counter, these caches immediately return `null` or recompute their values because the version check fails. Additionally, the in-memory LRU cache invalidates synchronously with the version bump.

### How does the hole endpoint verify content freshness?

The hole endpoint extracts the `data-instatic-version` attribute from incoming requests and compares it to the current value returned by `getPublishVersion()`. If the request's version differs from the global version, the endpoint treats the fragment as stale and re-fetches the content, ensuring users always receive the latest published data even when browser caches or CDN edge nodes are involved.