Architecture Overview

How Static's crates, protocols, and components fit together.

Architecture Overview

This document describes the technical architecture of Static: how the crates are organized, how messages flow through the system, how encryption works, and how the privacy layer operates. It is intended for developers, security researchers, and anyone who wants to understand the system at a technical level.

Design Principles

Static is built around six core design principles:

  1. Zero-knowledge relay. The supernode relays encrypted messages but cannot read them. It acts as a dumb pipe for MLS ciphertext. Community operators can host infrastructure without gaining access to private conversations.

  2. No identity requirements. Users are identified by Ed25519 public keys, not email addresses, phone numbers, or usernames. There is no central identity registry. Creating an identity is an offline operation that generates a key pair locally.

  3. Local-first storage. All decrypted content lives in a local SQLCipher-encrypted database. The UI reads from local storage, not from the network. Network connectivity is required only for sending and receiving new messages.

  4. MLS group management. One MLS group per channel provides end-to-end encryption with forward secrecy and post-compromise security. MLS Commit ordering is delegated to the supernode (single-committer pattern).

  5. Minimal metadata exposure. The privacy layer uses relay routing, ephemeral session keys, message padding, and batched delivery to reduce the metadata visible to network observers and supernodes.

  6. Defense in depth. No single mechanism provides complete privacy. Multiple overlapping layers (transport encryption, MLS content encryption, relay routing, padding, batching, cover traffic) work together.

System Overview

Static has three types of participants:

  • Client app — The Flutter application running on a user’s device. Contains the Rust core library for cryptography, networking, and storage. Communicates with supernodes and other peers via Iroh QUIC connections through relays.

  • Supernode — A persistent headless server that facilitates community operations. Relays encrypted messages, orders MLS Commits, archives ciphertext, and bridges push notifications. Cannot decrypt content.

  • Iroh relays — Third-party or self-hosted relay servers that forward QUIC packets between clients and supernodes. They see source IP addresses but not message content (which is MLS-encrypted inside the QUIC stream). Clients connect through relays to hide their IP from supernodes.

Client A ──► Iroh Relay ──► Supernode ──► Iroh Relay ──► Client B
  (encrypts)   (sees IP A,      (sees ciphertext,    (sees IP B,    (decrypts)
                not content)     not IPs or content)  not content)

Crate Architecture

The Rust backend is organized as a Cargo workspace with strict dependency boundaries. Each crate has a single responsibility and a well-defined public API.

static-core         CoreApi: single entry point for the Flutter client
  |-- static-net        Iroh networking + privacy primitives
  |-- static-mls        OpenMLS abstraction layer
  |-- static-store      SQLite/SQLCipher local storage
  |-- static-crdt       CRDT implementations (pure logic, no I/O)
  |-- static-identity   Key management, device certificates, recovery
  |-- static-voice      LiveKit voice/video integration
  |-- static-supernode  Supernode binary
  |-- static-push-relay Push notification relay binary

Dependency Rules

These rules are enforced at code review time and will be enforced by CI:

  • static-net is the only crate that imports iroh directly. All other crates interact through the IrohPrivacyService trait. This isolates the networking layer and allows testing with mock transports.

  • static-mls is the only crate that imports openmls directly. All other crates interact through the MlsService trait. This isolates MLS complexity and allows swapping implementations.

  • static-store is the sole database owner. No other crate opens SQLite connections. All persistence goes through MessageStore.

  • The Flutter UI never touches crypto, networking, or storage directly. It calls into CoreApi via flutter_rust_bridge generated FFI bindings.

  • No circular dependencies. If crate A depends on crate B, crate B cannot depend on crate A.

static-core — CoreApi

The central orchestration crate. Exposes CoreApi, a thread-safe (Send + Sync + 'static) struct that serves as the single entry point for all backend operations. The Flutter client calls CoreApi methods; it never interacts with lower-level crates directly.

// Simplified CoreApi interface
impl CoreApi {
    pub async fn initialize(config: CoreConfig) -> Result<Self, CoreError>;
    pub fn subscribe_events(&self) -> EventReceiver;

    // Identity
    pub async fn create_identity(&self, name: &str, device: &str) -> Result<IdentityInfo, CoreError>;
    pub async fn get_identity(&self) -> Option<IdentityInfo>;

    // Communities & channels
    pub async fn create_community(&self, name: &str) -> Result<CommunityInfo, CoreError>;
    pub async fn create_channel(&self, community: CommunityId, name: &str, kind: ChannelType) -> Result<ChannelInfo, CoreError>;

    // Messaging
    pub async fn send_message(&self, channel: ChannelId, content: &str) -> Result<Message, CoreError>;
    pub async fn get_messages(&self, channel: ChannelId, before: Option<u64>, limit: u32) -> Result<Vec<Message>, CoreError>;

    // Network
    pub async fn connect_to_supernode(&self, addr: &str) -> Result<(), CoreError>;
    pub async fn connection_state(&self) -> ConnectionState;

    // Privacy
    pub async fn get_privacy_config(&self) -> PrivacyConfig;
    pub async fn set_privacy_config(&self, config: PrivacyConfig) -> Result<(), CoreError>;

    pub async fn shutdown(&self) -> Result<(), CoreError>;
}

CoreApi uses interior mutability (Arc<RwLock<_>>) so all methods take &self. It emits CoreEvent values through a broadcast channel that the UI subscribes to for real-time updates.

static-net — Networking

Wraps Iroh’s QUIC networking with the custom privacy layer:

  • Endpoint management — Creates and configures Iroh endpoints with ephemeral session keys, relay-only mode, and custom relay URLs.
  • Message padding — Pads all outgoing messages to fixed-size buckets (256B, 1KB, 4KB, 16KB) using a randomized wire format: [4-byte random mask][4-byte XOR'd length][payload][random padding].
  • Batched delivery — Queues outgoing messages and delivers them in periodic batches.
  • Cover traffic — Generates optional dummy encrypted messages at configurable intervals.
  • Ephemeral session rotation — Rotates the node’s connection identity periodically (default: every 4 hours).

static-mls — MLS Encryption

Wraps OpenMLS v0.8 with Static-specific abstractions:

  • Group lifecycle — Create, join (via Welcome), update, and leave MLS groups.
  • Commit ordering — Integrates with the supernode’s single-committer ordering.
  • StorageProvider — Custom SQLite-backed MLS state storage, wrapped in transactions with static-store to prevent desync on crash.
  • Key export — Derives per-sender media keys for LiveKit voice E2EE from MLS epoch secrets.

static-store — Local Storage

SQLCipher-encrypted SQLite database for all local persistent state:

  • Messages — Plaintext messages (decrypted locally), indexed by channel and timestamp.
  • FTS5 search — Full-text search index over message content.
  • Communities and channels — Local cache of community/channel metadata.
  • Identity — Local device identity (master public key, device public key, display name).
  • Settings — Key-value store for user preferences and privacy configuration.
  • Roles — Per-community member roles.
  • Attachments — File attachment metadata and chunked encrypted data.

All database operations run via tokio::task::spawn_blocking because rusqlite is synchronous. The MessageStore wraps Arc<Mutex<Connection>> for thread-safe concurrent access.

The encryption key is set via PRAGMA key as the first statement after opening the connection (SQLCipher requirement).

static-identity — Key Management

Manages the Ed25519 identity hierarchy:

  • Master key pair — Generated from BIP-39 entropy, stored in the OS keychain or encrypted file.
  • Device certificates — Delegated key pairs signed by the master key, one per device.
  • Recovery — BIP-39 mnemonic generation and master key derivation.
  • Key zeroization — All key material in memory is zeroized on drop using the zeroize crate.

static-supernode — Supernode Binary

The headless server binary. Accepts QUIC connections from clients, manages channel subscriptions, relays encrypted messages, and orders MLS Commits.

Key features:

  • Batched message delivery with configurable intervals.
  • Per-channel encrypted message archive for offline catch-up.
  • Persistent Node ID via secret key stored in data_dir/node.key.
  • Configurable resource limits (max connections, channels, message size).
  • Graceful shutdown on CTRL-C.

Message Send/Receive Flow

When a user sends a message, here is exactly what happens:

Sending

  1. User types message in the Flutter UI and taps Send.
  2. Flutter calls CoreApi::send_message(channel_id, content) via flutter_rust_bridge.
  3. CoreApi validates the input (non-empty content, valid channel).
  4. MLS encryption: The plaintext is encrypted using the channel’s current MLS group key, producing a single ciphertext blob.
  5. Message padding: The ciphertext is padded to the next fixed-size bucket (256B / 1KB / 4KB / 16KB). The wire format is: [4-byte random mask][4-byte XOR'd length][ciphertext][random padding].
  6. Local storage: The plaintext message is written to the local SQLCipher database with a generated message ID and timestamp. The FTS5 index is updated.
  7. Batched delivery: The padded ciphertext is queued for the next batch interval.
  8. Transmission: At the next batch tick, all queued messages are shuffled and sent to the supernode via the Iroh QUIC connection (which routes through a relay).
  9. CoreApi emits a MessageSent event to the UI.

Receiving

  1. Supernode receives the ciphertext from the sender’s relay connection.
  2. Supernode assigns a sequence number and fans out the ciphertext to all online subscribers of that channel.
  3. Recipient’s Iroh connection receives the padded ciphertext from the supernode via a relay.
  4. Unpadding: The padding is stripped using the XOR mask and length header.
  5. MLS decryption: The ciphertext is decrypted using the channel’s MLS group key, yielding the plaintext.
  6. Local storage: The plaintext is written to the local SQLCipher database. The FTS5 index is updated.
  7. CoreApi emits a MessageReceived event to the UI.
  8. Flutter UI updates the channel view with the new message.

MLS Group Lifecycle

Each channel has its own MLS group with an independent epoch counter and key schedule.

Group Creation

When a community owner creates a channel:

  1. A new MLS group is created with the owner as the only member.
  2. The group ID is derived from the channel ID.
  3. The initial group state is persisted to static-store.

Adding Members

When a new user joins a channel:

  1. The community admin (or designated committer) creates an MLS Add Proposal for the new member, using their published KeyPackage.
  2. The admin creates an MLS Commit containing the Add Proposal.
  3. The Commit is sent to the supernode, which validates and orders it (first-valid-commit-wins for each epoch).
  4. The supernode distributes the Commit to existing members.
  5. An MLS Welcome message is generated and delivered to the new member via QUIC streams (Welcomes can be large for bigger groups, so chunked delivery is used for groups over ~10 members).
  6. The new member processes the Welcome, derives the epoch secrets, and joins the group.

Removing Members

When a member is removed or leaves:

  1. An MLS Remove Proposal is created.
  2. A Commit containing the Remove is submitted to the supernode.
  3. All remaining members process the Commit, advancing the epoch and rotating keys.
  4. The removed member can no longer decrypt future messages (forward secrecy).

Epoch Transitions

Every Add, Remove, or Update Commit advances the group’s epoch number. Epoch transitions rotate the group’s encryption keys, providing:

  • Forward secrecy: Old keys are deleted; compromising current keys does not expose past messages.
  • Post-compromise security: If a member’s key was compromised but then rotated via an Update, future messages are again secure.

Epoch numbers are scoped per group — different channels have independent epoch counters.

Privacy Layer

The privacy layer is implemented in static-net and consists of four mechanisms:

Layer 1: Relay Routing

All client traffic routes through Iroh relays by default (relay_only = true). The relay sees the client’s IP address but not message content (which is MLS-encrypted inside the QUIC stream). The supernode sees the relay’s IP, not the client’s.

Clients connect to a pool of relays (not a single relay) for redundancy and to distribute trust. Relay selection is randomized and rotated periodically.

Layer 2: Ephemeral Session Keys

Each app session (or every N hours, configurable via node_id_rotation_hours, default 4) generates a fresh ephemeral keypair for the Iroh connection. This prevents:

  • The supernode from correlating today’s session with yesterday’s session by connection identity.
  • Relay operators from building long-term profiles of connection patterns.

The user’s long-term Ed25519 identity is only revealed inside MLS-encrypted channel messages, never at the network layer.

Layer 3: Message Padding

All messages are padded to one of four fixed-size buckets before transmission:

BucketSizeTypical Content
0256 bytesControl messages, acknowledgments
11,024 bytesShort text messages
24,096 bytesLonger messages, small files
316,384 bytesMLS Commits, Welcomes for small groups

The wire format uses a randomized header:

[mask: 4 random bytes][masked_length: u32 LE XOR mask][payload][random_padding]

The length prefix is XOR’d with the random mask so the header bytes are indistinguishable from random data. The padding bytes are also random, making the entire padded message indistinguishable from random to an observer.

Messages larger than 16,376 bytes (16,384 minus the 8-byte header) must be chunked before padding.

Layer 4: Batched Delivery and Cover Traffic

Batched delivery: Outgoing messages are queued and delivered in periodic batches (default: every 1000ms on clients, 500ms on supernodes). Messages within a batch are shuffled before transmission, preventing timing correlation between message creation and network transmission.

Cover traffic (optional, disabled by default): When enabled, the client generates dummy encrypted messages at configurable intervals. These are indistinguishable from real messages to network observers (same padding, same encryption layer). Cover traffic masks activity patterns — an observer cannot determine whether a given transmission is a real message or a dummy.

Cover traffic generates approximately 1 KB per 30 seconds of overhead per connection. It is disabled by default to conserve bandwidth, especially on mobile/metered connections.

Storage Model

Local Device Storage (SQLCipher)

The local database schema includes:

  • messages — Plaintext messages indexed by (channel_id, created_at) for efficient pagination. Supports soft-delete (tombstone) and edit tracking.
  • messages_fts — FTS5 virtual table for full-text search over message content.
  • communities — Community metadata (ID, name, created_at).
  • channels — Channel metadata (ID, community_id, name, type, DM peer ID).
  • local_identity — Single-row table for the local device identity (master and device public keys, display name).
  • settings — Generic key-value store for user preferences.
  • roles — Per-community member role assignments.
  • attachments — File attachment metadata (file ID, channel, name, size, MIME type, encryption key, chunks).
  • attachment_chunks — Individual encrypted file chunks.

The database is opened with PRAGMA key as the first statement (SQLCipher requirement). WAL mode is used for concurrent read access. FTS5 indexing is updated on every message insert.

Supernode Storage

The supernode stores:

  • Encrypted message archive — Per-channel ciphertext for offline catch-up. Configurable archive depth (default: last 1000 messages per channel).
  • MLS commit ordering log — Canonical commit sequence per channel for consistent epoch management.
  • Node secret keynode.key file for persistent identity.

The supernode stores only ciphertext. It has no ability to decrypt the messages it archives.

Identity Model

Key Hierarchy

Master Key (Ed25519, from BIP-39 entropy)
    |
    |-- Device Key #1 (delegated, signed by master)
    |       |-- MLS Credential (per-group, per-device)
    |
    |-- Device Key #2 (delegated, signed by master)
    |       |-- MLS Credential (per-group, per-device)
    |
    |-- Recovery Key (BIP-39 mnemonic, offline backup)
  • Master key is derived from 256 bits of entropy, encoded as a 24-word BIP-39 mnemonic. It never leaves secure storage.
  • Device keys are Ed25519 key pairs delegated from the master key via signed device certificates. One per device.
  • MLS credentials are derived per-group, per-device from the device key. They are used within MLS groups and verified by other members against the device certificate chain.

User ID

A user’s ID is the 32 bytes of their master Ed25519 public key. This is a permanent, globally unique identifier that does not depend on any central registry.

Pinned Dependencies

Static pins critical dependency versions to prevent regressions:

DependencyVersionRole
iroh0.96QUIC networking, relay connections
iroh-gossip0.96Pub/sub for group fan-out
iroh-blobs0.97Content-addressed blob transfer
openmls0.8MLS protocol implementation
rusqlite0.31SQLite/SQLCipher database access
tokio1Async runtime
ed25519-dalek2Ed25519 signatures
automerge0.5CRDT for community settings

These versions are not upgraded without full regression testing. The Rust edition is 2021 with MSRV 1.75+. All wire protocol messages use CBOR serialization via the ciborium crate.

Current Status

Static is in Phase 0 (Proof of Concept). The current implementation includes:

  • Working supernode with message relay, batch delivery, and MLS commit ordering.
  • Client app with identity creation, community/channel management, and real-time messaging.
  • MLS encryption for all messages.
  • Privacy layer with relay routing, ephemeral sessions, and message padding.
  • SQLCipher local storage with FTS5 search.
  • Ed25519 identity with BIP-39 recovery phrases and device certificates.

Planned but not yet implemented:

  • LiveKit voice/video integration.
  • Push notification relay.
  • Multi-device sync.
  • CRDT-based reactions, pins, and community settings.
  • Multi-supernode architecture (Phase 2).