Part 5 of 8

Batteries included: an embedded FalkorDB you can cargo run

Every "getting started" guide has the same speed bump on line one: first, run a server. It's a small ask, but it's friction at the exact moment someone is deciding whether your library is worth their afternoon. So we removed it. With the embedded feature, the client can start a real FalkorDB for you, in-process, and shut it down when you're done.

What "embedded" actually does

The embedded server spins up redis-server as a child process with the FalkorDB module loaded, waits until it answers PING, hands you a connected client, and tears the whole thing down on drop. The interesting part is where the module comes from.

sequenceDiagram
    autonumber
    participant App
    participant E as EmbeddedServer
    participant Cache as module cache / build-time bundle
    participant R as redis-server (child process)
    participant DB as FalkorDB module
    App->>E: build() with FalkorConnectionInfo::Embedded
    E->>R: check `redis-server --version` (must be >= 8.0)
    E->>Cache: resolve falkordb.so (cached / downloaded / embedded bytes)
    Cache-->>E: module path
    E->>R: spawn redis-server --loadmodule falkordb.so
    E->>R: poll PING until ready
    R-->>E: PONG
    E-->>App: connected client
    Note over App,DB: on drop, the child server is shut down for you
Resolve the module, spawn the server, wait for ready, hand back a client.

There are three ways the falkordb.so module gets resolved, in increasing order of "I have no network and I like it that way":

  1. Auto-download (default). If the module isn't already present, it's fetched from the official releases, verified against a pinned SHA-256 checksum, and cached locally. First run pays the download; every run after is instant. The redis-server binary itself is not downloaded — it's found on your PATH — so this leans on a tool you already have rather than smuggling in a binary.
  2. Already-installed / offline. Point at an existing module, or disable downloading and let it search common system locations. No network at all.
  3. Build-time bundle. Enable the embedded-bundle feature instead, and a build.rs fetches the module for your build target at compile time and bakes it into your binary with include_bytes!. The running process then needs zero network — it extracts the embedded bytes and goes. This is the one for air-gapped and network-isolated deployments, and it's why the feature exists.

Here's the real examples/embedded_usage.rs — note that the usage is identical regardless of how the module is sourced; only the EmbeddedConfig differs:

/*
 * Copyright FalkorDB Ltd. 2023 - present
 * Licensed under the MIT License.
 */

//! Example demonstrating the embedded FalkorDB server feature.
//!
//! This example shows how to use FalkorDB with an embedded Redis server
//! that has the FalkorDB module loaded. This is useful for:
//! - Testing without external dependencies
//! - Embedded applications
//! - Quick prototyping
//!
//! # Self-Contained Mode
//!
//! The embedded server can auto-provision the FalkorDB **module** via the
//! `auto_download` option (enabled by default). When enabled, a missing
//! `falkordb.so` is downloaded from the official FalkorDB releases, verified
//! against a pinned SHA-256 checksum and cached locally, so the embedded server
//! "just works" without a pre-installed module.
//!
//! The `redis-server` binary is **not** downloaded — it is located on the host
//! via `PATH` or `redis_server_path`. Install it from your package manager
//! (e.g. `brew install redis`, `apt-get install redis-server`).
//!
//! # Using an already-installed FalkorDB
//!
//! To run fully offline against binaries already on the machine, either set
//! `falkordb_module_path` explicitly or disable `auto_download` (common system
//! locations are still searched). See `already_installed_config()` below.
//!
//! # Build-time bundle (offline at runtime)
//!
//! For network-isolated deployments, enable the `embedded-bundle` feature instead
//! of `embedded`. A `build.rs` fetches the module for the build target at compile
//! time and embeds it in the binary, so the running process needs **no network**.
//! The same `EmbeddedConfig` / API shown here works unchanged; only the module
//! source differs. Run it with:
//! ```bash
//! cargo run --example embedded_usage --features embedded-bundle
//! ```
//!
//! # Platform-Specific Requirements
//!
//! ## macOS
//! The FalkorDB module requires OpenMP (libomp) on macOS. If not already installed via
//! Homebrew, install it with: `brew install libomp`
//!
//! # License Note
//!
//! The FalkorDB module binary (downloaded at runtime, or embedded at build time
//! with `embedded-bundle`) is licensed under the Server Side Public License
//! (SSPL) v1. See https://www.falkordb.com/ for licensing details.
//!
//! # Running this example
//! ```bash
//! cargo run --example embedded_usage --features embedded
//! ```

use falkordb::{EmbeddedConfig, FalkorClientBuilder, FalkorConnectionInfo, FalkorResult};

/// Build a config that uses binaries already installed on the machine, without
/// any network access: redis-server is taken from `PATH` and the FalkorDB
/// module from the given path (or common system locations when left as `None`).
#[allow(dead_code)]
fn already_installed_config() -> EmbeddedConfig {
    EmbeddedConfig {
        // Disable downloading: resolve only explicit/system binaries.
        auto_download: false,
        // Optionally pin an installed module; `None` searches system locations.
        falkordb_module_path: None,
        ..Default::default()
    }
}

fn main() -> FalkorResult<()> {
    println!("Starting embedded FalkorDB example...");

    // Default config: download + cache the FalkorDB module if missing, and find
    // redis-server on PATH. Swap in `already_installed_config()` to run offline
    // against an existing install instead.
    let embedded_config = EmbeddedConfig::default();

    // You can also customize the configuration:
    // let embedded_config = EmbeddedConfig {
    //     redis_server_path: Some(PathBuf::from("/path/to/redis-server")),
    //     falkordb_module_path: Some(PathBuf::from("/path/to/falkordb.so")),
    //     db_dir: Some(PathBuf::from("/tmp/my_falkordb")),
    //     db_filename: "mydb.rdb".to_string(),
    //     socket_path: Some(PathBuf::from("/tmp/falkordb.sock")),
    //     start_timeout: Duration::from_secs(10),
    //     auto_download: true,
    //     falkordb_version: None, // or Some("v4.18.10".to_string())
    //     cache_dir: None,        // or Some(PathBuf::from("/tmp/fk-cache"))
    // };

    println!("Creating client with embedded FalkorDB server...");

    // Build a client with embedded FalkorDB
    let client = FalkorClientBuilder::new()
        .with_connection_info(FalkorConnectionInfo::Embedded(embedded_config))
        .build()?;

    println!("Connected to embedded FalkorDB server!");

    // Now use the client normally
    let mut graph = client.select_graph("social");

    // Create some nodes
    println!("Creating nodes...");
    graph
        .query("CREATE (:Person {name: 'Alice', age: 30})")
        .execute()?;
    graph
        .query("CREATE (:Person {name: 'Bob', age: 25})")
        .execute()?;
    graph
        .query("CREATE (:Person {name: 'Charlie', age: 35})")
        .execute()?;

    // Create relationships
    println!("Creating relationships...");
    graph
        .query(
            "MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'}) CREATE (a)-[:KNOWS]->(b)",
        )
        .execute()?;
    graph
        .query(
            "MATCH (b:Person {name: 'Bob'}), (c:Person {name: 'Charlie'}) CREATE (b)-[:KNOWS]->(c)",
        )
        .execute()?;

    // Query the graph
    println!("\nQuerying the graph:");
    let result = graph
        .query("MATCH (p:Person) RETURN p.name, p.age ORDER BY p.age")
        .execute()?;

    println!("Found {} results:", result.data.len());
    for row in result.data {
        println!("  {:?}", row);
    }

    // Query with relationships
    println!("\nQuerying relationships:");
    let result = graph
        .query("MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name, b.name")
        .execute()?;

    println!("Found {} relationships:", result.data.len());
    for row in result.data {
        println!("  {:?}", row);
    }

    // Clean up
    println!("\nDeleting graph...");
    graph.delete()?;

    println!("Example completed successfully!");
    println!("The embedded server will be automatically shut down when the client is dropped.");

    Ok(())
}

Source: examples/embedded_usage.rs — compiled in CI.

The detail I'm quietly proud of

Two, actually.

First, the embedded server now pre-flights redis-server --version and fails early, with a clear message, if it's older than the 8.0 the module requires. The failure mode it replaces — a server that starts and then mysteriously can't load the module — is exactly the kind of thing that eats an hour of someone's day. Fail fast, fail legible.

Second, the build-time bundle is genuinely hermetic. The checksum verification on the download path isn't decoration: a graph module is native code you're loading into your process, and "download some bytes and run them" without verifying them is how you end up in an incident review. Pinned version, pinned checksum, or embedded-at-build-time bytes. Pick your paranoia level.

Why this matters more for a graph database

Graph code is fiddly to test. You want a real engine — Cypher semantics, real planning, real results — not a mock that lies to you. Embedded mode means your integration tests boot an actual FalkorDB per run with no external setup, and your CI doesn't need a service container just to check that a MATCH returns what you think. It collapses the distance between "I have an idea" and "I ran it against the real thing" to a single cargo run. That distance is where adoption lives or dies.

Next: the values inside those nodes. Part 6 is about mapping rows straight into your structs with serde, and decoding temporal values into types with actual arithmetic.