Backbone
Seven data primitives in one service — no databases to provision.
Backbone is your slice's encrypted data layer. Your functions reach it through the SDK with a single call — no connection strings, no ORM, no migrations. Collections, queues, and the rest spring into existence the first time you use them, and you can also seed and inspect them from the CLI.
The primitives
| Primitive | What it's for | CLI |
|---|---|---|
| NoSQL | JSON document collections with lookups by id and field filters. | drift backbone nosql |
| SQL | Per-slice SQLite databases with schemas and transactions. | Driftfile + SDK |
| Blobs | Binary object storage for files and uploads, organised by bucket. | drift backbone blob |
| Secrets | Encrypted configuration (AES-256-GCM), injected into your functions. | drift backbone secret |
| Queues | FIFO message queues that can trigger functions. | drift backbone queue |
| Cache | Fast key/value store with an optional TTL. | drift backbone cache |
| Locks | Distributed locks so work isn't processed twice. | drift backbone lock |
| Realtime | In-slice pub/sub — live messages fanned out to subscribed browser clients over WebSocket. | WebSocket |
Inspect your slice's data and usage any time with drift backbone status.
NoSQL
Document collections. From a function:
// insert (upserts on the document's id) and read back
id, _ := drift.NoSQL.Collection("users").Insert(map[string]any{"name": "Alice", "role": "admin"})
admins, _ := drift.NoSQL.Collection("users").List(map[string]string{"role": "admin"})
The interpreted SDKs mirror this in lowercase — drift.nosql.collection("users").insert(doc). From the CLI:
drift backbone nosql write --collection users --data '{"name":"Alice","role":"admin"}'
drift backbone nosql list --collection users --field role --value admin
drift backbone nosql read --collection users
drift backbone nosql drop users
Collections can be seeded at deploy time from a JSONL file — see the Driftfile.
SQL
When you need relational queries and transactions, declare a SQLite database in your
Driftfile (with an optional schema and seed) and reach it by
name from the SDK:
rows, _ := drift.SQL("app").Query("SELECT * FROM orders WHERE status = ?", "open")
res, _ := drift.SQL("app").Execute("INSERT INTO orders (item, qty) VALUES (?, ?)", "widget", 3)
Wrap multiple statements in drift.SQL("app").Begin() for a transaction. SQL lives in the Driftfile and the SDK — there's no separate CLI command for it.
Blobs
Binary object storage. Files are addressed as bucket/key (a bare name lands in the default bucket):
drift.Blob.Put("uploads/photo.jpg", data, "image/jpeg")
photo, _ := drift.Blob.Get("uploads/photo.jpg")
drift backbone blob put uploads photo.jpg ./photo.jpg
drift backbone blob get uploads photo.jpg > downloaded.jpg
drift backbone blob list uploads
drift backbone blob delete uploads photo.jpg
Secrets
Encrypted at rest with AES-256-GCM. Declare which secrets a function may read with the
secrets= directive keyword, then read them with the SDK (they arrive as environment
variables):
// @atomic http=post:charge auth=apikey secrets=STRIPE_KEY
key, _ := drift.Secret.Get("STRIPE_KEY")
drift backbone secret set STRIPE_KEY=sk-123abc
drift backbone secret get STRIPE_KEY
drift backbone secret list
drift backbone secret delete STRIPE_KEY
You can also set secrets declaratively in the Driftfile (literal values or $ENV references resolved at deploy).
At runtime a function reads secrets — it doesn't create them. Name the secrets a
function needs in its secrets= directive (secrets=STRIPE_KEY,SIGNING_KEY) and
the runner injects each as an environment variable that drift.Secret.Get(name) returns.
Reading a secret a function didn't declare is rejected, and so is a runtime
Secret.Set — secrets are provisioned out of band, with
drift backbone secret set or declaratively in the
Driftfile.
That makes Secrets the right home for a value your app must keep stable across cold starts, like a signing key: generate it out of band, set it once, declare it, then read it — validating the length so a missing or unreadable secret fails loudly instead of signing with garbage.
# provision once, out of band — never from inside a function
drift backbone secret set SIGNING_KEY=$(openssl rand -hex 32)
// @atomic http=post:sign secrets=SIGNING_KEY
func signingKey() ([]byte, error) {
hex, err := drift.Secret.Get("SIGNING_KEY") // from the injected env var
if err != nil || len(hex) != 64 {
return nil, fmt.Errorf("SIGNING_KEY missing or malformed")
}
return decodeHex(hex)
}
If all you need is a token for user sessions, you don't need a key of your own at all — the platform's JWT signing key is managed for you.
Queues
FIFO message queues. Push from anywhere; process messages with a queue-triggered function:
drift.Queue("orders").Push(map[string]any{"item": "widget", "qty": 3})
drift backbone queue push orders '{"item":"widget","qty":3}'
drift backbone queue peek orders # look without removing
drift backbone queue pop orders # take the next message
drift backbone queue len orders
Wire a function to a queue with // @atomic queue=orders and it runs for every message — see Atomic.
Cache
A fast key/value store with an optional TTL in seconds (0 = no expiry):
drift.Cache.Set("session:abc", token, 3600)
v, _ := drift.Cache.Get("session:abc")
drift backbone cache set greeting "hello" --ttl 3600
drift backbone cache get greeting
drift backbone cache exists greeting
drift backbone cache del greeting
Locks
Distributed locks stop two workers doing the same job at once. Acquire returns a token you present to Release:
token, err := drift.Lock.Acquire("payout:42", 30) // hold for up to 30s
if err == nil {
defer drift.Lock.Release("payout:42", token)
// ... do the work exactly once ...
}
drift backbone lock acquire payout-42 worker-1 --ttl 30
drift backbone lock renew payout-42 worker-1 --ttl 30
drift backbone lock release payout-42 worker-1
Acquiring a lock that's already held returns a conflict, so the second worker simply backs off.
Realtime
In-slice pub/sub over WebSocket. Browser clients open a socket to a named channel; anything one client sends is fanned out to every other client on that channel, live. There's no broker to run and no second service — the channel lives in the slice, same-origin with your Canvas site, so there's no cross-origin handshake.
// in your Canvas site's JavaScript — join a channel and react to others
const ws = new WebSocket(`wss://${location.host}/realtime/board:42`);
ws.onmessage = (e) => applyDelta(JSON.parse(e.data));
// broadcast your own change to everyone else on the channel
ws.send(JSON.stringify({ type: "move", x: 10, y: 20 }));
This is what drives live cursors and collaborative editing in the Myral whiteboard demo: clients exchange cursor and element deltas over the channel, and a normal function persists the result for durability and late-joiner catch-up. The number of concurrent connections is capped by your tier.
stream=sse or stream=ws to the
function — see Atomic.