🦁 BuffDB
BuffDB is a lightweight, high-performance embedded database with networking capabilities, designed for edge computing and offline-first applications. Built in Rust with <2MB binary size.
⚠️ Experimental: This project is rapidly evolving. If you are trying it out and hit a roadblock, please open an issue.
Key Features
- High Performance - Optimized for speed with SQLite backend
- gRPC Network API - Access your database over the network
- Key-Value Store - Fast key-value operations with streaming support
- BLOB Storage - Binary large object storage with metadata
- Secondary Indexes - Hash and B-tree indexes for value-based queries
- Raw SQL Queries - Execute SQL directly on the underlying database
- Tiny Size - Under 2MB binary with SQLite backend
- Pure Rust - Safe, concurrent, and memory-efficient
🚀 Quick Start
Prerequisites
BuffDB requires protoc (Protocol Buffers compiler):
# Ubuntu/Debian
# macOS
# Windows
macOS Setup
macOS users need additional dependencies due to linking requirements:
# Install required dependencies
# Clone the repository
# The project includes a .cargo/config.toml that sets up the correct paths
# If you still encounter linking errors, you can manually set:
Building and Running
Option 1: Install from crates.io
Option 2: Build from source
# Build with all features (includes all backends)
# Run the server
# Or run directly with cargo
Option 3: Quick development build
# For development with faster compilation
Language Examples
use ;
use ;
use ;
use Channel;
use StreamExt;
use serde_json;
use chrono;
async
Add to Cargo.toml:
[]
= "0.5"
= { = "1", = ["full"] }
= "0.12"
= "0.3"
= "1.0"
= "0.4"
= "0.1"
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
// Load proto definitions
const kvProto = protoLoader.loadSync('kv.proto');
const blobProto = protoLoader.loadSync('blob.proto');
const kvDef = grpc.loadPackageDefinition(kvProto).buffdb.kv;
const blobDef = grpc.loadPackageDefinition(blobProto).buffdb.blob;
// Connect to BuffDB
const kvClient = new kvDef.Kv('[::1]:9313', grpc.credentials.createInsecure());
const blobClient = new blobDef.Blob('[::1]:9313', grpc.credentials.createInsecure());
// Model metadata interface
interface ModelInfo {
name: string;
version: string;
framework: string;
description: string;
input_shape: number[];
output_shape: number[];
blob_ids: number[];
created_at: string;
parameters: Record<string, string>;
}
// 1. Store ML model
async function storeModel() {
const modelInfo: ModelInfo = {
name: 'bert-base',
version: 'uncased-v1',
framework: 'tensorflow',
description: 'BERT base uncased model',
input_shape: [1, 512], // batch_size, sequence_length
output_shape: [1, 512, 768], // batch_size, sequence_length, hidden_size
blob_ids: [],
created_at: new Date().toISOString(),
parameters: { 'attention_heads': '12', 'hidden_layers': '12' }
};
// Store model weights (simulate with dummy data)
const modelWeights = Buffer.alloc(1024 * 1024); // 1MB dummy weights
// Store weights as blob
const blobStream = blobClient.Store();
const blobId = await new Promise<number>((resolve, reject) => {
blobStream.on('data', (response) => resolve(response.id));
blobStream.on('error', reject);
blobStream.write({
bytes: modelWeights,
metadata: JSON.stringify({
model: modelInfo.name,
version: modelInfo.version,
type: 'weights'
})
});
blobStream.end();
});
// Update model info with blob ID
modelInfo.blob_ids = [blobId];
// Store model metadata
const kvStream = kvClient.Set();
await new Promise<void>((resolve, reject) => {
kvStream.on('end', resolve);
kvStream.on('error', reject);
kvStream.write({
key: `model:${modelInfo.name}:${modelInfo.version}:metadata`,
value: JSON.stringify(modelInfo)
});
kvStream.end();
});
console.log(`Stored model ${modelInfo.name} v${modelInfo.version}`);
return modelInfo;
}
// 2. Load model for inference
async function loadModel(name: string, version: string): Promise<void> {
// Get model metadata
const kvStream = kvClient.Get();
const modelInfo = await new Promise<ModelInfo>((resolve, reject) => {
kvStream.on('data', (response) => {
resolve(JSON.parse(response.value) as ModelInfo);
});
kvStream.on('error', reject);
kvStream.write({ key: `model:${name}:${version}:metadata` });
kvStream.end();
});
console.log(`Loaded model: ${modelInfo.name} v${modelInfo.version}`);
console.log(`Framework: ${modelInfo.framework}`);
console.log(`Output shape: ${modelInfo.output_shape}`);
// Load model weights
for (const blobId of modelInfo.blob_ids) {
const blobStream = blobClient.Get();
const weights = await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
blobStream.on('data', (response) => {
chunks.push(response.bytes);
});
blobStream.on('end', () => {
resolve(Buffer.concat(chunks));
});
blobStream.on('error', reject);
blobStream.write({ id: blobId });
blobStream.end();
});
console.log(`Loaded model weights: ${weights.length} bytes`);
// Here you would load weights into your ML framework (e.g., TensorFlow.js)
}
}
// Run example
async function main() {
await storeModel();
await loadModel('bert-base', 'uncased-v1');
}
main().catch(console.error);
Install dependencies:
# Connect to BuffDB
=
=
=
:
:
:
:
:
:
:
:
:
# 1. Store ML model
=
# Store model weights (simulate with dummy data)
= b * # 1MB dummy weights
# Store weights as blob
=
=
= .
# Update model info with blob ID
=
# Store model metadata
= f
=
return
# 2. Load model for inference
# Get model metadata
= f
=
=
=
# Load model weights
=
=
= .
# Here you would load weights into your ML framework (e.g., PyTorch, TensorFlow)
# Example with PyTorch (pseudo-code):
# import torch
# import io
# buffer = io.BytesIO(weights)
# model_state_dict = torch.load(buffer)
# model.load_state_dict(model_state_dict)
return
# 3. List available models
# Get model index (you would maintain this index)
=
= f
=
=
=
pass # Model not in index
# Run example
# Store a model
=
# List available models
# Load model for inference
=
Install dependencies:
# Generate Python gRPC code from proto files
;
;
;
;
;
;
;
;
;
;
;
;
;
;
Add to build.gradle:
dependencies
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
kvpb "your-module/proto/kv"
blobpb "your-module/proto/blob"
)
// ModelInfo represents ML model metadata
type ModelInfo struct
type MLModelManager struct
// NewMLModelManager creates a new model manager
func NewMLModelManager(addr string) (*MLModelManager, error)
// StoreModel stores an ML model in BuffDB
func (ctx context.Context, info *ModelInfo, weights []byte) error
// LoadModel loads a model from BuffDB
func (ctx context.Context, name, version string) (*ModelInfo, []byte, error)
// ListModels lists available models
func (ctx context.Context) error
func main()
Install dependencies:
# Generate Go code from proto files
import Foundation
import GRPC
import NIO
import SwiftProtobuf
// Model metadata structure
struct ModelInfo: Codable {
let name: String
let version: String
let framework: String
let description: String
let inputShape: [Int]
let outputShape: [Int]
var blobIds: [UInt64]
let createdAt: String
let parameters: [String: String]
enum CodingKeys: String, CodingKey {
case name, version, framework, description
case inputShape = "input_shape"
case outputShape = "output_shape"
case blobIds = "blob_ids"
case createdAt = "created_at"
case parameters
}
}
class MLModelManager {
private let group: EventLoopGroup
private let channel: ClientConnection
private let kvClient: Buffdb_Kv_KvNIOClient
private let blobClient: Buffdb_Blob_BlobNIOClient
init(host: String = "localhost", port: Int = 9313) throws {
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
self.channel = ClientConnection
.insecure(group: group)
.connect(host: host, port: port)
self.kvClient = Buffdb_Kv_KvNIOClient(channel: channel)
self.blobClient = Buffdb_Blob_BlobNIOClient(channel: channel)
}
// 1. Store ML model
func storeModel(info: inout ModelInfo, weights: Data) async throws {
// Store model weights as blob
let blobMetadata = [
"model": info.name,
"version": info.version,
"type": "weights"
]
let metadataJSON = try JSONSerialization.data(withJSONObject: blobMetadata)
var storeRequest = Buffdb_Blob_StoreRequest()
storeRequest.bytes = weights
storeRequest.metadata = String(data: metadataJSON, encoding: .utf8) ?? ""
let blobCall = blobClient.store()
try await blobCall.sendMessage(storeRequest)
let blobResponse = try await blobCall.response.get()
// Update model info with blob ID
info.blobIds.append(blobResponse.id)
// Store model metadata
let encoder = JSONEncoder()
let infoJSON = try encoder.encode(info)
var setRequest = Buffdb_Kv_SetRequest()
setRequest.key = "model:\(info.name):\(info.version):metadata"
setRequest.value = String(data: infoJSON, encoding: .utf8) ?? ""
let kvCall = kvClient.set()
try await kvCall.sendMessage(setRequest)
_ = try await kvCall.response.get()
print("Stored model \(info.name) v\(info.version)")
}
// 2. Load model for inference
func loadModel(name: String, version: String) async throws -> (ModelInfo, Data) {
// Get model metadata
var getRequest = Buffdb_Kv_GetRequest()
getRequest.key = "model:\(name):\(version):metadata"
let kvCall = kvClient.get()
try await kvCall.sendMessage(getRequest)
var modelInfo: ModelInfo?
for try await response in kvCall.responseStream {
let decoder = JSONDecoder()
modelInfo = try decoder.decode(ModelInfo.self, from: response.value.data(using: .utf8)!)
break
}
guard let info = modelInfo else {
throw NSError(domain: "MLModelManager", code: 404,
userInfo: [NSLocalizedDescriptionKey: "Model not found"])
}
print("Loaded model: \(info.name) v\(info.version)")
print("Framework: \(info.framework)")
print("Output shape: \(info.outputShape)")
// Load model weights
var weights = Data()
for blobId in info.blobIds {
var blobRequest = Buffdb_Blob_GetRequest()
blobRequest.id = blobId
let blobCall = blobClient.get()
try await blobCall.sendMessage(blobRequest)
for try await response in blobCall.responseStream {
weights.append(response.bytes)
}
}
print("Loaded model weights: \(weights.count) bytes")
return (info, weights)
}
deinit {
try? group.syncShutdownGracefully()
}
}
// Usage example
@main
struct MLModelExample {
static func main() async throws {
let manager = try MLModelManager()
// Create model info
var model = ModelInfo(
name: "coreml-resnet",
version: "50-v1",
framework: "coreml",
description: "ResNet50 for iOS devices",
inputShape: [1, 224, 224, 3],
outputShape: [1, 1000],
blobIds: [],
createdAt: ISO8601DateFormatter().string(from: Date()),
parameters: ["quantized": "true", "precision": "float16"]
)
// Store with dummy weights
let dummyWeights = Data(repeating: 0, count: 1024 * 1024) // 1MB
try await manager.storeModel(info: &model, weights: dummyWeights)
// Load model
let (loaded, weights) = try await manager.loadModel(
name: "coreml-resnet",
version: "50-v1"
)
// Here you would load into CoreML
// let mlModel = try MLModel(contentsOf: modelURL)
}
}
Add to Package.swift:
dependencies: [
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.15.0"),
]
import io.grpc.ManagedChannel
import io.grpc.ManagedChannelBuilder
import io.grpc.stub.StreamObserver
import com.google.protobuf.ByteString
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import java.time.Instant
import java.util.concurrent.CountDownLatch
@Serializable
data class ModelInfo(
val name: String,
val version: String,
val framework: String,
val description: String,
@SerialName("input_shape")
val inputShape: List<Int>,
@SerialName("output_shape")
val outputShape: List<Int>,
@SerialName("blob_ids")
var blobIds: MutableList<Long> = mutableListOf(),
@SerialName("created_at")
val createdAt: String = Instant.now().toString(),
val parameters: Map<String, String> = emptyMap()
)
class MLModelManager(host: String = "localhost", port: Int = 9313) {
private val channel: ManagedChannel = ManagedChannelBuilder
.forAddress(host, port)
.usePlaintext()
.build()
private val kvStub = KvGrpc.newStub(channel)
private val blobStub = BlobGrpc.newStub(channel)
private val json = Json { prettyPrint = true }
// 1. Store ML model
suspend fun storeModel(modelInfo: ModelInfo, weights: ByteArray) = coroutineScope {
val latch = CountDownLatch(2)
// Store model weights as blob
val blobObserver = object : StreamObserver<StoreResponse> {
override fun onNext(response: StoreResponse) {
modelInfo.blobIds.add(response.id)
println("Stored blob with ID: ${response.id}")
}
override fun onError(t: Throwable) {
t.printStackTrace()
latch.countDown()
}
override fun onCompleted() {
latch.countDown()
}
}
val blobStream = blobStub.store(blobObserver)
val metadata = mapOf(
"model" to modelInfo.name,
"version" to modelInfo.version,
"type" to "weights"
)
blobStream.onNext(
StoreRequest.newBuilder()
.setBytes(ByteString.copyFrom(weights))
.setMetadata(json.encodeToString(metadata))
.build()
)
blobStream.onCompleted()
// Store model metadata
val metadataKey = "model:${modelInfo.name}:${modelInfo.version}:metadata"
val kvObserver = object : StreamObserver<SetResponse> {
override fun onNext(response: SetResponse) {
println("Stored model metadata: ${response.key}")
}
override fun onError(t: Throwable) {
t.printStackTrace()
latch.countDown()
}
override fun onCompleted() {
latch.countDown()
}
}
val kvStream = kvStub.set(kvObserver)
kvStream.onNext(
SetRequest.newBuilder()
.setKey(metadataKey)
.setValue(json.encodeToString(modelInfo))
.build()
)
kvStream.onCompleted()
withContext(Dispatchers.IO) {
latch.await()
}
println("Stored model ${modelInfo.name} v${modelInfo.version}")
}
// 2. Load model for inference
suspend fun loadModel(name: String, version: String): Pair<ModelInfo, ByteArray> = coroutineScope {
val metadataKey = "model:$name:$version:metadata"
val modelInfoDeferred = CompletableDeferred<ModelInfo>()
// Get model metadata
val kvObserver = object : StreamObserver<GetResponse> {
override fun onNext(response: GetResponse) {
val modelInfo = json.decodeFromString<ModelInfo>(response.value)
modelInfoDeferred.complete(modelInfo)
println("Loaded model: ${modelInfo.name} v${modelInfo.version}")
println("Framework: ${modelInfo.framework}")
println("Output shape: ${modelInfo.outputShape}")
}
override fun onError(t: Throwable) {
modelInfoDeferred.completeExceptionally(t)
}
override fun onCompleted() {}
}
val kvStream = kvStub.get(kvObserver)
kvStream.onNext(GetRequest.newBuilder().setKey(metadataKey).build())
kvStream.onCompleted()
val modelInfo = modelInfoDeferred.await()
// Load model weights
val weights = mutableListOf<ByteArray>()
for (blobId in modelInfo.blobIds) {
val blobDeferred = CompletableDeferred<ByteArray>()
val blobObserver = object : StreamObserver<GetResponse> {
private val chunks = mutableListOf<ByteArray>()
override fun onNext(response: GetResponse) {
chunks.add(response.bytes.toByteArray())
}
override fun onError(t: Throwable) {
blobDeferred.completeExceptionally(t)
}
override fun onCompleted() {
blobDeferred.complete(chunks.flatMap { it.toList() }.toByteArray())
}
}
val blobStream = blobStub.get(blobObserver)
blobStream.onNext(GetRequest.newBuilder().setId(blobId).build())
blobStream.onCompleted()
weights.add(blobDeferred.await())
}
val allWeights = weights.flatMap { it.toList() }.toByteArray()
println("Loaded model weights: ${allWeights.size} bytes")
return@coroutineScope Pair(modelInfo, allWeights)
}
fun shutdown() {
channel.shutdown()
}
}
// Usage example
fun main() = runBlocking {
val manager = MLModelManager()
try {
// Create model info
val model = ModelInfo(
name = "tflite-mobilenet",
version = "v2-224",
framework = "tflite",
description = "MobileNet V2 for Android devices",
inputShape = listOf(1, 224, 224, 3),
outputShape = listOf(1, 1000),
parameters = mapOf(
"quantized" to "true",
"input_mean" to "127.5",
"input_std" to "127.5"
)
)
// Store with dummy weights
val dummyWeights = ByteArray(1024 * 1024) // 1MB
manager.storeModel(model, dummyWeights)
// Load model
val (loaded, weights) = manager.loadModel("tflite-mobilenet", "v2-224")
// Here you would load into TensorFlow Lite
// val interpreter = Interpreter(modelByteBuffer)
} finally {
manager.shutdown()
}
}
Add to build.gradle.kts:
dependencies {
implementation("io.grpc:grpc-kotlin-stub:1.3.0")
implementation("io.grpc:grpc-netty:1.58.0")
implementation("com.google.protobuf:protobuf-kotlin:3.24.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}
Installation
# Install from crates.io
# Docker
# From source
Advanced Features
Secondary Indexes
BuffDB supports creating secondary indexes on values for efficient lookups:
use ;
// Create an index manager
let index_manager = new;
// Create a hash index for exact matches
index_manager.create_index?;
// Create a B-tree index for range queries
index_manager.create_index?;
Raw SQL Queries
Execute SQL directly on the underlying SQLite database:
# Via CLI
# Via gRPC (using the Query service)
BLOB Storage
Store binary data with metadata:
# Store a file
# Retrieve by ID
ML Model Storage
BuffDB's combination of KV store and BLOB storage makes it ideal for storing and serving ML models:
use ;
use ;
use ;
// Store a model with metadata
let model_info = ModelInfo ;
// Store model weights as BLOB
let weights = read?;
let blob_metadata = format_blob_metadata;
let store_request = StoreRequest ;
let blob_id = blob_client.store
.await?
.into_inner
.next
.await
.unwrap?
.id;
// Store model metadata in KV
let metadata_key = metadata_key;
let set_request = SetRequest ;
kv_client.set.await?;
Example Use Cases:
- Edge AI: Deploy models to edge devices with local inference
- Model Versioning: Track multiple versions of models
- A/B Testing: Serve different model versions to different users
- Offline Inference: Run models without network connectivity
See the ml_model_inference example for a complete implementation.
Hugging Face Integration
BuffDB can be used as a caching layer for Hugging Face models, enabling efficient model distribution:
// Download model from Hugging Face and cache in BuffDB
use Client;
async
Benefits:
- Reduced Latency: Serve models from local BuffDB instead of downloading
- Bandwidth Savings: Download once, serve many times
- Offline Support: Models available without internet connection
- Version Control: Track and serve specific model versions
See the huggingface_integration example for complete implementation.
🔧 Configuration
CLI Options
Backends
| Backend | Feature Flag | Performance | Use Case | Status |
|---|---|---|---|---|
| SQLite | vendored-sqlite |
Balanced | General purpose | ✅ Stable |
| DuckDB | duckdb |
Analytics | OLAP workloads | 🚧 Temporarily disabled |
Architecture
BuffDB combines embedded database efficiency with network accessibility:
┌─────────────┐ gRPC ┌─────────────┐
│ Client │ ◄──────────► │ BuffDB │
│ (Any Lang) │ │ Server │
└─────────────┘ └──────┬──────┘
│
┌──────┴──────┐
│ Backend │
│ (SQLite/ │
│ DuckDB) │
└─────────────┘
📊 Performance
- Binary Size: <2MB (SQLite backend)
- Startup Time: <10ms
- Throughput: 100K+ ops/sec (varies by backend)
- Latency: <1ms for local operations
🤝 Contributing
We welcome contributions! See CONTRIBUTING.md for guidelines.
License
Licensed under the Apache License, Version 2.0. See LICENSE for details.
🔧 Backend Support
⚠️ DuckDB Support (Experimental)
DuckDB backend is currently experimental due to:
- Platform-specific linking issues (especially on macOS)
- Incomplete Rust bindings (duckdb/duckdb-rs#368)
- Performance optimizations still in progress
For production use, we recommend SQLite backend which is stable and well-tested.
By default, SQLite backend is included and vendored. To use DuckDB (experimental), enable it with --features duckdb.
Command line interface
You can use buffdb help to see the commands and flags permitted. The following operations are
currently supported:
buffdb run [ADDR], starting the server. The default address is[::1]:9313.buffdb kv get <KEY>, printing the value to stdout.buffdb kv set <KEY> <VALUE>, setting the value.buffdb kv delete <KEY>, deleting the value.buffdb kv eq [KEYS]..., exiting successfully if the values for all provided keys are equal. Exits with an error code if any two values are not equal.buffdb kv not-eq [KEYS]..., exiting successfully if the values for all provided keys are unique. Exits with an error code if any two values are equal.buffdb blob get <ID>, printing the data to stdout. Note that this is arbitrary bytes!buffdb blob store <FILE> [METADATA], storing the file (use-for stdin) and printing the ID to stdout. Metadata is optional.buffdb blob update <ID> data <FILE>, updating the data of the blob. Use-for stdin. Metadata is unchanged.buffdb blob update <ID> metadata [METADATA], updating the metadata of the blob. Data is unchanged. Omitting[METADATA]will set the metadata to null.buffdb blob update <ID> all <FILE> [METADATA], updating both the data and metadata of the blob. For<FILE>, use-for stdin. Omitting[METADATA]will set the metadata to null.buffdb blob delete <ID>, deleting the blob.buffdb blob eq-data [IDS]..., exiting successfully if the blobs for all provided IDs are equal. Exits with an error code if any two blobs are not equal.buffdb blob not-eq-data [IDS]..., exiting successfully if the blobs for all provided IDs are unique. Exits with an error code if any two blobs are equal.
Commands altering a store will exit with an error code if the key/id does not exist. An exception to this is updating the metadata of a blob to be null, as it is not required to exist beforehand.
All commands for kv and blob can use -s/--store to specify which store to use. The defaults
are kv_store.db and blob_store.db respectively. To select a backend, use -b/--backend. The
default varies by which backends are enabled.
📚 Using BuffDB as a Library
See the Rust example above for library usage.
🎯 Use Cases
- Offline-First Apps: Note-taking, games, fieldwork applications, airline systems, collaborative documents
- IoT & Edge Computing: Managing device configurations and states locally before cloud sync
- Low-Bandwidth Environments: Reducing serialization overhead with Protocol Buffers
- Embedded Analytics: Local data processing with optional network access
🔧 Troubleshooting
macOS Linking Errors
If you encounter ld: library not found for -liconv errors:
- Ensure you have the
.cargo/config.tomlfile in the project root:
[]
= ["-L/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib"]
[]
= "/opt/homebrew/lib"
- For Apple Silicon (M1/M2) Macs, ensure Homebrew is in
/opt/homebrew:
- For Intel Macs, Homebrew may be in
/usr/local:
# Update the config.toml accordingly:
- If issues persist, add to your shell profile (
~/.zshrcor~/.bash_profile):
🙏 Acknowledgments
This project is inspired by conversations with Michael Cahill, Professor of Practice, School of Computer Science, University of Sydney, and feedback from edge computing customers dealing with low-bandwidth, high-performance challenges.