# AimDB - Full Documentation and Blog Content > Distributed by design. Data-driven by default. AimDB is a distributed dataflow runtime written in Rust. It lets you define portable data contracts (records) once and deploy them across the full hardware spectrum — from STM32 microcontrollers running `no_std + alloc` to Linux edge gateways, Kubernetes clusters, and browser-based SPAs via WebAssembly. The same Rust API works everywhere, eliminating the serialization mismatches and glue code that plague traditional IoT architectures. AimDB is **not** a traditional database. It is a reactive, in-memory engine where data flows through typed records, buffers, and transforms — enabling real-time sync, automatic observability, and declarative data pipelines without external message brokers. --- ## Documentation ### Getting Started AimDB is an async, in-memory dataflow engine for distributed systems across **MCU → edge → cloud → browser** environments. Distributed by design, data-driven by default. Define your schemas once and deploy them anywhere. #### Installation Add AimDB to your `Cargo.toml`: ```toml [dependencies] aimdb-core = "0.4" aimdb-tokio-adapter = { version = "0.4", features = ["tokio-runtime"] } serde = { version = "1.0", features = ["derive"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } # OR for embedded/no_std environments: # aimdb-core = { version = "0.4", default-features = false, features = ["alloc"] } # aimdb-embassy-adapter = "0.4" ``` #### Quick Example ```rust use aimdb_core::{AimDbBuilder, buffer::BufferCfg, RuntimeContext, Producer}; use aimdb_tokio_adapter::{TokioAdapter, TokioRecordRegistrarExt}; use serde::{Deserialize, Serialize}; use std::sync::Arc; // Define your data contract #[derive(Debug, Clone, Serialize, Deserialize)] struct Temperature { celsius: f32, sensor_id: String, } // Background producer task - simulates a sensor async fn sensor_producer( ctx: RuntimeContext, producer: Producer, ) { let time = ctx.time(); for i in 0..5 { let temp = Temperature { celsius: 20.0 + (i as f32 * 0.5), sensor_id: "sensor-01".into(), }; println!("📤 Producing: {:.1}°C", temp.celsius); producer.produce(temp).await.ok(); time.sleep(time.secs(1)).await; } } #[tokio::main] async fn main() -> Result<(), Box> { let adapter = Arc::new(TokioAdapter); let mut builder = AimDbBuilder::new().runtime(adapter); builder.configure::("sensor::Temperature", |reg| { reg.buffer(BufferCfg::SpmcRing { capacity: 10 }) .source(sensor_producer) .tap(|_ctx, consumer| async move { let Ok(mut reader) = consumer.subscribe() else { return }; while let Ok(temp) = reader.recv().await { println!("📥 Received: {:.1}°C from {}", temp.celsius, temp.sensor_id); } }); }); let _db = builder.build().await?; tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; Ok(()) } ``` This example demonstrates AimDB's core pattern: - `.source()` registers a background producer task - `.tap()` reacts to every produced value - `.build()` spawns all tasks automatically --- ### Data Contracts Data contracts are the heart of AimDB. They define your data schemas in Rust and ensure type safety across all deployment targets—from microcontrollers to Kubernetes. #### Why Data Contracts? | Problem | Solution | |---------|----------| | Celsius vs Fahrenheit confusion | Explicit units in contract metadata | | Schema drift between services | Single source of truth | | Breaking changes in production | Versioned contracts with migrations | | Platform-specific serialization | Portable serde-based encoding | #### Installation ```toml [dependencies] aimdb-data-contracts = "0.4" [dependencies.aimdb-data-contracts] version = "0.4" features = ["linkable", "simulatable", "migratable", "observable"] ``` #### Defining Contracts Contracts implement the `SchemaType` trait for identity and versioning: ```rust use aimdb_data_contracts::{SchemaType, Settable}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Temperature { pub celsius: f32, pub timestamp: u64, } impl SchemaType for Temperature { const NAME: &'static str = "temperature"; const VERSION: u32 = 1; } impl Settable for Temperature { type Value = f32; fn set(value: Self::Value, timestamp: u64) -> Self { Self { celsius: value, timestamp } } } ``` #### Schema Traits | Trait | Feature | Purpose | |-------|---------|---------| | `SchemaType` | always | Identity (`NAME`, `VERSION`) | | `Settable` | always | Canonical construction | | `Observable` | `observable` | Signal extraction for thresholds/logging | | `Linkable` | `linkable` | Serialization for connectors (MQTT, KNX) | | `Simulatable` | `simulatable` | Generate realistic test data | | `Migratable` | `migratable` | Runtime schema migration | #### Observable — Signal Extraction ```rust use aimdb_data_contracts::Observable; impl Observable for Temperature { type Signal = f32; const ICON: &'static str = "🌡️"; const UNIT: &'static str = "°C"; fn signal(&self) -> Self::Signal { self.celsius } } ``` #### Linkable — Connector Serialization ```rust use aimdb_data_contracts::Linkable; impl Linkable for Temperature { fn from_bytes(data: &[u8]) -> Result { serde_json::from_slice(data).map_err(|e| e.to_string()) } fn to_bytes(&self) -> Result, String> { serde_json::to_vec(self).map_err(|e| e.to_string()) } } ``` #### Simulatable — Test Data Generation ```rust use aimdb_data_contracts::{Simulatable, SimulationConfig}; impl Simulatable for Temperature { fn simulate( config: &SimulationConfig, previous: Option<&Self>, rng: &mut R, timestamp: u64, ) -> Self { let base = previous.map(|p| p.celsius).unwrap_or(20.0); let delta = rng.gen_range(-0.5..0.5); Self { celsius: base + delta, timestamp } } } ``` #### Schema Versioning | Change Type | Allowed? | Approach | |-------------|----------|----------| | Add optional field | ✅ Yes | `#[serde(default)]` | | Add field with default | ✅ Yes | Deserializes to default | | Remove unused field | ✅ Yes | Old data still parses | | Rename field | ⚠️ Migration | Use `Migratable` trait | | Change field type | ⚠️ Migration | Use `Migratable` trait | #### Migratable — Runtime Migration ```rust use aimdb_data_contracts::{Migratable, MigrationError}; use serde_json::Value; impl SchemaType for Temperature { const NAME: &'static str = "temperature"; const VERSION: u32 = 2; } impl Migratable for Temperature { fn migrate(raw: &mut Value, from_version: u32) -> Result<(), MigrationError> { if from_version < 2 { if let Some(v) = raw.get("temp").cloned() { raw["celsius"] = v; raw.as_object_mut().unwrap().remove("temp"); } } Ok(()) } } ``` #### Portable by Design The same contract works across all runtimes: ``` ┌─────────────────────────────────────────────────────────────┐ │ Temperature Contract │ ├─────────────────────────────────────────────────────────────┤ │ MCU (Embassy) │ Edge (Tokio) │ Cloud (Tokio/K8s) │ │ no_std + alloc │ std │ std │ │ 264KB RAM │ Full featured │ Full featured │ └─────────────────────────────────────────────────────────────┘ ``` #### Best Practices 1. Use explicit units — Document units in field names or comments 2. Prefer primitives — `f32`, `u64`, `bool` are portable everywhere 3. Avoid `String` on MCUs — Use `heapless::String` for `no_std` 4. Version your contracts — Increment `VERSION` when schema changes 5. Implement `Observable` — Enables consistent logging and monitoring --- ### Connectors Connectors bridge AimDB records to external protocols like MQTT, KNX, HTTP and more. They handle serialization, transport and protocol-specific details automatically. #### Available Connectors | Protocol | Crate | Status | Platforms | |----------|-------|--------|-----------| | MQTT | `aimdb-mqtt-connector` | ✅ Ready | std, no_std | | KNX | `aimdb-knx-connector` | ✅ Ready | std, no_std | | HTTP/REST | — | 🔨 Building | std | | Kafka | — | 📋 Planned | std | | Modbus | — | 📋 Planned | std, no_std | #### MQTT Connector ```toml [dependencies] aimdb-mqtt-connector = { version = "0.4", features = ["tokio-runtime"] } ``` Publishing Records (Outbound): ```rust use aimdb_core::{AimDbBuilder, buffer::BufferCfg}; use aimdb_tokio_adapter::{TokioAdapter, TokioRecordRegistrarExt}; use aimdb_mqtt_connector::MqttConnector; use std::sync::Arc; let runtime = Arc::new(TokioAdapter); let mut builder = AimDbBuilder::new() .runtime(runtime) .with_connector(MqttConnector::new("mqtt://broker.local:1883") .with_client_id("sensor-01")); builder.configure::("sensor::Temperature", |reg| { reg.buffer(BufferCfg::SingleLatest) .link_to("mqtt://sensors/temperature") .with_serializer(|temp: &Temperature| { serde_json::to_vec(temp) .map_err(|_| aimdb_core::connector::SerializeError::InvalidData) }) .finish(); }); ``` Subscribing to Topics (Inbound): ```rust builder.configure::("command::Temperature", |reg| { reg.buffer(BufferCfg::SpmcRing { capacity: 10 }) .link_from("mqtt://commands/temperature") .with_deserializer(|data: &[u8]| { serde_json::from_slice(data).ok() }) .finish(); }); ``` #### KNX Connector For building automation systems using KNX/IP: ```rust use aimdb_knx_connector::KnxConnector; let mut builder = AimDbBuilder::new() .runtime(runtime) .with_connector(KnxConnector::new("knx://192.168.1.100:3671")); builder.configure::("light::State", |reg| { reg.buffer(BufferCfg::SingleLatest) .link_from("knx://1/0/7") .with_deserializer(|data: &[u8]| { let is_on = data.get(0).map(|&b| b != 0).unwrap_or(false); Some(LightState { is_on }) }) .finish() .link_to("knx://1/0/8") .with_serializer(|state: &LightState| { Ok(vec![if state.is_on { 1 } else { 0 }]) }) .finish(); }); ``` --- ### Deployment AimDB runs on three tiers: MCUs, edge gateways and cloud. The same data contracts work everywhere—only the runtime adapter changes. #### Platform Overview | Target | Runtime | Features | Memory | |--------|---------|----------|--------| | ARM Cortex-M (STM32H5, STM32F4) | Embassy | `no_std`, async | ~50KB+ | | Linux Edge Devices | Tokio | Full `std` | ~10MB+ | | Containers/K8s | Tokio | Full `std` | ~10MB+ | #### MCU Deployment (STM32 + Embassy) Targets the STM32H563ZI Nucleo board with Ethernet, using the KNX connector for building automation. Requirements: - Rust 1.90+ with `thumbv8m.main-none-eabihf` target - probe-rs for flashing - STM32H563ZI Nucleo board (or similar with Ethernet) ```toml [dependencies] aimdb-core = { version = "0.4", default-features = false, features = ["alloc", "derive"] } aimdb-embassy-adapter = { version = "0.4", default-features = false, features = [ "embassy-runtime", "embassy-task-pool-16", ] } aimdb-knx-connector = { version = "0.4", default-features = false, features = [ "embassy-runtime", "defmt", ] } embassy-stm32 = { version = "0.2", features = ["stm32h563zi", "time-driver-any", "exti"] } embassy-executor = { version = "0.7", features = ["arch-cortex-m", "executor-thread"] } embassy-time = { version = "0.4" } embassy-net = { version = "0.6", features = ["tcp", "dhcpv4", "medium-ethernet"] } ``` ```rust #![no_std] #![no_main] extern crate alloc; use aimdb_core::AimDbBuilder; use aimdb_embassy_adapter::{EmbassyAdapter, EmbassyBufferType, EmbassyRecordRegistrarExt}; use aimdb_knx_connector::embassy_client::KnxConnectorBuilder; use embassy_executor::Spawner; use embassy_time::{Duration, Timer}; #[embassy_executor::main] async fn main(spawner: Spawner) { let stack = init_network(&spawner).await; let runtime = alloc::sync::Arc::new( EmbassyAdapter::new_with_network(spawner, stack) ); let mut builder = AimDbBuilder::new() .runtime(runtime) .with_connector(KnxConnectorBuilder::new("knx://192.168.1.19:3671")); builder.configure::("temp::living_room", |reg| { reg.buffer_sized::<4, 2>(EmbassyBufferType::SingleLatest) .link_from("knx://9/1/0") .with_deserializer(|data| decode_dpt9(data)) .finish(); }); let db = builder.build().await.unwrap(); info!("✅ AimDB running on STM32"); loop { Timer::after(Duration::from_secs(1)).await; } } ``` Flashing: ```bash cargo build --release probe-rs run --chip STM32H563ZITx target/thumbv8m.main-none-eabihf/release/my-app ``` Task Pool Configuration: - `embassy-task-pool-8` — Simple applications (1-2 connectors) - `embassy-task-pool-16` — Medium complexity (recommended for KNX/MQTT) - `embassy-task-pool-32` — Complex multi-sensor setups #### Edge Deployment (Tokio) For Linux-based edge devices (x86_64, ARM64): ```toml [dependencies] aimdb-core = { version = "0.4", features = ["std", "derive"] } aimdb-tokio-adapter = { version = "0.4", features = ["tokio-runtime", "tracing"] } aimdb-knx-connector = { version = "0.4", features = ["tokio-runtime"] } serde = { version = "1.0", features = ["derive"] } tokio = { version = "1", features = ["full"] } ``` ```rust use aimdb_core::{AimDbBuilder, buffer::BufferCfg}; use aimdb_tokio_adapter::{TokioAdapter, TokioRecordRegistrarExt}; use aimdb_knx_connector::KnxConnector; use std::sync::Arc; #[tokio::main] async fn main() -> Result<(), Box> { tracing_subscriber::fmt::init(); let runtime = Arc::new(TokioAdapter); let mut builder = AimDbBuilder::new() .runtime(runtime) .with_connector(KnxConnector::new("knx://192.168.1.19:3671")); builder.configure::("temp::living_room", |reg| { reg.buffer(BufferCfg::SingleLatest) .link_from("knx://9/1/0") .with_deserializer(|data| decode_dpt9(data)) .finish(); }); let _db = builder.build().await?; std::future::pending::<()>().await; Ok(()) } ``` #### Cloud Deployment (Kubernetes) Dockerfile: ```dockerfile FROM rust:1.85 as builder WORKDIR /app COPY . . RUN cargo build --release FROM debian:bookworm-slim RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* COPY --from=builder /app/target/release/my-cloud-app /usr/local/bin/ CMD ["my-cloud-app"] ``` Kubernetes Deployment: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: aimdb-service spec: replicas: 3 selector: matchLabels: app: aimdb-service template: metadata: labels: app: aimdb-service spec: containers: - name: aimdb image: myregistry/aimdb-service:latest env: - name: MQTT_BROKER value: "mqtt://mqtt-broker:1883" - name: RUST_LOG value: "info,aimdb_core=debug" resources: requests: memory: "64Mi" cpu: "100m" limits: memory: "256Mi" cpu: "500m" ``` #### Environment Configuration | Variable | Description | Default | |----------|-------------|---------| | `MQTT_BROKER` | MQTT broker URL | `mqtt://localhost:1883` | | `KNX_GATEWAY` | KNX/IP gateway URL | `knx://192.168.1.19:3671` | | `RUST_LOG` | Logging verbosity | `info` | --- ## Blog Posts ### What the Graph Owes: Connectors That Drive Outputs (2026-05-18) Some connectors carry consequence — a `link_from` on a SingleLatest buffer isn't observing the world, it's reaching into it. When the next tap is going to act irreversibly, the graph owes that consumer three things: freshness (the instruction wasn't already obsolete), ordering (newer instructions supersede older ones), and at-most-once-applied (the same instruction doesn't fire twice). AimDB's answer: **the buffer handles freshness and ordering; the connector handles delivery; the record schema handles idempotency; and confirmation is another record.** SingleLatest gives you freshness and ordering as a buffer property — latest instruction wins, older ones never resurface, no queue stacks up while the actuator catches up. QoS 1 on the connector plus a request ID in the record schema is the recommended pattern for actuation (QoS 2's four-way handshake is real latency; idempotent application achieves the same guarantee for less). Confirmation — whether the actuator succeeded — is modelled as a separate outbound record (`SetpointApplied`) published via `link_to`, carrying the request ID, actual outcome and timestamp. Anyone who needs to react subscribes the same way they subscribe to anything else. The runtime doesn't grow a hidden ack channel; the existing primitives compose. --- ### Record Ownership: Which Side Is Right? (2026-05-10) When the same record lives on two sides of a connector — produced on a microcontroller, consumed in a browser — a question that didn't exist in-process now matters: which side is right? AimDB's answer comes from the buffer choice you already made. **SPMC Ring** keeps a bounded history per side; nothing is shared, so there's no conflict to resolve. **SingleLatest** shares one slot — two writers can overwrite each other. **Mailbox** shares one pending instruction — two senders can contradict. The buffer tells you what kind of conflict is even possible. The runtime's stance: **every record key has exactly one authoritative producer; everyone else is a subscriber.** `link_to` declares "I produce this here," `link_from` declares "I observe this from elsewhere." Both on the same key on the same node is a misconfiguration (today the runtime allows it, but a v1 lint will warn). ```rust // Sensor node — owner of the reading builder.configure::(ClimateKey::Reading, |reg| { reg.buffer(BufferCfg::SpmcRing { capacity: 256 }) .source(read_temperature_loop) .link_to("mqtt://climate/zone-a/reading") .with_serializer_raw(Temperature::to_bytes) .finish(); }); // Browser dashboard — observer of the same key builder.configure::(ClimateKey::Reading, |reg| { reg.buffer(BufferCfg::SingleLatest) .link_from("mqtt://climate/zone-a/reading") .with_deserializer_raw(Temperature::from_bytes) .tap(render_chart) .finish(); }); ``` What the runtime carries today: WebSocket protocol attaches a server-side timestamp (`ts: u64`) to every `Data` frame. MQTT preserves whatever's in the payload — so timestamps and sequence numbers live in your record schema if you need them. KNX is wire-compact and lossy by design. What's *not* there yet: a generic record-level version field, a monotonic sequence, an origin tag. Those are likely v1 additions; for now they live in the schema. When ownership-by-key isn't enough — multiple operators each adjusting a setpoint, two browsers editing the same configuration — AimDB does not ship CRDTs (a deliberate non-goal for v1). The pattern that works without them is to **model the conflict as two records**: one request stream per writer (each owned by its source), one applied stream owned by an arbiter that consumes all of them. The graph stays a DAG; ownership stays singular per key; the conflict becomes a transform, not a merge inside a buffer. The seam is the owner's choice. The buffer choice tells you what disagreement is possible; the topology tells the runtime which side is right. The work is up front, in the wiring, where you can see it — not in a per-record conflict-resolution loop down the line. --- ### Connectors: Where AimDB Meets the Real World (2026-05-03) A reactive pipeline moves records from a source through transforms to a tap — a complete story inside the runtime. Connectors are what happens when that story crosses a process boundary. Connectors are the edges of the dataflow graph, not an integration layer. Two methods cover both directions: `link_to` publishes records out, `link_from` subscribes records in. Same builder, same record type, opposite direction. The buffer type already encodes what kind of edge you're building. **SPMC Ring + connector** is a bounded stream that crosses the wire — telemetry out, events in, with the same backpressure semantics as the in-process buffer. **SingleLatest + connector** is mirrored state — configuration from a central authority, UI state reflected from a service, feature flags propagated everywhere. **Mailbox + connector** is instructions arriving from elsewhere — device commands, setpoints, actuation requests. Three buffers, two directions, six kinds of edge. ```rust // Outbound: stream readings as they happen builder.configure::(ClimateKey::Reading, |reg| { reg.buffer(BufferCfg::SpmcRing { capacity: 256 }) .source(|ctx, producer| async move { /* read sensor */ }) .link_to("mqtt://climate/zone-a/reading") .with_serializer_raw(Temperature::to_bytes) .finish(); }); // Inbound: receive setpoints — latest instruction wins builder.configure::(ClimateKey::Setpoint, |reg| { reg.buffer(BufferCfg::Mailbox) .link_from("mqtt://climate/zone-a/setpoint") .with_deserializer_raw(Setpoint::from_bytes) .tap(apply_setpoint) .finish(); }); ``` AimDB ships connectors for MQTT, WebSocket and KNX today, with the roster growing toward industrial and event-streaming protocols. The `weather-mesh-demo` shows a full three-sensor mesh wired entirely with `link_to`/`link_from`. --- ### Reactive Pipelines: The Engine of Data-First Architecture (2026-04-26) Data-first architecture says the data model is the architecture. Reactive pipelines are the execution model that makes it run. Most pipelines today are imperative: a scheduler wakes up, code fetches inputs, a transformer runs, an output is written. Data is passive; code does the asking. A reactive pipeline inverts this — data arrives, code reacts, outputs propagate. Nothing polls. The presence of a new record is the trigger. AimDB's reactive pipeline has three operators acting on typed buffers: **Source** — a producer that pushes new records into the graph. Sensor readings, checkout events, inbound MQTT messages. Sources are the only nodes without inputs. **Transform** — a function from one record type to another, validated against the type graph at compile time. A transform fires when its input buffer receives a new value, not when something asks for output. **Tap** — a side-effecting consumer. Logging, alerting, persistence, network publication. Taps observe the graph without re-injecting into it. ```rust builder.configure::(AlertKey::Checkout, |reg| { reg.buffer(BufferCfg::SpmcRing { capacity: 256 }) .transform::(CheckoutKey::Event, |t| t.map(to_alert)) .tap(alert_tap) .finish(); }); ``` Three properties follow without effort: causality is explicit (every output traces to an input through typed transforms), backpressure is structural (buffer policies are part of the schema, not a tuning layer), and distribution is incidental (a pipeline that runs in-process runs identically when one of its edges crosses an MQTT connector). --- ### The Next Era of Software Architecture Is Data-First (2026-04-14) *And AimDB is built to drive it.* Software architecture has always been organized around behavior. You design services, write functions, define APIs. Data is a consequence — something that gets passed around, stored and occasionally analyzed. Observability is instrumented on top. Experimentation is bolted to the side. Analytics are a separate system entirely. This isn't a tooling problem. It's a paradigm problem. We've been building systems where **code is primary and data is a side effect**. The next era inverts that relationship. In a data-first architecture, the data model *is* the architecture. You don't design services and then figure out what data they produce. You define the records that matter — what they contain, how they flow, who produces them, who consumes them — and the system's behavior emerges from that definition. AimDB's core insight is deceptively simple: **all data movement fits one of three patterns**. **SPMC Ring** — for high-frequency streams where multiple consumers need independent reads. Sensor telemetry. Interaction events. Logs. **SingleLatest** — for state where only the current value matters. Feature flags. Configuration. UI state. **Mailbox** — for commands where the latest instruction wins. Device control. RPC. Actuation. These aren't IoT-specific patterns. They're **universal**. Every data-driven system — from a $2 `no_std` Rust microcontroller to a distributed Kubernetes service — is built from these three primitives. AimDB just makes them explicit, typed and portable across the entire hardware stack. ```rust #[derive(RecordKey, Clone, Copy, PartialEq, Eq, Debug)] #[key_prefix = "events.checkout."] enum CheckoutKey { #[key = "step"] #[link_address = "mqtt://events/checkout/step"] Step, } builder.configure::(CheckoutKey::Step, |reg| { reg.buffer(BufferCfg::SpmcRing { capacity: 1000 }) .source(|ctx, producer| async move { /* emit checkout events */ }) .link_to("mqtt://events/checkout/step") .with_serializer(|e| e.to_bytes()) .finish(); }); ``` Every AimDB deployment has an implicit record graph: nodes are typed records, edges are producer and consumer relationships and buffer types are edge semantics. This graph *is* your system's architecture. --- ### Long-Term Record History: Persistence Comes to AimDB (2026-02-21) AimDB has always been built around live data: sensors flow in from MCU and edge devices, transforms derive new values, taps fan out to consumers — all in memory, all in real time. But real-world applications need more than the latest value. That's what **long-term record history** adds. The persistence layer is split across two crates: - **`aimdb-persistence`** — the traits and extension methods - **`aimdb-persistence-sqlite`** — a concrete SQLite implementation The backend trait: ```rust pub trait PersistenceBackend: Send + Sync { fn store<'a>(&'a self, record_name: &'a str, value: &'a Value, timestamp: u64) -> BoxFuture<'a, Result<(), PersistenceError>>; fn query<'a>(&'a self, record_pattern: &'a str, params: QueryParams) -> BoxFuture<'a, Result, PersistenceError>>; fn cleanup(&self, older_than: u64) -> BoxFuture<'_, Result>; } ``` Enabling persistence: ```rust let backend = Arc::new(SqliteBackend::new("./data/history.db")?); let mut builder = AimDbBuilder::new() .runtime(runtime) .with_persistence(backend, Duration::from_secs(7 * 24 * 3600)); builder.configure::("accuracy::vienna", |reg| { reg.buffer(BufferCfg::SpmcRing { capacity: 500 }) .source(accuracy_producer) .persist("accuracy::vienna"); // ← one call, that's it }); ``` Querying historical data: ```rust use aimdb_persistence::AimDbQueryExt; let latest: Vec = db.query_latest("accuracy::*", 5).await?; let history: Vec = db .query_range("accuracy::vienna", start, end) .await?; ``` Key features: - Opt-in per record with `.persist("key")` - Any `T: Serialize` works - Pattern queries with wildcards - Full time-range results (no implicit row cap) - Pluggable backend (SQLite, Postgres, TimescaleDB) - Automatic retention cleanup on startup + every 24 hours - AimX protocol integration for remote queries --- ### Source, Tap, Transform: How Data Flows Through AimDB's Rust Dataflow Engine (2026-02-09) Every AimDB application is built from three Rust dataflow primitives: `.source()`, `.tap()`, and `.transform()`. | Primitive | Direction | Purpose | |-----------|-----------|---------| | `.source()` | In | Produces values into a record | | `.tap()` | Out | Observes values from a record | | `.transform()` | In ← In | Derives a record's values from another record | `.source()` — the origin of data for a record: ```rust builder.configure::(TempKey::Berlin, |reg| { reg.buffer(BufferCfg::SpmcRing { capacity: 100 }) .source(|ctx, producer| async move { let mut interval = ctx.runtime.interval(Duration::from_secs(30)); loop { interval.tick().await; let reading = read_sensor().await; let _ = producer.produce(reading).await; } }); }); ``` `.tap()` — a read-only observer: ```rust reg.tap(|ctx, consumer| async move { let mut reader = consumer.subscribe().unwrap(); while let Ok(value) = reader.recv().await { println!("Got: {:?}", value); } }); ``` `.transform()` — derives one record from another: ```rust builder.configure::(FahrenheitKey::Berlin, |reg| { reg.buffer(BufferCfg::SpmcRing { capacity: 100 }) .transform::(TempKey::Berlin, |t| { t.map(|temp| Some(Fahrenheit { value: temp.celsius * 9.0 / 5.0 + 32.0, timestamp: temp.timestamp, })) }); }); ``` A record gets its values from **either** a `.source()` **or** a `.transform()`, never both. The dependency graph (DAG) is validated at build time, catching missing inputs and cyclic dependencies before the application starts. --- ### Data Contracts: A Deep Dive (2026-01-20) Data contracts are the foundation of AimDB's cross-platform Rust capabilities. They define a single source of truth using Rust traits. ```rust #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Humidity { pub percent: f32, pub timestamp: u64, } impl SchemaType for Humidity { const NAME: &'static str = "humidity"; const VERSION: u32 = 1; } ``` The trait system provides portable serialization, type-safe operations, and schema identity. Schema evolution is supported through the `Migratable` trait for breaking changes, and `#[serde(default)]` for additive changes. Feature-based capabilities: `linkable` (connector serialization), `simulatable` (test data generation), `migratable` (runtime schema migration), `observable` (signal extraction). --- ### Introducing AimDB: A Rust Dataflow Engine from MCU to Cloud (2026-01-15) AimDB is an async, in-memory Rust runtime designed to stream sensor and state data across microcontrollers, edge gateways and cloud environments using the same `no_std`-compatible API. AimDB introduces **portable data contracts** — Rust schema definitions that work identically whether you're running on an STM32 microcontroller or a Kubernetes cluster. ```rust #[derive(Contract)] struct Temperature { sensor_id: u32, celsius: f32, timestamp: u64, } ``` This same contract compiles for `no_std` embedded targets (Cortex-M4, STM32) and standard Rust on Linux or Kubernetes, letting you: 1. Define once — Write your data schema in one place 2. Deploy anywhere — Run the same logic on MCU, Edge or Cloud 3. Transform at the edge — Process data where it makes sense --- ### Two Ways In: AimDB's Sync and Async APIs (2026-02-15) AimDB's core is async. Every producer, consumer and connector runs as a lightweight task on an async runtime — Tokio on the edge, Embassy on an MCU. But not every caller lives in an async world. Legacy codebases, FFI boundaries, test harnesses and simple scripts all expect blocking calls. AimDB ships two first-class entry points into the same engine: an **async API** for reactive, event-driven systems and a **sync API** that wraps the async core with blocking channels. Same type safety. Same buffer semantics. Different calling conventions. **Async API** (`.build()` / `.run()`): You configure records with `.source()`, `.tap()`, and `.link_*()`, then hand control to the engine. Producers and consumers are plain async functions receiving a `RuntimeContext` and typed `Producer` or `Consumer` handles. **Sync API** (`.attach()` / `.detach()`): Targets callers that can't or don't want to run inside an async context. `.attach()` spawns a dedicated runtime thread and returns a blocking `SyncHandle` that is `Send + Sync`. Producers and consumers use blocking calls: `producer.set()`, `consumer.get()`, `consumer.try_get()`, `consumer.get_with_timeout()`. | Scenario | API | |----------|-----| | Event-driven service, already on Tokio | Async | | Embedded MCU with Embassy | Async | | C/C++ FFI calling into Rust | Sync | | Legacy codebase, no async runtime | Sync | | Quick script or CLI tool | Sync | | Test harness with blocking assertions | Sync | Both APIs configure the same dataflow graph. The engine doesn't know or care which entry point was used. --- ### AimDB Meets the Browser: Full Dataflow Engine in WebAssembly (2026-02-27) The `aimdb-wasm-adapter` crate brings the full AimDB engine — buffers, typed records, producer-consumer patterns — to `wasm32-unknown-unknown` in the browser. Contract validation happens in Rust, inside WASM, before JavaScript ever sees the data. The adapter implements the same executor traits (`RuntimeAdapter`, `Spawn`, `TimeOps`, `Logger`) as Tokio and Embassy adapters. Buffers use `Rc>` instead of atomics for the single-threaded browser environment. The `Streamable` trait and `dispatch_streamable!` macro provide type-erased dispatch at the WASM boundary. TypeScript never defines data types — Rust contracts are the single source of truth. **TypeScript API**: Two-phase lifecycle — configure records by schema name, then build and operate. Every value crossing the JS ↔ WASM boundary passes through Rust serde, enforcing missing fields, wrong types and unknown schemas. **Three operational modes**: Local Only (entirely in-browser), Synchronized (WebSocket bridge to server-side AimDB), Hybrid (local + synced records coexisting). **React Hooks**: `useRecord(key)` subscribes to real-time updates; `useSetRecord(key)` returns a setter. `AimDbProvider` handles WASM initialization and optional WebSocket bridge. --- ### Streamable: One Trait to Cross Every Boundary (2026-03-03) AimDB's `Streamable` trait eliminates parallel type systems at WASM, WebSocket and CLI boundaries. It's a capability marker in `aimdb-data-contracts`: ```rust pub trait Streamable: SchemaType + Serialize + DeserializeOwned + Send + Sync + Clone + Debug + 'static {} ``` The companion `StreamableVisitor` trait and `for_each_streamable()` function form a visitor-based registry — one function that enumerates all streamable types. Every consumer (WASM adapter, WebSocket connector, CLI) implements `StreamableVisitor` to build its dispatch table from the same source of truth. Adding a new contract is four steps: define the struct, implement `SchemaType`, mark `Streamable`, add one `visit` call. All consumers pick up the new type automatically — no codegen, no build step, no TypeScript interface. The approach is explicit and portable. The registry is a function you can read. The compiler verifies every `visit` call matches the `Streamable` bound. --- ### Schema Migration Without Ceremony (2026-03-08) AimDB's migration system makes every version transition a typed, bidirectional Rust function, validated at compile time and dispatched automatically at runtime. **`MigrationStep`** is the smallest unit — a typed conversion between two adjacent versions with `up` and `down` methods. Both directions are required for rollback and cross-version communication. **`migration_chain!`** macro validates the full chain at compile time (sequential versions, chain continuity, anchored bounds) and generates a `MigrationChain` implementation. `migrate_from_bytes` auto-upgrades from any historical version; `migrate_to_version` downgrades for legacy consumers. When a contract implements both `MigrationChain` and `Linkable`, connectors auto-migrate transparently — buffers only hold current-version values. The trade-off: concrete Rust structs per version, but compile-time correctness and explicit documentation of historical wire formats. --- ### AI-Assisted System Introspection: AimDB Meets the Model Context Protocol (2026-03-22) `aimdb-mcp` is a Model Context Protocol server that exposes a running AimDB instance to any MCP-capable AI client. The system's metadata — schema, buffers, dependency graph, observability — is already embedded in records, so the MCP server is an open interface without custom instrumentation. **Three layers of system knowledge:** 1. **Discovery and Records**: `discover_instances`, `list_records`, `get_record`, `query_schema` — find running instances, list records with buffer types and timestamps, retrieve current values and infer JSON Schema from live data. 2. **Graph Introspection**: `graph_nodes`, `graph_edges`, `graph_topo_order` — expose the dependency DAG showing data flow between records, origins (source, link, transform, passive), connector status and initialization order. 3. **Architecture Design**: Architecture agent reads `.aimdb/state.toml`, proposes changes via `resolve_proposal`, and validates against live instances with `validate_against_instance`. The AI is a design partner — proposals require human confirmation. **Decision Memory**: `save_memory` persists design context to `.aimdb/memory.md`, preserving rationale across sessions. Install with `cargo install aimdb-mcp` and configure in `.vscode/mcp.json`. --- ### Portable Async Rust: Targeting Embassy, Tokio and WASM from One Codebase (2026-03-27) AimDB is built on a unified async runtime abstraction that compiles the same typed structs for Cortex-M4 microcontrollers and Linux servers. Records consist of a key and a type; the same producers, consumers, and transforms run on every target — only the runtime adapter selection changes. **Runtime abstraction:** Four traits with zero platform dependencies — `RuntimeAdapter` (identity), `Spawn` (task creation), `TimeOps` (clocks and sleep), and `Logger` (structured output) — glued by a generic `Runtime` trait erased at compile time. Portable functions receive a `RuntimeContext` with `ctx.time()` and `ctx.log()` accessors. **Embassy dynamic spawning:** A pool of `#[embassy_executor::task(pool_size = TASK_POOL_SIZE)]` static task slots accepts boxed futures at runtime, bridging Embassy's static model with AimDB's dynamic registration. **Send + Sync bounds:** Core traits target multi-threaded Tokio; single-threaded adapters (Embassy, WASM) satisfy the bounds with scoped `unsafe impl Send/Sync`, justified by the single-threaded executor contract. Platform selection is a one-line change: `TokioAdapter::new()` for Linux/cloud, `EmbassyAdapter::new()` for Cortex-M4, and the WASM adapter for browsers. --- ## Resources - Website: https://aimdb.dev - Live Demo: https://aimdb.dev/demo - Blog: https://aimdb.dev/blog - Documentation: https://aimdb.dev/docs/01-getting-started - GitHub: https://github.com/aimdb-dev/aimdb - Issues: https://github.com/aimdb-dev/aimdb/issues - Discussions: https://github.com/aimdb-dev/aimdb/discussions - RSS Feed: https://aimdb.dev/feed.xml