Integrationsstorage
Cloudflare D1 storage
Keep Mayura's workflows, jobs, memory, budgets and API keys in Cloudflare D1 from a Worker with @mayurajs/storage-d1.
- Package
- @mayurajs/storage-d1
- Install
- npm install mayura @mayurajs/storage-d1
- Vendor
- Cloudflare ↗
Cloudflare D1 is Cloudflare's serverless SQLite database, reached from a
Worker through a binding. @mayurajs/storage-d1 is a Mayura store on D1. It holds everything
the SQL stores hold: run records and their events (the server's submission journal and run records), durable workflows
with their graphs, trees, waits and discovery, the leased job scheduler, durable budgets, native memory and
API keys. It has no dependencies and no driver: it calls the D1 binding your
Worker receives (env.DB), so it runs inside a Cloudflare Worker, where that binding exists.
Install
npm install mayura @mayurajs/storage-d1Credentials
The binding is the credential: there is no key, token or URL to pass. Create a database in the
Cloudflare dashboard or with wrangler d1 create, then bind it
to your Worker in its Wrangler configuration:
[[d1_databases]]
binding = "DB"
database_name = "mayura"
database_id = "<your database id>"The package reads nothing from the environment.
Use it
import { createKeyManager } from 'mayura/keys';
import { createD1Store, type D1Database } from '@mayurajs/storage-d1';
export default {
async fetch(request: Request, env: { DB: D1Database }): Promise<Response> {
const store = createD1Store({ database: env.DB });
await store.initialize();
// Check the caller's API key against the keys kept in D1.
const keys = createKeyManager({ store, prefix: 'acme' });
const presented = request.headers.get('authorization')?.replace(/^Bearer /u, '') ?? '';
const verified = await keys.verify(presented);
return new Response(verified.ok ? 'ok' : 'unauthorized', { status: verified.ok ? 200 : 401 });
},
};D1Database is the part of the binding the store uses, typed by shape, so you need no Workers types for it.
Native memory, durable budgets and the scheduler each have their own initialize() (see
What uses the store).
Options
| Option | Default | Meaning |
|---|---|---|
database |
required | The D1 binding from your Worker's environment, such as env.DB. |
table |
mayura_documents |
The table that holds Mayura's documents: letters, digits and _, starting with a letter or _, at most 63 characters. |
retryForMs |
30,000 | How long a transaction keeps running again after conflicts before it fails, from 0 to 600,000 ms. |
Any other option fails with INVALID_INPUT when you call createD1Store().
Setup and migrations
initialize() creates the one table the store needs (mayura_documents, or your table) when it is missing: a
WITHOUT ROWID table keyed by partition and sort key, with a version and a JSON body per document. Each feature's own
initialize() only marks it ready, since documents need no further schema. See
Migrations for how layout changes ship.
Transactions and concurrency
D1 has no interactive transactions: it commits a batch() of statements as one SQLite transaction, all or nothing. The
store runs Mayura's state machines as optimistic transactions over it:
- A transaction reads with plain queries and holds its writes. Reads of several keys go in one batch, so they see one moment.
- It commits one batch: first a guard per document it read or locked, which fails the whole batch if that document's version changed, then its writes.
- If a guard fails, the transaction runs again with fresh reads after a short random pause, for up to
retryForMs. Two writers of one record never both succeed. - Leases and deadlines use D1's clock, measured again every second, so every Worker agrees on when a lease ends.
Every call is a round trip to the database and a transaction makes several, so place your Worker near its database. Under heavy contention on one record, transactions run again rather than wait; different records never contend. Memory search scans a scope's term index and ranks it in your Worker, so it grows with the number of records in the scope. See D1 and DynamoDB.
Errors
Failures reach you as a StorageError with a storageCode; messages never contain D1's text. See
Errors for every code.
storageCode |
When |
|---|---|
INVALID_INPUT |
The binding is missing or not a D1 binding, the table name is invalid, retryForMs is out of range, or an option is unknown. |
CONFLICT |
A record changed after it was read, or an id or idempotency key already holds other content. |
STORAGE_UNAVAILABLE |
Concurrent writers kept changing the same records for longer than retryForMs, D1 failed, or a stored document failed its integrity check. |
STORE_NOT_INITIALIZED |
A call came before await store.initialize(). |
STORE_CLOSED |
A call came after close(). |
How it is tested
- Every storage, memory and workflow conformance suite the SQL stores pass, crash tests included, runs on the store's own D1 code over a local SQLite file (D1 is SQLite, and a batch is one transaction): records, the key manager, identity integrity, scheduled bounds, memory and native memory, workflows, the scheduler, scheduled workflows, graphs, graph discovery and coordination, execution waits, durable budgets and workflow trees.
- On Cloudflare's own D1 implementation, run locally in Miniflare (workerd), a test stores records and events and has eight writers race on one record, each landing exactly once.
scripts/live/storage-d1.mjschecks a real D1 database from outside a Worker, with D1's HTTP API standing in for the binding: a failing batch rolls back, then the store migrates, writes and reads a record, and issues, verifies and revokes an API key. It has not passed against a real account yet.
Related
- Storage: how D1 and DynamoDB run optimistic transactions.
- DynamoDB: the same document store on Amazon DynamoDB.
- Cloudflare R2 files: files from the same Worker.
- Authentication: API keys kept in this store.