NATS Architecture — Sovereign M⁸

Audience: Engineers working on the NATS message bus, cell mesh, or FranklinDriver routing.
Updated: 2026-05-05

---

Overview

Franklin.app runs an embedded NATS broker (SwiftNATSServer) on 127.0.0.1:4222.

All inter-process communication between Franklin, VQbitVM, and

FranklinConsciousnessService routes through this broker.

Franklin.app (PID N)
  ├── SwiftNATSServer  ← embedded broker, port 4222
  ├── FranklinDriver   ← NATS client, subscribes to cell + vm + receipt subjects
  │
  ├── VQbitVM (child PID N+1)
  │     └── NATSClient → connects to 127.0.0.1:4222
  │
  └── FranklinConsciousnessService (child PID N+2)
        └── NATSBridge → connects to 127.0.0.1:4222

No external NATS server. No separate terminal. One port, one owner.

---

Subject catalogue

Published by VQbitVM

Subject When Payload
gaiaftcl.vm.ready Once, after mooring acquired {schema_version, timestamp_utc, cell_id}
gaiaftcl.vm.heartbeat Every 30 seconds {tau_block_height, tau_source, tau_stale, moored, tau_sync, blocked_reason, prims_active, timestamp_utc, schema_version}
gaiaftcl.substrate.c4projection On each S4 delta processed C4 projection binary payload

Published by FranklinConsciousnessService

Subject When Payload
gaiaftcl.franklin.consciousness.state After GENESIS, on each pulse {terminalState, health, reason, memoryCount}
gaiaftcl.substrate.s4delta Synthetic S4 deltas for projection catch-up S4DeltaWire binary
gaiaftcl.stage.moored After geo-mooring acquired empty payload

Published by Franklin (FranklinDriver)

Subject When Payload
gaiaftcl.ingestion.request When user triggers ingestion {primIDs, sessionID, sourceText}
gaiaftcl.scene.select When user selects a domain scene scene ID string
gaiaftcl.franklin.monologue Franklin inner monologue lines UTF-8 text

Subscribed by FranklinDriver

client.subscribe(to: "gaiaftcl.cell.>")          // external HEL/NBG cell heartbeats
client.subscribe(to: NATSConfiguration.vmHeartbeatSubject)  // gaiaftcl.vm.heartbeat
client.subscribe(to: NATSConfiguration.ingestionReceiptSubject)
client.subscribe(to: NATSConfiguration.sceneSelectSubject)
client.subscribe(to: NATSConfiguration.franklinMonologueSubject)

---

The 9-cell sovereign mesh

The sovereign mesh has 9 named cells across two regions:

Region Cells Purpose
HEL (Helsinki) hel-01, hel-02, hel-03, hel-04, hel-05 Primary computation
NBG (Nuremberg) nbg-01, nbg-02, nbg-03, nbg-04 Secondary computation

In a multi-machine production deployment, each named cell runs its own

VQbitVM instance that publishes gaiaftcl.cell.<id>.heartbeat to the mesh NATS

bus. FranklinDriver listens on gaiaftcl.cell.> and inserts each cell ID into

driver.liveCells on receipt.

In a standalone sovereign DMG deployment (single machine), the named cells

don't run as separate processes. VQbitVM is the sovereign substrate managing the

whole mesh.

---

Standalone mesh fix: vm.heartbeat → all cells live

Problem (pre 2026-05-05): FranklinDriver.liveCells was always empty in

standalone mode. VQbitVM published gaiaftcl.vm.heartbeat but FranklinDriver

only subscribed to gaiaftcl.cell.> — a different subject pattern. The heartbeat

went unread, liveCells.count stayed 0, and OQ-004 (quorum check) always failed

in standalone deployments.

Fix: FranklinDriver now also subscribes to gaiaftcl.vm.heartbeat. When

that heartbeat arrives, all 9 mesh cell IDs are marked live:

// FranklinDriver.handle(_:)
if subject == NATSConfiguration.vmHeartbeatSubject {
    let now = Date()
    for cell in CellDescriptor.all {
        liveCells.insert(cell.id)
        lastHeartbeat[cell.id] = now
    }
    return
}

Rationale: VQbitVM IS the sovereign substrate. When its heartbeat arrives,

the mesh is alive. All 9 cells share the same substrate — marking them all live

from a single vm.heartbeat is the correct semantic for sovereign mode.

Timing: VQbitVM waits 30 seconds before its first heartbeat publish:

Task {
    while !Task.isCancelled {
        try await Task.sleep(for: .seconds(30))
        await publishHeartbeat(...)
    }
}

The first vm.heartbeat arrives ~30s after launch. Before it arrives, OQ-004

passes via the standalone safety net (launcher.phase == .ready). After it

arrives, OQ-004 passes via the standard path (9/9 cells live, threshold: 5).

---

CellDescriptor

// FranklinDriver.swift
public struct CellDescriptor: Identifiable, Hashable, Sendable {
    public let id:     String   // e.g. "hel-01"
    public let label:  String   // e.g. "01"
    public let region: String   // "HEL" or "NBG"
}

extension CellDescriptor {
    public static let quorumRequired = 5   // ≥5/9 cells must be live
    public static let all: [CellDescriptor] = [
        .init(id: "hel-01", label: "01", region: "HEL"),
        // ... hel-02 through hel-05 ...
        .init(id: "nbg-01", label: "01", region: "NBG"),
        // ... nbg-02 through nbg-04 ...
    ]
}

driver.liveCells: Set<String> — IDs confirmed alive via heartbeat (last 30s

for Cell Mesh Inspector; no expiry for OQ-004 quorum check — heartbeats refresh

the timestamp but never remove from the set within a session).

driver.activeCells: Set<String> — cells toggled ON by the user in the Cell

Target Grid UI. This is the ingestion routing set; quorumMet uses

activeCells, not liveCells.

---

NATSConfiguration reference

Key static members (Sources/GaiaFTCLCore/NATSConfiguration.swift):

public static let vqbitNATSURL            = "nats://127.0.0.1:4222"
public static let vmReadySubject          = "gaiaftcl.vm.ready"
public static let vmHeartbeatSubject      = "gaiaftcl.vm.heartbeat"
public static let ingestionReceiptSubject = "gaiaftcl.ingestion.receipt"
public static let sceneSelectSubject      = "gaiaftcl.scene.select"
public static let franklinMonologueSubject = "gaiaftcl.franklin.monologue"
public static let franklinStageMooredSubject = "gaiaftcl.stage.moored"
public static let tauSubject              = "gaiaftcl.tau.sync"

// Per-cell subjects (not used in standalone; used in multi-machine mesh)
public static func cellHeartbeatSubject(_ cellID: String) -> String {
    "gaiaftcl.cell.\(cellID).heartbeat"
}
public static func cellInteractionSubject(_ cellID: String) -> String {
    "gaiaftcl.cell.\(cellID).interaction"
}

---

NATS connection lifecycle

1. Franklin.app startsSwiftNATSServer binds port 4222.

2. Franklin creates NATSClient, calls connect(), subscribes to subjects.

3. Franklin spawns VQbitVM as a child process with GAIAFTCL_TENSOR_N,

GAIAFTCL_TENSOR_PATH, etc. in environment.

4. VQbitVM connects to 127.0.0.1:4222, subscribes to s4delta, tau, mooring.

5. Franklin publishes gaiaftcl.stage.moored → VQbitVM emits gaiaftcl.vm.ready.

6. SovereignStackLauncher.phase transitions to .ready on vm.ready receipt.

7. FranklinConsciousnessService connects, begins C4 projection loop.

8. VQbitVM publishes first gaiaftcl.vm.heartbeat after 30s → all 9 cells marked live.

---

Diagnosing NATS issues

# Confirm NATS broker is live
nc -z 127.0.0.1 4222 && echo "NATS up"

# Watch all messages (requires nats CLI)
nats sub "gaiaftcl.>" --server nats://127.0.0.1:4222

# Check VQbitVM process is running
pgrep -la VQbitVM

# Check vm.heartbeat arriving (fires every 30s)
nats sub "gaiaftcl.vm.heartbeat" --server nats://127.0.0.1:4222

Common failure: OQ-004 0/9 cells live

Federation cosignature: pending operator signing host (v26). Witness (sha256 of rendered body): 00d47b2ea438d6779f82c23791bb218059ba0e5b136d1b78a1d0a9c5720d4d90. This page serves with a substrate-honest pending-signature notice until the operator's Franklin signer cosigns it.