Skip to content

Open source database · Apache-2.0

DDBD

Schemaless record database with writer-based access control (WBAC).

Let your agents work on your data without an admin in the loop.

curl -fsSL https://cdn.broadwayinc.com/get/ddbd/v1 | sh
  • Linux and macOS
  • One binary
  • HTTP and JSON
Writer-based access controlalice saves a record at access level 05. DDBD keeps one sorted key range per access level. An agent key capped at access level 05 queries access levels 00 to 05 and reads the record. carol, at access level 00, queries access level 00 only and never reaches it.WRITERaliceaccess 10productsaccess 05tag saleDDBDsorted by access9910050403020100· · ·· · ·READERSagent keymax access 05reads itcarolaccess 00never reads it
The writer sets each record's access level. A reader's query covers only the access levels that reader may read, so it never reaches the rest.

00 · The solution

No server in the middle

With writer-based access control (WBAC), the client that writes a record decides who may read it, and DDBD enforces that on every request. The access rules live with the data, inside the database.

So browsers, serverless functions and AI agents talk to DDBD directly over HTTP, each with its own sign-in. Clients get full control of their data, and there is no backend to write, host or scale just to guard it.

Without a server in the middleUsually, an app or an AI agent reaches its data through a server in the middle that checks every request against rules someone wrote, and which someone has to build, host and scale. With DDBD's writer-based access control, the writer of each record sets who may read it, and DDBD enforces it on every request, so a browser, a serverless function and an AI agent each call it directly with their own sign-in.USUALLYapp oragentserver in the middlechecks every requestagainst your rulesyou build, host and scale itdatabasetrusts theserverWITH DDBDbrowserown sign-infunctionown sign-inAI agentagent keynothing in the middleDDBDWBAC: the writer ofeach record sets whomay read it, checkedon every request

Truly serverless

Your app needs no backend of its own to guard its data. Browsers and serverless functions call DDBD directly, and the database checks each record’s rules itself.

Safer AI agents

Each agent gets its own key: capped at an access level, limited to some collections, read-only if you want, set to expire, and revocable at any time. What it saves names the key that saved it.

Scales at low cost

Nothing in the middle to run: clients talk to one binary. Each record’s access level and keys are written with it, so a query reads one ready range and its cost follows the page it returns, not the size of the data.

01 · Storage

No schema, JSON store

There is no schema and no index to set up. With each record, the user who saves it names its collection, access level, tags and indexes. DDBD turns those into sorted keys and writes them together with the record in one atomic step.

A query reads one range of those keys for each access level the reader may read, merged in order. It starts at the first match and stops when the page is full, so its cost follows the page, not the size of the collection.

// postRecord body
{
  "data": { "title": "Desk Lamp", "color": "red" },
  "config": {
    "collection": { "name": "products", "access_level": "05" },
    "index": [{ "name": "price", "value": 1200 }],
    "tags": ["sale"]
  }
}
How a record is stored and foundSaving a record inserts its key at its place in a sorted list, and a query reads one contiguous range of that list. 4 examples: A number index: a product at price 1200 with the tag sale. The query for sale products from price 1000 reads 1200, 1800 and 2100; the product at access level 07 is out of its range. Another number index: a review rated 5 with the tag verified. A reader at access level 00 reads the verified reviews rated 4 and 5; the review at access level 02 is out of its range. A string index: a product titled Desk Lamp with the tag new. The query for new products whose title starts with Desk reads Desk Chair, Desk Lamp and Desk Mat, and stops at Floor Lamp. Loose match: a product titled Café Lamp. Loose keys ignore case and accents, so the query for titles starting with cafe finds Café Lamp, CAFÉ Sign and Cafe Table.postRecord products · 05 · sale · price 1200getRecords tag sale · price >= 1000 · reader 05COLLECTIONACCESSTAGINDEXVALUERECORDproducts03saleprice880VZOat3k…products05newprice1500VZOat2q…products05saleprice990VZOat4b…products05saleprice1200VZYu683…products05saleprice1800VZOat4m…products05saleprice2100VZOat4j…products07saleprice1100VZOat4o…
The saved record's key goes in at its place in the sorted list. The query reads one range, from the first match to the last, and never touches keys of other tags or of access levels the reader may not read.

03 · Agents

An agent key's limitsThe agent key shop-bot may read the products collection at access levels 00 to 05 and may not write. Reading products works; reading orders is refused because the key is not allowed that collection; saving a record is refused because the key is read-only.AGENT KEYshop-botcollections productsmax_level 05read_only trueexpires 30dWHAT IT CAN READproductsordersnotes00 to 0506 to 99privatealice/shop-bot> get products --index "price >= 1000"VZOat4bWCVbRBGAq products/05 [price=1200 ...]alice/shop-bot> get ordersNO_ACCESS: This agent key is not allowed touse the collection "orders".alice/shop-bot> post products --data '{"title":"Lamp"}'NO_ACCESS: This agent key is read-only.
An agent key signed in to the user shell. The answers are the server's own.

Agents get a key, not a password

A user makes a key for each agent or script, without asking the admin, and can revoke it at any time. A key can be limited to some collections, to a highest access level, to read-only, to private records or not, and to an expiry date.

An agent reads only what its user may read, within the key's limits. What it saves belongs to the user and names the key that saved it.

// createAgentKey body
{
  "name": "shop-bot",
  "collections": ["products"],
  "max_level": "05",
  "read_only": true,
  "expires": "30d"
}

// a record the key saved (some fields)
{
  "record_id": "VZYu683bLzapjrgk",
  "user_id": "JFeCedoO",
  "agent": { "key_id": "mtxBMs", "name": "shop-bot" },
  "data": { "title": "Desk Lamp" }
}
Agent keys

04 · Encryption

Data the admin cannot read

Access levels decide who can fetch a record. For data that must stay secret even from the admin and from backups, the app encrypts it before saving.

DDBD gives every user a salt and the settings to derive a key from their password with Argon2id. The key is made and used on the user's device, the same on every device they sign in on. The server stores only the result.

Client-side encryption
Client-side encryptionDDBD sends the user's enc_salt and key-derivation settings. On the device, Argon2id turns the password and the salt into a key, and AES-GCM encrypts the record data with it. Only the ciphertext is saved in DDBD; the key never leaves the device.ON THE USER'S DEVICEDDBDpasswordcorrect-horseenc_salt, kdf9f2c… argon2idenc_salt9f2c…Argon2idkeydatasecret noteAES-GCMdatau2Vq9L…savedu2Vq9L…no keyhere

05 · Performance

Query time grows with the answer, not with the database

How long DDBD, PostgreSQL, SQLite and MongoDB take to answer the same query as more records are stored.

Query to be fetched by

100% is close to a full fetch with nothing filtered out. The low numbers are the queries that matter.

1,000,000 records, median of 3 runs of 300 queries

DDBD 0.1.22.7 ms
PostgreSQL 16.215.8 ms
SQLite 3.46.064.7 ms
MongoDB 8.0.418.2 ms

DDBD answered 5.8× faster than PostgreSQL, 24× faster than SQLite and 6.7× faster than MongoDB.

0.1 ms1 ms10 ms100 ms10k records100k records1M recordsSQLite64.7 msMongoDB18.2 msPostgreSQL15.8 msDDBD2.7 ms
Median time per query, log scale, by number of records stored.
All results at 1,000,000 records
QueryCaller may readRecords returnedDDBDPostgreSQLSQLiteMongoDB
tag sale, price >= X0.1%21.71.8 ms10.8 ms65.8 ms4.0 ms
tag sale, price >= X1%46.32.7 ms15.8 ms64.7 ms18.2 ms
tag sale, price >= X10%503.5 ms12.1 ms68.2 ms30.9 ms
tag sale, price >= X100%504.9 ms3.1 ms65.2 ms6.0 ms
price >= X, no tag0.1%48.12.1 ms4.5 ms92.3 ms4.1 ms
price >= X, no tag1%49.93.1 ms7.5 ms10.6 ms16.2 ms
price >= X, no tag10%503.6 ms2.5 ms2.5 ms4.3 ms
price >= X, no tag100%503.7 ms2.0 ms1.6 ms2.9 ms

Without a tag, an index on price alone serves the query well once the user may read 10% or more of the records, and PostgreSQL and SQLite answer faster than DDBD. With a tag, a SQL database has to combine the tag index, the price order and the access filter at query time; DDBD wrote a key that already holds all three when the record was saved. When the user may read every record there is no access filter to combine, and PostgreSQL answers the tag query faster. MongoDB combines its indexes at query time as the SQL databases do, so it slows down with the tag and the access filter too; without them, when the user may read every record, it is faster than DDBD.

How this was measured
  • Machine: Intel Core i7-1160G7 laptop (4 cores, 8 threads), 15 GB RAM, Linux 7.0. All data in memory.
  • Data: 10,000, 100,000 and 1,000,000 products from one seeded generator: a price from 1 to 1,000,000 and one of 20 tags (sale on 5%). Each record got an access level: 00 on 0.1% of the records, 01 on 0.9%, 02 on 9% and 03 on 90%. A user may read records at their own access level or below, so users at access level 00, 01, 02 and 03 may read 0.1%, 1%, 10% and 100% of the records.
  • The same request for all four: every query is the same HTTP POST with a JSON body and a session token, and every answer is the 50 full records as JSON, the same bytes from each database. PostgreSQL, SQLite and MongoDB answer through a small HTTP server written for this test with the same libraries as DDBD's (hyper, tokio, serde_json). Like DDBD, it reads the token, looks up the user's access level, runs the query and writes the JSON answer.
  • DDBD 0.1.2: default settings; getRecords.
  • PostgreSQL 16.2: one table with the record id, collection, access level, price, tags and the record JSON; indexes on (collection, price, id), (collection, access level, price, id) and a GIN index on tags. 2 GB shared buffers, JIT off, a plan for each query's values. Its own process, reached by the HTTP server over its local socket, as any application reaches it.
  • SQLite 3.46.0: the same table and two indexes, with the tags in their own indexed table; 2 GB page cache. Runs inside the HTTP server's process, as RocksDB runs inside DDBD's.
  • MongoDB 8.0.4: one document per record with the record id, collection, access level, price, tags and the record itself; the same indexes as PostgreSQL: (collection, price, id), (collection, access level, price, id) and tags. 2 GB cache. Its own process, reached by the HTTP server over its local socket; a query is one find sorted by price and id.
  • Runs: one Python client on the same machine, one query at a time over a kept-alive connection: 300 queries per case with random price values, after 30 warm-up queries. The four databases took turns query by query, so any change in the machine's speed reached all four alike. The whole set was run 3 times; each number is the median of the runs' medians, because the laptop's speed changed by up to a third from one run to the next. All four returned the same records in the same order for every query.
  • Settled databases: after loading, DDBD was restarted and left idle until its storage engine had finished sorting the new data into place, and PostgreSQL was checkpointed and analyzed, so no database was still busy with its load while it answered.

06 · Operation

Small to run

No schema

Save any JSON. Collections, indexes and tags exist as soon as a record uses them.

HTTP and JSON

Every call is POST /v1/<method> with a bearer token. Any language with an HTTP client works.

One binary

RocksDB is compiled in. One command installs it; there is no other service to run.

Atomic writes

A record and all of its keys are written in one step, so no query sees half a record.

Live backups

Back up, export and import while the server keeps answering requests.

User shell

A terminal client to try any request as a user or as an agent key.

07 · Web admin

Run it from a browser

The web admin comes with the server, on the same address and port as the API. See every record, run any query, add users and agent keys, change settings, take backups, and stop or restart the server.

It stays off until the operating system user who runs DDBD sets its password on the server. Nobody can turn it on from the outside.

Web admin
The web admin's Records screen: a table of records with their collection, access level, owner, indexes and tags

08 · Install

Running in a minute

  1. 1

    Install

    # Linuxcurl -fsSL https://cdn.broadwayinc.com/get/ddbd/v1 | sh

    x86_64 or arm64, glibc 2.34 or newer. Puts ddbd in /usr/local/bin, or in ~/.local/bin without sudo.

    # macOS 11 or newer, Apple Silicon or Intelbrew install broadwayinc/ddbd/ddbd
  2. 2

    Create a data folder

    ddbd init --data ~/ddbd-data
  3. 3

    Start the server

    ddbd start --data ~/ddbd-data

    Answers on http://localhost:7070.

  4. 4

    Turn on the web admin

    ddbd admin --data ~/ddbd-data web-password

    Then open http://localhost:7070 and add your first user.

DDBD speaks plain HTTP by default: run it behind HTTPS, or set it up with --tls. Getting started walks through the first user, record and query.

The database design behind Skapi

skapi.com is built on the DDBD record design. It serves developers worldwide and handles rapidly growing traffic from AI agents.

skapi.com

Free and open source

DDBD is licensed under Apache-2.0. Use it, read it, change it.

Released under the Apache License 2.0.