Vix.cpp docs moved to vixcpp.com
Skip to content

Architecture ​

Softadastra Engine is organized as a modular local-first runtime.

Its architecture is built around one rule:

txt
Local correctness first.
Network later.

The engine is not a single monolithic system. It is a set of focused modules that work together:

txt
core
fs
wal
store
sync
transport
discovery
metadata
cli

Each module has one responsibility, clear boundaries, and explicit failure behavior.

Architecture goal ​

The goal of the engine architecture is to make local-first reliability possible.

A Softadastra node should be able to:

  • accept local work
  • persist it when configured
  • recover after restart
  • track sync work
  • retry later
  • connect peers when available
  • discover peers when available
  • describe itself through metadata
  • expose state through CLI and SDKs

The architecture is designed for unstable environments where the network may fail, peers may disappear, and processes may restart.

High-level architecture ​

The engine can be viewed as layers:

txt
Application / CLI / SDK
        ↓
metadata + discovery
        ↓
transport
        ↓
sync
        ↓
store
        ↓
wal
        ↓
fs
        ↓
core

A more practical view:

txt
core        -> safe primitives
fs          -> filesystem observation
wal         -> durable operation log
store       -> current local state
sync        -> operation propagation state
transport   -> peer communication
discovery   -> peer discovery
metadata    -> node identity and capabilities
cli         -> command-line runtime

The higher layers should depend on lower layers, not the opposite.

Current repository structure ​

The engine repository is:

txt
~/softadastra/softadastra

Current layout:

txt
softadastra/
├── apps/
│   ├── cli/
│   ├── node/
│   └── CMakeLists.txt
├── modules/
│   ├── cli/
│   ├── core/
│   ├── discovery/
│   ├── fs/
│   ├── metadata/
│   ├── store/
│   ├── sync/
│   ├── transport/
│   └── wal/
├── examples/
├── data/
├── cmake/
├── build-ninja/
├── CMakeLists.txt
├── CMakePresets.json
├── CHANGELOG.md
├── README.md
├── cmd.md
├── vix.json
└── LICENSE

The most important folders are:

txt
modules/  -> reusable runtime modules
apps/     -> runnable applications
examples/ -> focused examples
data/     -> local runtime data and test data
cmake/    -> build helpers

Module architecture ​

The engine is split into modules.

Each module should answer one question.

ModuleQuestion
coreWhich primitives are shared everywhere?
fsWhat changed on the filesystem?
walWhich operations were durably recorded?
storeWhat is the current local state?
syncWhich operations need propagation?
transportHow do nodes communicate?
discoveryWhich peers exist?
metadataWho is this node?
cliHow does the user interact from terminal?

This separation keeps the engine understandable.

Core architecture principle ​

The engine should not hide failure.

It should expose:

  • what failed
  • where it failed
  • why it failed
  • what state remains valid
  • what can be retried

This is important because Softadastra is built for unreliable environments.

A peer can disappear. A write can fail. A WAL path can be invalid. A discovery scan can return no peers.

The architecture must make these states explicit.

Local-first architecture ​

Local-first means local state can change without the network.

The runtime should allow:

txt
client writes value
  ↓
value is accepted locally
  ↓
sync tracks the operation
  ↓
transport sends later when possible

The network is not required for the first local write.

The basic local-first path is:

txt
Application
  ↓
Store
  ↓
WAL, if enabled
  ↓
Sync tracking

Transport and discovery are optional.

Offline-first architecture ​

Offline-first means the system keeps working when offline.

The runtime should support:

  • offline writes
  • offline reads
  • pending sync work
  • retry after reconnect
  • recovery after restart
  • eventual convergence

Offline should be treated as a normal state, not as a crash.

The architecture should allow this:

txt
network unavailable
  ↓
local writes continue
  ↓
operations remain in outbox
  ↓
sync retries later
  ↓
transport sends when available

Durability architecture ​

Durability is handled by the WAL.

The WAL stores accepted operations before they are treated as recoverable.

The durability path is:

txt
operation created
  ↓
WAL append
  ↓
WAL flush, when configured
  ↓
store apply
  ↓
sync tracking

The key rule is:

If WAL append fails, the operation must not be treated as durably accepted.

This keeps local-first behavior honest.

Store architecture ​

The store represents current local state.

It is not the same thing as the WAL.

txt
WAL   -> operation history
Store -> current local value

Example:

txt
put user:1 = Gaspard
put user:1 = Softadastra
remove user:1

The WAL remembers the operation sequence.

The store exposes the final state.

The store answers:

  • what is the value now?
  • does this key exist?
  • how many entries exist?
  • what version is this entry?

Sync architecture ​

Sync tracks operations that should be propagated.

It does not send network messages by itself.

The sync layer owns:

  • outbox
  • queue
  • in-flight operations
  • acknowledgements
  • retry policy
  • conflict resolution
  • scheduler tick

The sync path is:

txt
store operation
  ↓
sync operation
  ↓
outbox
  ↓
queue
  ↓
batch
  ↓
transport sends batch

The important separation is:

txt
Sync decides what should be sent.
Transport sends it.

Transport architecture ​

Transport moves messages between peers.

It does not own application state. It does not decide conflict resolution. It does not decide whether a local operation is valid.

Transport owns:

  • peer connection
  • message encoding
  • message dispatch
  • TCP backend
  • client/server communication
  • peer registry
  • sync bridge

The transport path is:

txt
sync batch
  ↓
transport message
  ↓
encoded frame
  ↓
peer connection
  ↓
remote node

If transport fails, local state remains valid.

txt
transport failure
  ↓
delivery delayed
  ↓
sync may retry
  ↓
store remains local

Discovery architecture ​

Discovery finds peers.

It does not connect to peers. It does not send sync operations.

Discovery owns:

  • announcements
  • probes
  • UDP backend
  • discovery registry
  • peer availability
  • stale peer tracking
  • expired peer pruning

The discovery path is:

txt
node announces itself
  ↓
other node receives announcement
  ↓
peer is added to discovery registry
  ↓
transport can connect later

The relationship is:

txt
Discovery finds peers.
Transport connects peers.
Sync sends operations.

Metadata architecture ​

Metadata describes nodes.

It answers:

  • who is this node?
  • what is its hostname?
  • what OS is it running?
  • what version is it running?
  • what capabilities does it expose?
  • how long has it been running?

Metadata owns:

  • node metadata
  • node capabilities
  • metadata registry
  • metadata encoding
  • metadata decoding
  • metadata service
  • platform info
  • hostname
  • version info

Metadata does not own application data.

txt
Metadata -> node description
Store    -> application state

CLI architecture ​

The CLI is a user-facing runtime built on top of the engine.

The CLI module provides reusable command-line primitives:

txt
Tokenizer
ArgParser
CommandLine
ParsedCommand
CliCommand
CommandRegistry
ICommandHandler
CliConfig
CliContext
CliEngine
CliService
TableFormatter
UI style helpers

The CLI app can expose commands such as:

txt
status
node
store
sync
peers

The CLI architecture is:

txt
Terminal input
  ↓
Tokenizer
  ↓
ArgParser
  ↓
CommandRegistry
  ↓
CommandHandler
  ↓
Engine modules
  ↓
Formatted output

Apps architecture ​

The apps/ folder contains runnable programs built from engine modules.

Current apps:

txt
apps/cli
apps/node

The rule is:

txt
modules provide reusable logic
apps compose modules into runnable programs

Application-specific behavior should live in apps/. Reusable runtime behavior should live in modules/.

SDK architecture ​

The SDKs sit above the engine.

txt
Softadastra Engine
  ↓
SDK C++
  ↓
Application
txt
Softadastra Engine
  ↓
SDK JS
  ↓
JavaScript application

The SDK exposes a smaller developer-facing API:

txt
Client
ClientOptions
Result
Error
Key
Value
Peer
NodeInfo
SyncResult
TickResult

The engine remains the lower-level runtime.

Engine and SDK boundary ​

The engine should expose reliable primitives.

The SDK should expose simple developer workflows.

Engine example:

txt
StoreEngine
SyncEngine
TransportEngine
DiscoveryEngine
MetadataService

SDK example:

txt
client.open()
client.put()
client.get()
client.tick()
client.startTransport()
client.startDiscovery()
client.refreshNodeInfo()

The SDK should hide wiring. The engine should keep behavior explicit.

Dependency direction ​

The recommended dependency direction is:

txt
core
  ↓
fs
  ↓
wal
  ↓
store
  ↓
sync
  ↓
transport
  ↓
discovery
  ↓
metadata
  ↓
cli/apps

This is a conceptual direction. Not every module must depend on every previous module.

The important rule is:

lower-level modules must not depend on higher-level modules

For example:

  • core must not depend on sync
  • wal must not depend on discovery
  • store must not depend on CLI
  • transport must not depend on apps

core dependency rule ​

core is the foundation. It must stay independent.

It can provide: Result, Error, IDs, time, hash, config, utilities.

It must not contain: filesystem logic, WAL logic, store logic, sync logic, network logic, CLI logic, business logic.

If core becomes coupled, the entire engine becomes harder to maintain.

fs dependency rule ​

fs observes filesystem state. It can depend on core.

It should not depend on: wal, store, sync, transport, discovery, metadata, cli.

The filesystem module should produce events, snapshots, and paths. It should not decide how to sync files.

wal dependency rule ​

wal records operations. It can depend on: core, fs (only when writing file event records).

It should not depend on: store, sync, transport, discovery, cli.

The WAL should not know how sync works. It should only know how to append, read, stream, and replay records.

store dependency rule ​

store owns current local state. It can depend on: core, wal.

It should not depend on: transport, discovery, metadata, cli.

The store can produce operations for sync, but it should not send them.

sync dependency rule ​

sync owns operation propagation state. It can depend on: core, store.

It should not depend directly on: discovery, metadata, cli, apps.

Sync should not require the network to exist. Sync prepares work. Transport delivers it.

transport dependency rule ​

transport owns communication. It can depend on: core, sync.

It should not depend on: cli, apps, business logic.

Transport should not decide application state. It only moves messages and dispatches them.

discovery dependency rule ​

discovery owns peer finding. It can depend on: core, transport.

It should not own store or sync behavior. Discovery can provide peers to transport. It should not decide what data is synchronized.

metadata dependency rule ​

metadata owns node identity and capabilities. It can integrate with discovery when needed. It should not own application state.

cli dependency rule ​

cli is a user-facing layer. It can depend on engine modules. It should not be required by lower-level modules.

The CLI should be replaceable by another interface: SDK, HTTP API, desktop UI, dashboard, tests.

Engine modules must not depend on CLI output.

Data flow architecture ​

A typical local write follows this path:

txt
SDK or CLI
  ↓
Client or command handler
  ↓
Store operation
  ↓
WAL append, if enabled
  ↓
Store apply
  ↓
Sync outbox entry
  ↓
Tick produces batch
  ↓
Transport sends batch, if peer exists

Read flow architecture ​

A local read is simpler:

txt
SDK or CLI
  ↓
Store get
  ↓
Value or not_found error

A read should not require: WAL, sync, transport, discovery, peer, network.

Reading local state must stay local.

Recovery flow architecture ​

When the process restarts:

txt
client or app starts
  ↓
open runtime
  ↓
read WAL, if enabled
  ↓
replay valid operations
  ↓
rebuild store state
  ↓
rebuild sync state, when supported
  ↓
runtime ready

The key idea is:

accepted durable operations should be recoverable

Sync flow architecture ​

A sync tick follows this path:

txt
tick
  ↓
retry expired operations
  ↓
prune completed operations, if requested
  ↓
select queued operations
  ↓
produce batch
  ↓
return TickResult

The tick should be observable. It should expose: retried count, pruned count, batch size.

Peer communication flow ​

A peer communication flow looks like this:

txt
discovery finds peer, optional
  ↓
transport connects to peer
  ↓
sync produces batch
  ↓
transport sends message
  ↓
remote transport receives message
  ↓
remote dispatcher decodes message
  ↓
remote sync receives operation
  ↓
remote store applies operation

Each layer has a clear job.

Failure architecture ​

The engine architecture assumes failure.

Examples:

  • WAL append failed
  • WAL read failed
  • store key missing
  • sync retry expired
  • transport connection refused
  • discovery returned no peers
  • metadata unavailable
  • filesystem watcher failed
  • CLI command invalid

Each failure should be reported by the layer that detects it.

Do not hide lower-level failures behind generic messages.

Failure isolation ​

A failure in one layer should not destroy unrelated state.

Examples:

txt
transport failure
  -> delivery delayed
  -> local store remains valid

discovery failure
  -> peer finding delayed
  -> local writes still work

sync failure
  -> propagation failed
  -> local value may still exist

metadata failure
  -> diagnostics unavailable
  -> store may still work

This is critical for local-first reliability.

Error architecture ​

Errors should be explicit.

The engine should prefer structured errors over hidden exceptions.

The common pattern is:

txt
Result<T, Error>
  -> ok(value)
  -> err(error)

This allows calling code to decide what to do.

The engine should make it clear:

  • is this error fatal?
  • can it be retried?
  • is local state still valid?
  • was the operation accepted?

Observability architecture ​

Softadastra should be observable.

A developer should be able to inspect:

txt
node id
runtime status
store size
sync outbox size
queued count
failed count
peer count
transport status
discovery status
metadata
WAL path
last sequence

This is why the engine includes: state structs, metadata, CLI output, sync state, registries, examples.

Hidden state makes local-first systems hard to debug.

Configuration architecture ​

Configuration should be explicit.

Examples:

txt
node id
WAL path
auto flush
transport host
transport port
discovery host
discovery port
retry policy
ACK timeout
version
display name

Avoid configuration that silently changes behavior without visibility.

Good configuration:

txt
options.enableWal = true
options.walPath = "data/node-a.wal"
options.autoFlush = true

Bad configuration:

txt
implicit WAL path
hidden transport start
silent discovery enable

Explicit configuration makes failures easier to understand.

Runtime composition ​

A full peer-aware runtime composes modules like this:

txt
Core primitives
  ↓
StoreEngine with WAL-backed config
  ↓
SyncContext with node id
  ↓
SyncEngine
  ↓
TransportContext with SyncEngine
  ↓
TransportEngine
  ↓
DiscoveryContext with TransportEngine
  ↓
DiscoveryEngine
  ↓
MetadataService
  ↓
CLI or SDK

The SDK should hide this composition from application developers.

Minimal local composition ​

A minimal local runtime can be:

txt
core
store

With persistence:

txt
core
wal
store

With sync state:

txt
core
wal
store
sync

With peer communication:

txt
core
wal
store
sync
transport

With peer discovery:

txt
core
wal
store
sync
transport
discovery

With node identity:

txt
core
wal
store
sync
transport
discovery
metadata

Architecture by use case ​

Local-only app ​

Use: core, store. Optional: wal, metadata.

No transport required. No discovery required.

Persistent local app ​

Use: core, wal, store, sync, metadata.

Transport is optional. Discovery is optional.

Peer-aware app ​

Use: core, wal, store, sync, transport, metadata.

Discovery can be added for automatic peer finding.

Local network app ​

Use: core, wal, store, sync, transport, discovery, metadata.

This is the full local-first peer-aware stack.

CLI runtime ​

Use: core, cli, store, sync, transport, discovery, metadata.

The CLI composes the runtime and exposes commands.

Architecture rules ​

The engine should follow these rules:

  1. Keep core independent.
  2. Keep modules focused.
  3. Keep local writes independent from the network.
  4. Persist before sync when WAL is enabled.
  5. Keep sync separate from transport.
  6. Keep discovery separate from transport.
  7. Keep metadata separate from application data.
  8. Expose errors clearly.
  9. Expose runtime state.
  10. Make examples small and focused.

What should not happen ​

Avoid this architecture:

txt
store directly sends TCP messages
core depends on CLI
transport decides conflict resolution
discovery writes application data
metadata owns store values
WAL depends on app commands
local writes require discovery
sync silently drops failed operations

These designs create coupling and make failures harder to debug.

Use this mental diagram:

txt
                    CLI / SDK / Apps
                          ↓
                    Public API Layer
                          ↓
        ┌─────────────────────────────────┐
        │           Metadata              │
        │    identity + capabilities       │
        └─────────────────────────────────┘
                          ↓
        ┌─────────────────────────────────┐
        │           Discovery             │
        │          peer finding            │
        └─────────────────────────────────┘
                          ↓
        ┌─────────────────────────────────┐
        │           Transport             │
        │        peer communication        │
        └─────────────────────────────────┘
                          ↓
        ┌─────────────────────────────────┐
        │             Sync                │
        │   outbox + retry + convergence   │
        └─────────────────────────────────┘
                          ↓
        ┌─────────────────────────────────┐
        │            Store                │
        │       current local state        │
        └─────────────────────────────────┘
                          ↓
        ┌─────────────────────────────────┐
        │             WAL                 │
        │       durable operation log      │
        └─────────────────────────────────┘
                          ↓
        ┌─────────────────────────────────┐
        │             Core                │
        │      errors + ids + time         │
        └─────────────────────────────────┘

This diagram is conceptual. Some modules can be used independently.

Architecture summary per module ​

ModuleLayerResponsibility
coreFoundationShared primitives
fsObservationFilesystem events and snapshots
walDurabilityDurable operation history
storeStateCurrent local key-value state
syncPropagationOutbox, queue, retry, convergence
transportCommunicationPeer messages
discoveryPeer awarenessPeer finding
metadataIdentityNode description
cliInterfaceTerminal commands and UI

When implementing or changing the engine, prefer this order:

  1. core
  2. wal
  3. store
  4. sync
  5. transport
  6. discovery
  7. metadata
  8. cli
  9. apps

This keeps foundational behavior stable before higher-level behavior changes.

Testing architecture ​

Tests should follow module boundaries.

Good test groups:

txt
core tests
fs tests
wal tests
store tests
sync tests
transport tests
discovery tests
metadata tests
cli tests
integration tests

Unit tests should verify each module alone. Integration tests should verify flows across modules.

Examples:

txt
WAL append + read
store recovery from WAL
sync outbox after store operation
transport encodes sync batch
discovery adds peer
metadata describes node
CLI command calls module

Examples architecture ​

Examples should teach one concept at a time.

Good example progression:

txt
core result
fs scan
wal write
wal read
store put/get
store recovery
sync local operation
sync retry
transport client/server
discovery announce/listen
metadata local snapshot
cli command

Avoid one giant example that teaches everything at once.

The SDK can have simpler examples. The engine can have lower-level examples.

Documentation architecture ​

The engine docs should be read like a book:

  1. Overview
  2. Architecture
  3. Runtime Flow
  4. Modules
  5. Core
  6. Filesystem
  7. WAL
  8. Store
  9. Sync
  10. Transport
  11. Discovery
  12. Metadata
  13. CLI

This order follows the runtime from foundations to user-facing tools.

Summary ​

The Softadastra Engine architecture is modular, local-first, and failure-aware.

The key separation is:

txt
WAL persists operations.
Store exposes current state.
Sync tracks propagation.
Transport sends messages.
Discovery finds peers.
Metadata describes nodes.
CLI exposes runtime behavior.

The most important rule is:

Local correctness must not depend on the network.

Next step ​

Continue with runtime flow:

Go to Runtime Flow

Released under the Apache License 2.0.