# How Instatic's Two-Slot Symlink Swap Enables Atomic Page Publishing

> Instatic ensures atomic page publishing with its two-slot symlink swap. Discover how this method prevents partial file views for robust content delivery.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-28

---

**Instatic guarantees atomic page publishing by maintaining two directory slots (`a` and `b`) under `uploads/published/` and atomically flipping a `current` symlink between them using a POSIX `rename(2)` operation, ensuring readers never see partially written files.**

In the `CoreBunch/Instatic` repository, the **two-slot symlink swap** is the core mechanism that makes static page publishing an all-or-nothing operation. This design ensures that site visitors always see a complete, consistent version of the published files and never encounter a partially written directory. The implementation lives primarily in [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts) and leverages the atomicity of the `rename(2)` system call to cut over traffic instantly.

## The Two-Slot Directory Model

The architecture relies on three elements inside `uploads/published/`:

- **`uploads/published/a`** — A complete directory of static artefacts for one publish.
- **`uploads/published/b`** — A complete directory of static artefacts for the next publish.
- **`uploads/published/current`** — A symlink that points to whichever slot is active. All runtime readers resolve files through this symlink.

A comment at the top of [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts) (line 8) outlines this model: "`current -> a | b (symlink; visitor router reads through this)`". Because the kernel resolves the symlink on every `open(2)`, each HTTP request sees a consistent snapshot of whichever slot was active at the moment the request began.

## The Atomic Publishing Pipeline

### Step 1 — Identify the Inactive Slot

Before writing any new files, the publisher determines which slot is safe to overwrite. The `getInactiveSlot()` function in [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts) (line 178) inspects the `current` symlink and returns the opposite slot, either `'a'` or `'b'`. This guarantees that the actively served directory remains untouched during the write phase.

### Step 2 — Write Artefacts to the Inactive Slot

With the inactive slot identified, Instatic copies the freshly generated static pages into that directory—for example, `uploads/published/a`. Since `current` still points to the other slot, existing readers continue to see the old, fully published version while the new files land on disk without interfering with live traffic.

### Step 3 — Stage a Temporary Symlink

Once the inactive slot contains a complete set of files, the code creates a temporary symlink named `current.tmp` that points to the newly filled slot. In [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts) (line 286), the implementation calls `symlink(targetSlot, tmpPath)` to prepare the new pointer without affecting the live `current` symlink.

### Step 4 — Atomically Flip the Pointer

The critical moment uses `rename(tmpPath, currentPath)` as implemented around line 258 of [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts). On POSIX systems, renaming a symlink is an atomic kernel operation; it either succeeds completely or fails without side effects, ensuring that `current` switches instantly from the old slot to the new one. The implementation also includes a Windows fallback that first removes a stale temporary symlink before renaming, preserving the same all-or-nothing guarantee on non-POSIX platforms.

### Step 5 — Leave the Previous Slot Intact

After the rename succeeds, the old slot remains on disk in its complete state. This enables near-instant rollbacks and eliminates any race condition where a reader might see a half-written directory. The swap is literally atomic: every request resolves `current/<file>` at the kernel level, so the moment the rename completes, subsequent opens see the new slot, while in-flight requests continue reading from the old slot.

## Why the Two-Slot Symlink Swap Works

Several properties of filesystems and the Instatic implementation combine to make this reliable:

- **Atomic `rename(2)`** — The Linux and macOS kernels handle symlink renames atomically, meaning `current` never points to an invalid or mixed state.
- **Directory-level isolation** — Each slot is a completely separate directory tree, so writing never corrupts the files that readers currently have open.
- **Kernel-resolved symlinks** — Because the symlink is resolved on every `open(2)`, there is no user-space caching that could leak an intermediate state to visitors.
- **Graceful Windows fallback** — The special-case logic for Windows removes stale temporary links before renaming, preventing atomicity violations on non-POSIX hosts.

The "swapSlot semantics" test in [`src/__tests__/server/staticArtefact.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/staticArtefact.test.ts) (lines 151–158) validates that `swapSlot` correctly flips the active slot and that the previously live directory stays intact.

## Publishing and Reading Through the Symlink

**Flipping the active slot during a publish:**

```typescript
import { swapSlot, getInactiveSlot } from '@server/publish/staticArtefact';

// Determine the inactive slot (the one we will write to)
const inactive = await getInactiveSlot(uploadsDir);

// Write your new static files into `uploads/published/${inactive}` …

// Atomically make the new slot live
await swapSlot(uploadsDir, inactive);

// After the call, getActiveSlot points to the freshly published slot
const nowLive = await getActiveSlot(uploadsDir); // 'a' or 'b'

```

**Reading a page through the stable symlink endpoint:**

```typescript
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';

// The router always reads through the `current` symlink
export async function readPublishedPage(path: string) {
  const fullPath = join(uploadsDir, 'published', 'current', path);
  return await readFile(fullPath, 'utf8'); // kernel resolves symlink atomically
}

```

## Core Source Files

- **[`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts)** — Contains the core protocol implementation, including `getActiveSlot`, `getInactiveSlot`, and `swapSlot`.
- **[`src/__tests__/server/staticArtefact.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/staticArtefact.test.ts)** — Exercises atomic swap behavior, slot discovery, and crash recovery scenarios such as the `swapSlot` semantics test.
- **[`server/publish/publicRouter.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publicRouter.ts)** — Demonstrates how the production server resolves requests by reading files through the `current` symlink.
- **[`docs/features/publisher.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/features/publisher.md)** — Provides the high-level architectural overview of the publishing pipeline, including how the two-slot symlink swap serves Layer A.

## Summary

- Instatic uses a **two-slot symlink swap** to guarantee that published pages are never served in a partially written state.
- The design depends on two physical directories, `a` and `b`, and a single `current` symlink that points to the active slot.
- `getInactiveSlot()` identifies the safe target for new writes, while `swapSlot()` performs an atomic `rename(2)` to cut over traffic.
- Readers always resolve files through `current`, so the kernel's atomic symlink semantics ensure each request sees a consistent snapshot.
- The old slot remains available after a cutover, enabling immediate rollback if a new publish must be reverted.

## Frequently Asked Questions

### How does Instatic's two-slot symlink swap prevent serving partially published pages?

By writing every new publish into whichever directory slot `current` does **not** point to, Instatic ensures that live readers never touch incompletely written files. The final cutover happens through a single atomic `rename(2)` on the symlink, so at no point can `current` resolve to a mixed or half-written directory.

### What happens if a publish fails midway through writing to the inactive slot?

If the write phase aborts or crashes before `swapSlot` is called, the `current` symlink simply continues pointing to the previously completed slot. Visitors remain unaffected, and the publisher can retry by wiping the incomplete inactive slot and starting the write fresh.

### Is the symlink swap atomic on Windows as well as Linux?

Yes. While POSIX kernels guarantee atomic symlink renames natively, [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts) (around line 258) includes a Windows-specific fallback that removes any stale temporary symlink before performing the rename. This non-POSIX path preserves the same all-or-nothing guarantee.

### How can I read published pages through the current symlink?

Any runtime reader—such as the request handler in [`server/publish/publicRouter.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publicRouter.ts)—should build paths under `uploads/published/current/`. Because the kernel resolves the symlink on every `open(2)`, each request automatically reads from whichever slot is active at that exact instant.