This guide explains Bao’s functions through fiction, blending in a touch of fantasy. Bob and Alice work at MI6, where they frequently handle sensitive data. Their agency chose the Bao library because it enables secure, encrypted exchanges on a convenient public cloud.

Table of Contents

Start

First step is the installation. There are different ways depending on the programming language. Alice chooses Go for a command-line tool, while Bob prefers Dart for a mobile app.

pip install baolib
go get github.com/stregato/bao/lib
dart pub add bao
dart run bao:bootstrap
import ink.francesco.bao.Bao;

Identity

Identity makes each person unique and recognizable, which is crucial when access must remain discrete.

Bao defines identity through elliptic keys, an elegant mathematical tool. Each identity is made of two keys: secp256k1 for encryption and ed25519 for signing.

For Bob and Alice to communicate, both must create a private identity, made with private keys and share their public identity, based on their public keys.

from baolib import *
bob, bob_secret = newKeyPair()

print('Owner public ID:', bob)

keys = bob_secret.decode()
print(f'secp256k1 key: {keys["cryptKey"]}, ed25519 key: {keys["signKey"]}')
import "github.com/stregato/bao/lib"

//create an identity
bob, bobSecret := lib.NewKeyPair()

//decode the secp256k1 and ed25519 keys
cryptKey, signKey, err := DecodeID(string(bobSecret))
import 'package:bao/bao.dart';

final (bob, bobSecret) = newKeyPair();
print('Owner public ID: $bob');

final keys = bobSecret.decode();
print('secp256k1 key: ${keys["cryptKey"]}, ed25519 key: ${keys["signKey"]}');
import ink.francesco.bao.IDs;
import java.util.Map;

var keyPair = IDs.newKeyPair();
var bob = keyPair.publicID();
System.out.println("Owner public ID: " + bob);

Map<String, Object> keys = keyPair.privateID().decode();
System.out.println("secp256k1 key: " + keys.get("cryptKey") + ", ed25519 key: " + keys.get("signKey"));

Vault

The simplest data container in Bao is a vault. A vault supports common file operations, such as create, read and write, with strong access control.

A vault is defined by a realm, a storage technology (e.g. S3 or WebDav), a creator and administrator and a local database. Alice and Bob choose S3 because of its popularity and availability. A user with high volumes may instead choose WebDav or SFTP and avoid variable costs.

The below code shows a vault created by Alice.

from baolib import *

db = DB('sqlite3', 'my_db_path.db')
store_config = {
    'type': 's3',
    'id': 'bao-test',
    's3': {
        'bucket': 'my-bucket',
        'endpoint': 's3.amazonaws.com',
        'prefix': 'bao-test',
        'auth': {
            'accessKeyId': 'your-access-key-id',
            'secretAccessKey': 'your-secret-access-key'
        }
    }
}
store = Store(store_config)
vault = Vault.create(Vault.users, alice_secret, store, db)
import "github.com/stregato/bao/lib"

//set a store configuration
store, err := lib.OpenStore(lib.StoreConfig{
  Type: "s3",
  Id:   "bao-test",
  S3: lib.S3Config{
    Bucket:   "my-bucket",
    Endpoint: "s3.amazonaws.com",
    Prefix:   "bao-test",
    Auth: lib.S3ConfigAuth{
      AccessKeyId:     "your-access-key-id",
      SecretAccessKey: "your-secret-access-key",
    },
  },
})

db, err := lib.OpenDB("sqlite3", "my_bao.db", "")
vault := lib.CreateVault(alice, db, store, lib.Config{})
import 'package:bao/bao.dart';

final (alice, aliceSecret) = newKeyPair();
final db = await DB.open('sqlite3', 'my_bao.db');
final storeConfig = StoreConfig(
  id: 'bao-test',
  type: 's3',
  s3: S3Config(
    bucket: 'my-bucket',
    endpoint: 's3.amazonaws.com',
    prefix: 'bao-test',
    auth: S3ConfigAuth(
      accessKeyId: 'your-access-key-id',
      secretAccessKey: 'your-secret-access-key',
    ),
  ),
);
final store = await Store.open(storeConfig);
final vault = await Vault.create(users, aliceSecret, store, db);
import ink.francesco.bao.Vault;
import ink.francesco.bao.DB;
import ink.francesco.bao.IDs;
import ink.francesco.bao.Store;
import ink.francesco.bao.StoreConfig;
import java.util.HashMap;

var aliceKeyPair = IDs.newKeyPair();
var db = new DB("alice_bao.db");

var storeConfig = StoreConfig.s3(
  "bao-test",
  "s3.amazonaws.com",
  "",
  "my-bucket",
  "bao-test",
  "your-access-key-id",
  "your-secret-access-key"
);

var store = Store.open(storeConfig);
var aliceKeyPair = IDs.newKeyPair();
var config = new HashMap<>();
var vault = Vault.create(Vault.USERS, aliceKeyPair.privateID(), store, db, config);

The realm is the context for the vault. By convention, the realm is often called “users”, and the library defines a variable for it. However, any realm is acceptable. As the creator, Alice has admin access to the vault by default. Then she must grant access to Bob.

Access is managed through the sync_access function, which synchronizes the local database with access data in remote storage. The command may include one or more AccessChange objects to update a user’s access to a group. Revoking access is represented by an AccessChange where the access field is 0.

vault.sync_access([
    AccessChange(bob, Access.read_write),
])
alice_access = vault.get_access(alice)
bob_access = vault.get_access(bob)
print(f'Alice has access level: {alice_access}')
print(f'Bob has access level: {bob_access}')
import (
  "fmt"
  "github.com/stregato/bao/lib/vault"
)

alicePublic := alice.PublicIDMust()
bobPublic := bob.PublicIDMust()

err := v.SyncAccess(0,
  vault.AccessChange{Access: vault.ReadWrite, UserId: alicePublic},
  vault.AccessChange{Access: vault.ReadWrite, UserId: bobPublic},
)
if err != nil {
  panic(err)
}

aliceAccess, err := v.GetAccess(alicePublic)
if err != nil {
  panic(err)
}
bobAccess, err := vault.GetAccess(bobPublic)
if err != nil {
  panic(err)
}
fmt.Printf("Alice has access level: %d\n", aliceAccess)
fmt.Printf("Bob has access level: %d\n", bobAccess)
final (bob, bobSecret) = newKeyPair();
final changes = [
  AccessChange(alice, accessReadWrite),
  AccessChange(bob, accessReadWrite),
];
await vault.syncAccess(changes);

final aliceAccess = await vault.getAccess(alice);
final bobAccess = await vault.getAccess(bob);
print('Alice has access level: $aliceAccess');
print('Bob has access level: $bobAccess');
import java.util.Arrays;

int readWrite = 3;
var aliceKeyPair = IDs.newKeyPair();
var bobKeyPair = IDs.newKeyPair();
var changes = Arrays.asList(
  new Vault.AccessChange(readWrite, aliceKeyPair.publicID()),
  new Vault.AccessChange(readWrite, bobKeyPair.publicID())
);
vault.syncAccess(changes, 0);

int aliceAccess = vault.getAccess(aliceKeyPair.publicID());
int bobAccess = vault.getAccess(bobKeyPair.publicID());
System.out.println("Alice has access level: " + aliceAccess);
System.out.println("Bob has access level: " + bobAccess);

Bob can then open the vault. Alice’s public key is required because the creator’s identity confirms the blockchain’s validity.

db2 = DB('bob_bao.db')
vault = Vault.open(Vault.users, bob_secret, alice, store, db2)
vault.sync_access()
db2, err := lib.OpenDB("sqlite3", "bob_bao.db", "")
if err != nil {
  panic(err)
}

vault, err := lib.OpenVault(bob, db2, store, alicePublic)
if err != nil {
  panic(err)
}

err = vault.SyncAccess(0)
if err != nil {
  panic(err)
}
final db2 = await DB.open('sqlite3', 'bob_bao.db');
final (bob, bobSecret) = newKeyPair();
final vault = await Vault.open(users, bobSecret, alice, store, db2);
await vault.syncAccess();
import java.util.HashMap;
import java.util.List;

var db2 = new DB("bob_bao.db");
var bobKeyPair = IDs.newKeyPair();
var aliceKeyPair = IDs.newKeyPair();
var vault = Vault.open(Vault.USERS, bobKeyPair.privateID(), aliceKeyPair.publicID(), store, db2);
vault.syncAccess(List.of(), 0);

File operations

These operations handle creating, listing, writing, and reading files. Depending on the operation, the source or destination can be a local file or a location in the vault.

In the example below, Alice copies a file into the vault at docs/secret.txt, then checks the vault content under the docs folder. The folder content is a list of File objects.

import tempfile
from pathlib import Path

temp_root = Path(tempfile.mkdtemp())
data_path = temp_root / "secret.txt"
data_path.write_text("Hello from Alice!")

vault.write("docs/secret.txt", src=str(data_path))
files = vault.read_dir("docs", None, 0, 10)
for f in files:
    print("File:", f['name'] if isinstance(f, dict) else f.name)
import (
  "fmt"
  "os"
  "path/filepath"
  "time"

  "github.com/stregato/bao/lib/vault"
)

tempRoot, err := os.MkdirTemp("", "bao")
if err != nil {
  panic(err)
}
dataPath := filepath.Join(tempRoot, "secret.txt")
err = os.WriteFile(dataPath, []byte("Hello from Alice!"), 0o600)
if err != nil {
  panic(err)
}

_, err = v.Write("docs/secret.txt", dataPath, nil, 0, nil)
if err != nil {
  panic(err)
}

files, err := v.ReadDir("docs", time.Time{}, 0, 10)
if err != nil {
  panic(err)
}
for _, f := range files {
  fmt.Println("File:", f.Name)
}
import 'dart:io';

final tempRoot = Directory.systemTemp.createTempSync('bao');
final dataPath = File('${tempRoot.path}/secret.txt');
dataPath.writeAsStringSync('Hello from Alice!');

await vault.write('docs/secret.txt', src: dataPath.path);
final files = await vault.readDir('docs', limit: 10);
for (final f in files) {
  print('File: ${f.name}');
}
import java.nio.file.Files;

var tempRoot = Files.createTempDirectory("bao");
var dataPath = tempRoot.resolve("secret.txt");
Files.writeString(dataPath, "Hello from Alice!");

vault.write("docs/secret.txt", new byte[0], dataPath.toString(), 0L);
var files = vault.readDir("docs", 0L, 0L, 10);
for (var f : files) {
  System.out.println("File: " + f.name);
}

Bob copies the file in the vault to the local file system.

import tempfile
from pathlib import Path

temp_root = Path(tempfile.mkdtemp())
vault.sync([Vault.users])
out_path = temp_root / "alice-secret.txt"
file = vault.read('docs/secret.txt', str(out_path))
print("Alice's secret:", out_path.read_text())
import (
  "fmt"
  "os"
  "path/filepath"

  "github.com/stregato/bao/lib/vault"
)

tempRoot, err := os.MkdirTemp("", "bao")
if err != nil {
  panic(err)
}

_, err = v.Sync(vault.Users)
if err != nil {
  panic(err)
}

outPath := filepath.Join(tempRoot, "alice-secret.txt")
_, err = v.Read("docs/secret.txt", outPath, 0, nil)
if err != nil {
  panic(err)
}

data, err := os.ReadFile(outPath)
if err != nil {
  panic(err)
}
fmt.Println("Alice's secret:", string(data))
import 'dart:io';

final tempRoot = Directory.systemTemp.createTempSync('bao');
await vault.sync([users]);
final outPath = File('${tempRoot.path}/alice-secret.txt');
await vault.read('docs/secret.txt', outPath.path);
print("Alice's secret: ${await outPath.readAsString()}");
import java.nio.file.Files;
import java.util.List;

var tempRoot = Files.createTempDirectory("bao");
vault.sync(List.of(Vault.USERS));
var outPath = tempRoot.resolve("alice-secret.txt");
vault.read("docs/secret.txt", outPath.toString(), 0);
System.out.println("Alice's secret: " + Files.readString(outPath));

Read and write operations are by default synchronous and the request completes only once the data has been fully transfered between the remote storage and the local filesystem. The library supports asynchronous operations that are especially useful with event driven application.

There are two type of asynchrous operations:

  1. async: the operation starts at the request time and continues on a separate thread
  2. scheduled: the operation runs in a background thread periodically scheduled

Each file has a unique id, which can be used to wait for the operation to complete.

import tempfile
from pathlib import Path

temp_root = Path(tempfile.mkdtemp())
data_path = temp_root / "secret.txt"
data_path.write_text("Hello from Alice!")
file = vault.write('docs/hello.txt', src=str(data_path), options=Vault.async_operation)

# other operations while write is in progress
vault.wait_files([file['id'] if isinstance(file, dict) else file.id])
import (
  "os"
  "path/filepath"

  "github.com/stregato/bao/lib/vault"
)

tempRoot, err := os.MkdirTemp("", "bao")
if err != nil {
  panic(err)
}
dataPath := filepath.Join(tempRoot, "hello.txt")
err = os.WriteFile(dataPath, []byte("Hello from Alice!"), 0o600)
if err != nil {
  panic(err)
}

file, err := v.Write("docs/hello.txt", dataPath, nil, vault.AsyncOperation, nil)
if err != nil {
  panic(err)
}

err = vault.WaitFiles(file.Id)
if err != nil {
  panic(err)
}
import 'dart:io';

final tempRoot = Directory.systemTemp.createTempSync('bao');
final dataPath = File('${tempRoot.path}/hello.txt');
dataPath.writeAsStringSync('Hello from Alice!');

final file = await vault.write('docs/hello.txt',
    src: dataPath.path, options: asyncOperation);
await vault.waitFiles([file.id]);
import java.nio.file.Files;
import java.util.List;

var tempRoot = Files.createTempDirectory("bao");
var dataPath = tempRoot.resolve("hello.txt");
Files.writeString(dataPath, "Hello from Alice!");

long asyncOption = 1;
var file = vault.write("docs/hello.txt", new byte[0], dataPath.toString(), asyncOption);
vault.waitFiles(List.of(file.id));

The other file operations are delete and stat.

file = vault.stat('docs/hello.txt')
print(f'File: {file["name"]}, size: {file["size"]} bytes')

file = vault.delete('docs/hello.txt', options=Vault.async_operation)
vault.wait_files([file['id'] if isinstance(file, dict) else file.id])
import (
  "fmt"

  "github.com/stregato/bao/lib/vault"
)

file, err := v.Stat("docs/hello.txt")
if err != nil {
  panic(err)
}
fmt.Printf("File: %s, size: %d bytes\n", file.Name, file.Size)

file, err = v.Delete("docs/hello.txt", vault.AsyncOperation, nil)
if err != nil {
  panic(err)
}
err = v.WaitFiles(file.Id)
if err != nil {
  panic(err)
}
final file = await vault.stat('docs/hello.txt');
print('File: ${file.name}, size: ${file.size} bytes');

final deletedFile = await vault.delete('docs/hello.txt', options: asyncOperation);
await vault.waitFiles([deletedFile.id]);
var file = vault.stat("docs/hello.txt");
System.out.println("File: " + file.name + ", size: " + file.size + " bytes");

long asyncOption = 1;
var deletedFile = vault.delete("docs/hello.txt", asyncOption);
vault.waitFiles(List.of(deletedFile.id));

Realm All

A vault created with a realm all does not use encryption. Data is accessible to all users without the need to grant access.

vault = Vault.create(Vault.all, alice_secret, store, db)
v, err := vault.Create(vault.All, store, db)
if err != nil {
  panic(err)
}
final vault = await Vault.create(Vault.all, store, db);
var vault = Vault.create(Vault.ALL, store, db);

Realm Home

A vault created with a realm home organizes files in folders named after each user’s public identity. Only the owner of a home folder can read the files in it, but other users can write to it.

This is useful when Alice and Bob want to leave messages or files for each other in a shared vault. Alice can write files to Bob’s home folder but she cannot read what’s already there. Only Bob can decrypt and read his own files. It’s like leaving a sealed envelope on someone’s desk: you know they’ll receive it, but you can’t open it yourself.

# Alice creates a home vault
alice_vault = Vault.create(Vault.home, alice_secret, store, db)

# Grant Bob write access to her home vault
alice_vault.sync_access(0, {'userId': bob, 'access': 'ReadWrite'})

tmp_file = Path(tempfile.mkdtemp()) / "message.txt"
tmp_file.write_text("Hello Bob")

# Alice writes to Bob's home folder (requires Bob's public ID)
alice_vault.write(f'{bob}/message.txt', str(tmp_file))

# Now open Bob's home vault (Bob can read his own messages)
bob_vault = Vault.open(Vault.home, bob_secret, alice, store, db)

# Bob can read messages in his home folder
bob_vault.read(f'{bob}/message.txt', 'message.txt')

# But Alice cannot read what Bob has in his home folder
# alice_vault.read(f'{bob}/secret.txt', 'secret.txt')  # This would fail
import (
  "github.com/stregato/bao/lib/vault"
)

// Alice creates a home vault
v, err := vault.Create(vault.Home, aliceSecret, store, db)
if err != nil {
  panic(err)
}

// Grant Bob write access to her home vault
err = v.SyncAccess(0, vault.AccessChange{
  UserId: bob,
  Access: vault.ReadWrite,
})
if err != nil {
  panic(err)
}

// Alice writes to Bob's home folder (uses Bob's public ID as folder)
bobID := bob.String()
_, err = v.Write(bobID+"/message.txt", tmpFile, nil, 0, nil)
if err != nil {
  panic(err)
}

// Now open Bob's home vault (Bob can read his own messages)
bobV, err := vault.Open(vault.Home, bobSecret, alice, store, db)
if err != nil {
  panic(err)
}

// Bob can read messages in his home folder
bobV.Read(bobID+"/message.txt", "message.txt", 0, nil)

// But Alice cannot read what Bob has in his home folder
// v.Read(bobID+"/secret.txt", "secret.txt", 0, nil)  // This would fail
import 'package:bao/bao.dart';

// Alice creates a home vault
final v = await Vault.create(
  Vault.home,
  aliceSecret,
  store,
  db,
);

// Grant Bob write access to her home vault
await v.syncAccess(0, [
  AccessChange(userId: bob, access: AccessLevel.readWrite),
]);

// Alice writes to Bob's home folder (uses Bob's public ID as folder)
final bobID = bob.toString();
await v.write('$bobID/message.txt', src: tmpFile);

// Now open Bob's home vault (Bob can read his own messages)
final bobV = await Vault.open(
  Vault.home,
  bobSecret,
  alice,
  store,
  db,
);

// Bob can read messages in his home folder
await bobV.read('$bobID/message.txt', 'message.txt');

// But Alice cannot read what Bob has in his home folder
// await v.read('$bobID/secret.txt', 'secret.txt');  // This would fail
import ink.francesco.bao.Vault;
import java.nio.file.Paths;

// Alice creates a home vault
var v = Vault.create(
  Vault.HOME,
  aliceSecret,
  store,
  db
);

// Grant Bob write access to her home vault
v.syncAccess(0, new AccessChange[]{
  new AccessChange(bob, AccessLevel.READ_WRITE)
});

// Alice writes to Bob's home folder (uses Bob's public ID as folder)
String bobID = bob.toString();
v.write(bobID + "/message.txt", tmpFile);

// Now open Bob's home vault (Bob can read his own messages)
var bobV = Vault.open(
  Vault.HOME,
  bobSecret,
  alice,
  store,
  db
);

// Bob can read messages in his home folder
bobV.read(bobID + "/message.txt", "message.txt");

// But Alice cannot read what Bob has in his home folder
// v.read(bobID + "/secret.txt", "secret.txt");  // This would fail

Summary

Alice and Bob need to exchange sensitive information. They use a public service based on S3 and they encrypt files with Bao library. Alice creates a vault on S3 and gives access to Bob.

Replica

Files work well for information that does not require questioning. For dynamic and structured information, a database is a better fit. Bao offers replication between local databases. Each peer maintains a local database, usually SQLite, with changes synchronized over encrypted storage.

We can imagine a useful scenario for Alice and Bob. Intelligence agents need to take time-stamped notes and share them strictly with the intended audience. Since an agent sometimes operates in a hostile environment, the notes are stored on a local device. At the same time, they are shared periodically with the base and other agents to reduce the risk of losing them.

Alice scratches a sample draft on paper.

Note (Summary) Observed At Recorded At Sensitivity Location
Additional checkpoint installed near main road 2026-01-29 07:40 2026-01-29 08:05 High West Crossing
Unmarked vehicle repeated area patrol twice 2026-01-30 16:12 2026-01-30 16:18 Medium Market District
Meeting venue changed without notice 2026-01-31 09:55 2026-01-31 10:02 High Old Port
Network access intermittently unavailable 2026-02-01 21:30 2026-02-01 22:10 Low Temporary Lodging

She realizes the format makes sense and decide to create a database structure.

Annotated SQL

Bao helps define the database structure using annotated DDL. These annotations are SQL comments placed immediately before a SQL statement.

Comments starting with – INIT are used at database creation and typically introduce CREATE TABLE or CREATE INDEX .

-- INIT 1.0
CREATE TABLE notes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    summary VARCHAR(1024) NOT NULL,
    observedAt INTEGER
    recordedAt INTEGER
    sensitity INTEGER
    location VARCHAR(1024)
);

-- INIT 1.0
CREATE INDEX idx_files_notes_summary ON notes (observedAt);

Other comments define placeholders that can be used instead of SQL statements in queries.

-- INSERT_NOTE 1.0
INSERT INTO notes (summary, observedAt, recordedAt, sensitity, location)
VALUES (:summary, :observedAt, :recordedAt, :sensitity, :location);

-- SELECT_NOTES 1.0
SELECT summary, recordedAt, sensitity, location FROM notes WHERE observedAt>:after;

The operations

Alice opens a local database using the ddl variable, which contains the above INIT statements and placeholders.


ddl = ... #The DB definition and placeholders

db_path = temp_root / 'data.db'
if db_path.exists():
    db_path.unlink()
    
data_db = DB('sqlite3', str(db_path), ddl=ddl)
import (
  "os"
  "path/filepath"

  sqlxpkg "github.com/stregato/bao/lib/sqlx"
)

ddl := "" // The DB definition and placeholders

dbPath := filepath.Join(tempRoot, "data.db")
if _, err := os.Stat(dbPath); err == nil {
  os.Remove(dbPath)
}

dataDB, err := sqlxpkg.Create("sqlite3", dbPath, ddl)
if err != nil {
  panic(err)
}
final ddl = ''; // The DB definition and placeholders

final dbPath = '${tempRoot.path}/data.db';
if (File(dbPath).existsSync()) {
  File(dbPath).deleteSync();
}

final dataDB = await DB.create('sqlite3', dbPath, ddl: ddl);
String ddl = ""; // The DB definition and placeholders

var dbPath = tempRoot.resolve("data.db");
if (Files.exists(dbPath)) {
  Files.delete(dbPath);
}

var dataDB = DB.create("sqlite3", dbPath.toString(), ddl);

She then open a DB replica bound to a vault. The replica behaves like a SQL interface, providing exec for modifying data and query or fetch for retrieving it.

from datetime import datetime, timezone

def iso_to_ms(iso: str) -> int:
    return int(
        datetime.fromisoformat(
            iso.replace("Z", "+00:00")
        ).timestamp() * 1000
    )  

  replica = Replica(vault, data_db)

  # Asset observation — Zurich, early morning
  replica.exec("INSERT_NOTE", {
      "summary": "Asset confirmed entering secondary location",
      "observedAt": iso_to_ms("2026-01-31T05:12:18.340Z"),
      "recordedAt": iso_to_ms("2026-01-31T05:12:41.902Z"),
      "sensitity": 2,
      "location": "Zurich / District 4",
  })

  # Delayed report — safehouse relay
  replica.exec("INSERT_NOTE", {
      "summary": "Encrypted briefcase transfer observed",
      "observedAt": iso_to_ms("2026-01-31T06:03:55.000Z"),
      "recordedAt": iso_to_ms("2026-01-31T06:47:10.611Z"),
      "sensitity": 3,
      "location": "Vienna / Underground station U3",
  })    

  notes = replica.fetch('SELECT_NOTES', {after: iso_to_ms("2026-01-1T0:0:0.340Z")})
import (
  "time"

  replicapkg "github.com/stregato/bao/lib/replica"
)

replica := replicapkg.New(vault, dataDB)

// Asset observation — Zurich, early morning
params := map[string]interface{}{
  "summary": "Asset confirmed entering secondary location",
  "observedAt": time.Date(2026, 1, 31, 5, 12, 18, 340000000, time.UTC).UnixMilli(),
  "recordedAt": time.Date(2026, 1, 31, 5, 12, 41, 902000000, time.UTC).UnixMilli(),
  "sensitity": 2,
  "location": "Zurich / District 4",
}
err = replica.Exec("INSERT_NOTE", params)
if err != nil {
  panic(err)
}

// Delayed report — safehouse relay
params["summary"] = "Encrypted briefcase transfer observed"
params["observedAt"] = time.Date(2026, 1, 31, 6, 3, 55, 0, time.UTC).UnixMilli()
params["recordedAt"] = time.Date(2026, 1, 31, 6, 47, 10, 611000000, time.UTC).UnixMilli()
params["sensitity"] = 3
params["location"] = "Vienna / Underground station U3"
err = replica.Exec("INSERT_NOTE", params)
if err != nil {
  panic(err)
}

queryParams := map[string]interface{}{
  "after": time.Date(2026, 1, 1, 0, 0, 0, 340000000, time.UTC).UnixMilli(),
}
rows, err := replica.Query("SELECT_NOTES", queryParams)
if err != nil {
  panic(err)
}
final replica = Replica(vault, dataDB);

// Asset observation — Zurich, early morning
await replica.exec("INSERT_NOTE", {
  "summary": "Asset confirmed entering secondary location",
  "observedAt": DateTime.parse("2026-01-31T05:12:18.340Z").millisecondsSinceEpoch,
  "recordedAt": DateTime.parse("2026-01-31T05:12:41.902Z").millisecondsSinceEpoch,
  "sensitity": 2,
  "location": "Zurich / District 4",
});

// Delayed report — safehouse relay
await replica.exec("INSERT_NOTE", {
  "summary": "Encrypted briefcase transfer observed",
  "observedAt": DateTime.parse("2026-01-31T06:03:55.000Z").millisecondsSinceEpoch,
  "recordedAt": DateTime.parse("2026-01-31T06:47:10.611Z").millisecondsSinceEpoch,
  "sensitity": 3,
  "location": "Vienna / Underground station U3",
});

final notes = await replica.fetch('SELECT_NOTES', {
  "after": DateTime.parse("2026-01-01T00:00:00.340Z").millisecondsSinceEpoch,
});
var replica = new Replica(vault, dataDB);

// Asset observation — Zurich, early morning
replica.exec("INSERT_NOTE", Map.of(
  "summary", "Asset confirmed entering secondary location",
  "observedAt", 1738308738340L,
  "recordedAt", 1738308761902L,
  "sensitity", 2,
  "location", "Zurich / District 4"
));

// Delayed report — safehouse relay
replica.exec("INSERT_NOTE", Map.of(
  "summary", "Encrypted briefcase transfer observed",
  "observedAt", 1738312235000L,
  "recordedAt", 1738313230611L,
  "sensitity", 3,
  "location", "Vienna / Underground station U3"
));

var notes = replica.fetch("SELECT_NOTES", Map.of(
  "after", 1704067200340L
));

The exec and query operate only on the local database. The sync function instead exchanges changes with the vault and other peers.

During synchronization, sync performs the following steps:

  1. It rolls back all local changes that have not yet been synchronized.
  2. It applies locally the changes retrieved from the vault, which may include updates made by other peers.
  3. It writes the previously rolled-back local changes to the vault.
  4. It reapplies those local changes to the local database.

This process guarantees that local changes are confirmed only if they are compatible with the changes already present in the vault. If a conflict occurs, sync fails and returns an error. This mirrors the behavior of a traditional database commit, where changes are rolled back on failure. As with standard transactions, error handling is explicit and conflicting changes must be retried when necessary.

replica.sync()
err = replica.Sync()
if err != nil {
  panic(err)
}
await replica.sync();
replica.sync();

Bob’s implementation follows the same principle and requires read and write access to the vault.


ddl = ... #The DB definition and placeholders

db_path = temp_root / 'data.db'
if db_path.exists():
    db_path.unlink()
    
data_db = DB('sqlite3', str(db_path), ddl=ddl)
replica = Replica(vault, data_db)
#Bob operations

replica.sync()
import (
  replicapkg "github.com/stregato/bao/lib/replica"
  sqlxpkg "github.com/stregato/bao/lib/sqlx"
)

ddl := "" // The DB definition and placeholders

dbPath := filepath.Join(tempRoot, "data.db")
if _, err := os.Stat(dbPath); err == nil {
  os.Remove(dbPath)
}

dataDB, err := sqlxpkg.Create("sqlite3", dbPath, ddl)
if err != nil {
  panic(err)
}

replica := replicapkg.New(vault, dataDB)
// Bob operations

err = replica.Sync()
if err != nil {
  panic(err)
}
final ddl = ''; // The DB definition and placeholders

final dbPath = '${tempRoot.path}/data.db';
if (File(dbPath).existsSync()) {
  File(dbPath).deleteSync();
}

final dataDB = await DB.create('sqlite3', dbPath, ddl: ddl);
final replica = Replica(vault, dataDB);
// Bob operations

await replica.sync();
String ddl = ""; // The DB definition and placeholders

var dbPath = tempRoot.resolve("data.db");
if (Files.exists(dbPath)) {
  Files.delete(dbPath);
}

var dataDB = DB.create("sqlite3", dbPath.toString(), ddl);
var replica = new Replica(vault, dataDB);
// Bob operations

replica.sync();

Mailbox

Alice wants to broadcast a lunchtime message to her colleagues. Using a database would be overkilling, so she uses Bao’s Mailbox interface instead.

She creates a mailbox bound to a vault, specifying the vault folder where messages are stored. Once sent, the lunch message is delivered to Bob and any other peer, including Alice herself.

mailbox = Mailbox(vault, 'mailbox')
mailbox.send(Message('Lunch time', 'Let's have baozi!'))
message = mailbox.receive(0, 0)
import (
  mailboxpkg "github.com/stregato/bao/lib/mailbox"
)

mailbox := mailboxpkg.New(vault, "mailbox")
err := mailbox.Send(&mailboxpkg.Message{
  Subject: "Lunch time",
  Body:    "Let's have baozi!",
})
if err != nil {
  panic(err)
}

message, err := mailbox.Receive(0, 0)
if err != nil {
  panic(err)
}
final mailbox = Mailbox(vault, 'mailbox');
await mailbox.send(Message(
  subject: 'Lunch time',
  body: 'Let\'s have baozi!',
));
final message = await mailbox.receive(0, 0);
var mailbox = new Mailbox(vault, "mailbox");
mailbox.send(new Message("Lunch time", "Let's have baozi!"));
Message message = mailbox.receive(0, 0);

Messages in a mailbox are usually visible to all users with access to the vault. However, there is an easy way to create private mailboxes, where only a single user can receive the content.

In the code below, Alice opens a vault using the realm home. She then creates a mailbox in the folder named after Bob’s public. Because the realm is home, only Bob can read the data stored there.

vault = Vault.open(Vault.home, alice_secret, alice, store, db2)
mailbox = Mailbox(vault, bob)
mailbox.send(Message('Lunch time', 'Let's have baozi!'))

vault = Vault.open(Vault.home, bob_secret, alice, store, db2)
mailbox = Mailbox(vault, bob)
message = mailbox.receive(0, 0)
import (
  "github.com/stregato/bao/lib/mailbox"
  "github.com/stregato/bao/lib/vault"
)

// Alice sends a message
v, err := vault.Open(vault.Home, alice_secret, alice, store, db2)
if err != nil {
  panic(err)
}

m := mailbox.New(v, bob)
err = m.Send(&mailbox.Message{
  Subject: "Lunch time",
  Body:    "Let's have baozi!",
})
if err != nil {
  panic(err)
}

// Bob receives the message
v, err = vault.Open(vault.Home, bob_secret, alice, store, db2)
if err != nil {
  panic(err)
}

m = mailbox.New(v, bob)
message, err := m.Receive(0, 0)
if err != nil {
  panic(err)
}
// Alice sends a message
final vault = await Vault.open(Vault.home, aliceSecret, alice, store, db2);
final mailbox = Mailbox(vault, bob);
await mailbox.send(Message(
  subject: 'Lunch time',
  body: 'Let\'s have baozi!',
));

// Bob receives the message
final vaultBob = await Vault.open(Vault.home, bobSecret, alice, store, db2);
final mailboxBob = Mailbox(vaultBob, bob);
final message = await mailboxBob.receive(0, 0);
// Alice sends a message
var vault = Vault.open(Vault.HOME, aliceSecret, alice, store, db2);
var mailbox = new Mailbox(vault, bob);
mailbox.send(new Message("Lunch time", "Let's have baozi!"));

// Bob receives the message
var vaultBob = Vault.open(Vault.HOME, bobSecret, alice, store, db2);
var mailboxBob = new Mailbox(vaultBob, bob);
Message message = mailboxBob.receive(0, 0);

Back to the vault

Metadata

Files in a vault can carry metadata through attributes. When Alice writes a file, she can attach metadata such as a file category, sensitivity level, or custom tags. These attributes are stored along with the file and become part of the file information.

When Bob later lists files in a directory or checks a file’s status, the attributes are automatically retrieved from the vault. This makes it easy to organize and filter files based on metadata without reading the actual file content.

In the example below, Alice writes a document with custom attributes describing its purpose and sensitivity level.

import tempfile
from pathlib import Path

temp_root = Path(tempfile.mkdtemp())
data_path = temp_root / "report.txt"
data_path.write_text("Q4 Financial Report")

# Write file with metadata
attrs = {
    'category': 'finance',
    'sensitivity': 'high',
    'reviewed': 'alice'
}
vault.write('reports/q4.txt', src=str(data_path), attrs=attrs)

# Read the file with its metadata
file_info = vault.stat('reports/q4.txt')
print(f"File: {file_info['name']}")
print(f"Attributes: {file_info.get('attrs', {})}")
import (
  "fmt"
  "os"
  "path/filepath"
)

tempRoot, err := os.MkdirTemp("", "bao")
if err != nil {
  panic(err)
}
dataPath := filepath.Join(tempRoot, "report.txt")
err = os.WriteFile(dataPath, []byte("Q4 Financial Report"), 0o600)
if err != nil {
  panic(err)
}

// Write file with metadata
attrs := map[string]interface{}{
  "category":    "finance",
  "sensitivity": "high",
  "reviewed":    "alice",
}
_, err = vault.Write("reports/q4.txt", dataPath, attrs, 0, nil)
if err != nil {
  panic(err)
}

// Read the file with its metadata
file, err := vault.Stat("reports/q4.txt")
if err != nil {
  panic(err)
}
fmt.Printf("File: %s\n", file.Name)
fmt.Printf("Attributes: %v\n", file.Attrs)
import 'dart:io';

final tempRoot = Directory.systemTemp.createTempSync('bao');
final dataPath = File('${tempRoot.path}/report.txt');
dataPath.writeAsStringSync('Q4 Financial Report');

// Write file with metadata
final attrs = {
  'category': 'finance',
  'sensitivity': 'high',
  'reviewed': 'alice',
};
await vault.write('reports/q4.txt', src: dataPath.path, attrs: attrs);

// Read the file with its metadata
final fileInfo = await vault.stat('reports/q4.txt');
print('File: ${fileInfo.name}');
print('Attributes: ${fileInfo.attrs}');
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;

var tempRoot = Files.createTempDirectory("bao");
var dataPath = tempRoot.resolve("report.txt");
Files.writeString(dataPath, "Q4 Financial Report");

// Write file with metadata
Map<String, Object> attrs = new HashMap<>();
attrs.put("category", "finance");
attrs.put("sensitivity", "high");
attrs.put("reviewed", "alice");
vault.write("reports/q4.txt", new byte[0], dataPath.toString(), 0L);

// Read the file with its metadata
var fileInfo = vault.stat("reports/q4.txt");
System.out.println("File: " + fileInfo.name);
System.out.println("Attributes: " + fileInfo.attrs);

Versions

When Alice overwrites a file, Bao preserves the previous version. This powerful feature ensures that no data is lost. Bob can always access earlier versions of a file using the versions() method.

File versions are retained according to the vault’s retention policy. By default, Bao keeps older versions for one month before deleting them to free up storage space. This means Bob can recover from accidental overwrites or track how a document has changed over time.

In the example below, Alice updates a document multiple times. Bob can retrieve all versions and see the history.

import tempfile
from pathlib import Path

temp_root = Path(tempfile.mkdtemp())

# Alice writes version 1
data_path = temp_root / "data.txt"
data_path.write_text("Version 1: Initial draft")
vault.write('docs/status.txt', src=str(data_path))

# Alice updates to version 2
data_path.write_text("Version 2: Reviewed by Bob")
vault.write('docs/status.txt', src=str(data_path))

# Alice updates to version 3
data_path.write_text("Version 3: Final approved")
vault.write('docs/status.txt', src=str(data_path))

# Bob retrieves all versions
versions = vault.versions('docs/status.txt')
for i, file in enumerate(versions):
  print(f"Version {i}: {file['name']}")
  out_path = temp_root / f"version_{i}.txt"
  vault.read(file['name'], str(out_path))
  print(f"  Content: {out_path.read_text()}")
import (
  "fmt"
  "os"
  "path/filepath"
)

tempRoot, err := os.MkdirTemp("", "bao")
if err != nil {
  panic(err)
}

// Alice writes version 1
dataPath := filepath.Join(tempRoot, "data.txt")
err = os.WriteFile(dataPath, []byte("Version 1: Initial draft"), 0o600)
if err != nil {
  panic(err)
}
vault.Write("docs/status.txt", dataPath, nil, 0, nil)

// Alice updates to version 2
os.WriteFile(dataPath, []byte("Version 2: Reviewed by Bob"), 0o600)
vault.Write("docs/status.txt", dataPath, nil, 0, nil)

// Alice updates to version 3
os.WriteFile(dataPath, []byte("Version 3: Final approved"), 0o600)
vault.Write("docs/status.txt", dataPath, nil, 0, nil)

// Bob retrieves all versions
versions, err := vault.Versions("docs/status.txt")
if err != nil {
  panic(err)
}
for i, file := range versions {
  fmt.Printf("Version %d: %s\n", i, file.Name)
  outPath := filepath.Join(tempRoot, fmt.Sprintf("version_%d.txt", i))
  vault.Read(file.Name, outPath, 0, nil)
  data, _ := os.ReadFile(outPath)
  fmt.Printf("  Content: %s\n", string(data))
}
import 'dart:io';

final tempRoot = Directory.systemTemp.createTempSync('bao');

// Alice writes version 1
var dataPath = File('${tempRoot.path}/data.txt');
dataPath.writeAsStringSync('Version 1: Initial draft');
await vault.write('docs/status.txt', src: dataPath.path);

// Alice updates to version 2
dataPath.writeAsStringSync('Version 2: Reviewed by Bob');
await vault.write('docs/status.txt', src: dataPath.path);

// Alice updates to version 3
dataPath.writeAsStringSync('Version 3: Final approved');
await vault.write('docs/status.txt', src: dataPath.path);

// Bob retrieves all versions
final versions = await vault.versions('docs/status.txt');
for (int i = 0; i < versions.length; i++) {
  final file = versions[i];
  print('Version $i: ${file.name}');
  final outPath = File('${tempRoot.path}/version_$i.txt');
  await vault.read(file.name, outPath.path);
  print('  Content: ${await outPath.readAsString()}');
}
import java.nio.file.Files;
import java.util.List;

var tempRoot = Files.createTempDirectory("bao");

// Alice writes version 1
var dataPath = tempRoot.resolve("data.txt");
Files.writeString(dataPath, "Version 1: Initial draft");
vault.write("docs/status.txt", new byte[0], dataPath.toString(), 0L);

// Alice updates to version 2
Files.writeString(dataPath, "Version 2: Reviewed by Bob");
vault.write("docs/status.txt", new byte[0], dataPath.toString(), 0L);

// Alice updates to version 3
Files.writeString(dataPath, "Version 3: Final approved");
vault.write("docs/status.txt", new byte[0], dataPath.toString(), 0L);

// Bob retrieves all versions
List<FileInfo> versions = vault.versions("docs/status.txt");
for (int i = 0; i < versions.size(); i++) {
  var file = versions.get(i);
  System.out.println("Version " + i + ": " + file.name);
  var outPath = tempRoot.resolve("version_" + i + ".txt");
  vault.read(file.name, outPath.toString(), 0);
  System.out.println("  Content: " + Files.readString(outPath));
}

Retention

Data stored in the vault has a retention period. By default, Bao keeps data for one month. After this period, older versions of files and deleted files are automatically removed from storage to reduce costs and free up space.

The retention policy is important for managing storage costs, especially when dealing with large volumes of data or frequent updates. When creating a vault, administrators can configure a custom retention period that suits their organization’s needs.

from baolib import *

# Create a vault with custom retention (30 days = 2592000 seconds)
store_config = {
    'type': 's3',
    'id': 'bao-test',
    's3': {
        'bucket': 'my-bucket',
        'endpoint': 's3.amazonaws.com',
        'prefix': 'bao-test',
        'auth': {
            'accessKeyId': 'your-access-key-id',
            'secretAccessKey': 'your-secret-access-key'
        }
    }
}
store = Store(store_config)
db = DB('sqlite3', 'my_db_path.db')

# Configuration with retention in seconds (30 days)
config = {
    'retention': 2592000  # 30 days in seconds
}
vault = Vault.create(Vault.users, alice_secret, store, db, config)
import (
  "time"
  "github.com/stregato/bao/lib/vault"
)

// Create a vault with custom retention (30 days)
config := &vault.Config{
  Retention: 30 * 24 * time.Hour,
}
v, err := vault.Create(vault.Users, store, db, config)
if err != nil {
  panic(err)
}
import 'package:bao/bao.dart';

// Create a vault with custom retention (30 days)
final config = VaultConfig(
  retention: Duration(days: 30),
);
final vault = await Vault.create(
  users,
  aliceSecret,
  store,
  db,
  config: config,
);
import ink.francesco.bao.Vault;
import java.util.HashMap;
import java.util.Map;

// Create a vault with custom retention (30 days in milliseconds)
Map<String, Object> config = new HashMap<>();
config.put("retention", 30L * 24 * 60 * 60 * 1000); // 30 days in milliseconds
var vault = Vault.create(Vault.USERS, aliceSecret, store, db, config);

Synchronization and latency

Synchronization between local state and the metadata stored in the backend is time-consuming. Bao implements a cooldown period (default 5 seconds) to avoid excessive sync attempts. This means changes from other peers may not appear immediately and applications should handle eventual consistency.

When immediate synchronization is critical, you can explicitly call the vault’s sync method to force a refresh.

from baolib import *
import time

# Vault created with default syncCooldown of 5 seconds
vault = Vault.create(Vault.users, alice_secret, store, db)

# Read files - sync happens automatically if cooldown has passed
files = vault.readDir('/')

# Wait for another vault to write a file...
time.sleep(6)  # Wait longer than syncCooldown

# Next readDir will sync and show the new file
files = vault.readDir('/')

# Force explicit sync when needed
vault.sync()
files = vault.readDir('/')
import (
  "time"
  "github.com/stregato/bao/lib/vault"
)

// Vault created with default syncCooldown of 5 seconds
v, _ := vault.Create(
  vault.Users,
  store,
  db,
  &vault.Config{},
)

// Read files - sync happens automatically if cooldown has passed
files, _ := v.ReadDir("/")

// Wait for another vault to write a file...
time.Sleep(6 * time.Second)

// Next ReadDir will sync and show the new file
files, _ = v.ReadDir("/")

// Force explicit sync when needed
v.Sync()
files, _ = v.ReadDir("/")
import 'package:bao/bao.dart';

// Vault created with default syncCooldown of 5 seconds
final vault = await Vault.create(
  users,
  aliceSecret,
  store,
  db,
);

// Read files - sync happens automatically if cooldown has passed
var files = await vault.readDir('/');

// Wait for another vault to write a file...
await Future.delayed(Duration(seconds: 6));

// Next readDir will sync and show the new file
files = await vault.readDir('/');

// Force explicit sync when needed
await vault.sync();
files = await vault.readDir('/');
import ink.francesco.bao.Vault;
import java.util.List;

// Vault created with default syncCooldown of 5 seconds
var vault = Vault.create(
  Vault.USERS,
  aliceSecret,
  store,
  db
);

// Read files - sync happens automatically if cooldown has passed
List<String> files = vault.readDir("/");

// Wait for another vault to write a file...
Thread.sleep(6000);  // 6 seconds

// Next readDir will sync and show the new file
files = vault.readDir("/");

// Force explicit sync when needed
vault.sync();
files = vault.readDir("/");

Configuration

The vault can be customized through configuration parameters passed at creation time. These parameters control the vault’s behavior, including how often data is synchronized, how much data can be stored, and other operational aspects.

Understanding the configuration options helps optimize the vault for different use cases. Whether managing small personal data or large enterprise systems, proper configuration ensures the vault operates as expected.

from baolib import *

store_config = {
    'type': 's3',
    'id': 'bao-production',
    's3': {
        'bucket': 'company-data',
        'endpoint': 's3.amazonaws.com',
        'prefix': 'bao-vault',
        'auth': {
            'accessKeyId': 'your-access-key-id',
            'secretAccessKey': 'your-secret-access-key'
        }
    }
}
store = Store(store_config)
db = DB('sqlite3', 'production.db')

# Create vault with configuration
config = {
    'retention': 2592000,          # 30 days in seconds
    'maxStorage': 1073741824,      # 1 GB in bytes
    'syncCooldown': 5,             # 5 seconds minimum between syncs
    'filesSyncPeriod': 600,        # 10 minutes
    'cleanupPeriod': 3600,         # 1 hour
    'ioThrottle': 10               # Max 10 concurrent I/O operations
}
vault = Vault.create(Vault.users, alice_secret, store, db, config)
import (
  "time"
  "github.com/stregato/bao/lib/vault"
)

config := &vault.Config{
  Retention:       30 * 24 * time.Hour,
  MaxStorage:      1073741824,     // 1 GB in bytes
  SyncCooldown:    5 * time.Second,
  FilesSyncPeriod: 10 * time.Minute,
  CleanupPeriod:   1 * time.Hour,
  IoThrottle:      10,
}
v, err := vault.Create(
  vault.Users,
  store,
  db,
  config,
)
if err != nil {
  panic(err)
}
import 'package:bao/bao.dart';

final config = VaultConfig(
  retention: Duration(days: 30),
  maxStorage: 1073741824,     // 1 GB in bytes
  syncCooldown: Duration(seconds: 5),
  filesSyncPeriod: Duration(minutes: 10),
  cleanupPeriod: Duration(hours: 1),
  ioThrottle: 10,
);
final vault = await Vault.create(
  users,
  aliceSecret,
  store,
  db,
  config: config,
);
import ink.francesco.bao.Vault;
import java.util.HashMap;
import java.util.Map;

Map<String, Object> config = new HashMap<>();
config.put("retention", 30L * 24 * 60 * 60 * 1000);  // 30 days in milliseconds
config.put("maxStorage", 1073741824L);               // 1 GB in bytes
config.put("syncCooldown", 5000L);                   // 5 seconds in milliseconds
config.put("filesSyncPeriod", 600000L);              // 10 minutes in milliseconds
config.put("cleanupPeriod", 3600000L);               // 1 hour in milliseconds
config.put("ioThrottle", 10L);                       // Max 10 concurrent operations

var vault = Vault.create(
  Vault.USERS,
  aliceSecret,
  store,
  db,
  config
);