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 starts → SwiftNATSServer 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
- VQbitVM may not have published its first heartbeat yet (wait 30s after launch).
- VQbitVM may have failed to start (check
SovereignStackLauncherlogs). FranklinDriver.connect()may not have been called (check boot sequence).
00d47b2ea438d6779f82c23791bb218059ba0e5b136d1b78a1d0a9c5720d4d90.
This page serves with a substrate-honest pending-signature notice until the operator's Franklin signer cosigns it.