@paykernel/store-durable-objects is per-partition strong coordination on SQLite-backed Durable Objects (new_sqlite_classes). The Worker client is async (stub RPC). In-object SQL is sync (storage.sql.exec). Single root export.
Version 0.1.1 — published on npm as @paykernel/store-durable-objects. Install: bun add @paykernel/store-durable-objects.
Install
bun add @paykernel/store-durable-objects
# optional DX types (not required at runtime):
bun add -d @cloudflare/workers-typespaymentsSdk.runtime: "cloudflare-only". Root does not static-import cloudflare:workers. No Cloudflare REST API or account token for store construction.
Quick start (Worker + sharding)
import {
createDoPaymentStores,
RECOMMENDED_HASH_PARTITIONS,
} from "@paykernel/store-durable-objects";
const stores = createDoPaymentStores({
namespace: env.PAYMENTS_DO,
sharding: { kind: "hash", partitions: RECOMMENDED_HASH_PARTITIONS },
});
const r = await stores.idempotency.reserve({
key: "pay_123",
fingerprint: "fp",
owner: "worker-1",
leaseMs: 30_000,
});
// 1) claim 2) exit storage txn 3) external provider work 4) complete with leaseTokencreateDoPaymentStores does not migrate and does not default to a global object. kind: "global" / "singleton" is rejected. RECOMMENDED_HASH_PARTITIONS is 16. The package README also shows partitions: 32.
Direct storage path (tests / in-object)
import {
createDoPaymentStoresFromStorage,
migrateDoAdapter,
PaymentsStoreObject,
} from "@paykernel/store-durable-objects";
await migrateDoAdapter(storage); // DoStorageLike or DoExecutor
const bundle = createDoPaymentStoresFromStorage({ storage });
const object = new PaymentsStoreObject({ storage });
await object.ensureSchema();Worker-client withTransaction hard-fails (StoreUnsupportedFeatureError) — no cross-object multi-mutation atomicity. In-object createDo*Store / PaymentsStoreObject may use transactionSync.
Wrangler (SQLite-backed DO)
Use new_sqlite_classes, not legacy KV-only new_classes. This adapter requires storage.sql / transactionSync.
[[durable_objects.bindings]]
name = "PAYMENTS_DO"
class_name = "PaymentsStoreDurableObject"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["PaymentsStoreDurableObject"]Call ensureDoSchema / migrateDoAdapter in the DO constructor (for example blockConcurrencyWhile) or via ops — not on package import. That constructor path is DO lifecycle, not npm-import auto-migrate.
Forward every name in REQUIRED_DO_RPC_METHODS from your wrapper onto PaymentsStoreObject. Hash sharding requires bindHashPartitionLayout (DO-1). tableNamespace is sent on every store RPC and applied inside the DO.
Official SQLite storage API: Cloudflare Durable Objects SQLite storage (adapter pin: 2026-08-03).
Sharding
| Strategy | Behavior |
|---|---|
key |
One object per key — strongest per-key serialization. No global listDue / listRetryable / deleteExpired (hard-fail StoreUnsupportedFeatureError) |
hash |
Bounded partitions (partitions >= 1, recommend ≥ 16 / RECOMMENDED_HASH_PARTITIONS). Fan-out discovery. partitions = 1 is a single partition (all keys share one DO; not a silent global default) |
tenant |
One object per tenant. Worker strategy: static tenantId string, or a function of key only (store contracts have no tenantId) |
- Within a shard: single-threaded serialization.
- Across shards: no global total order.
- Hot-key: many ops for the same key/tenant hit the same object.
Hash partitions are sealed on a layout meta DO named with suffix __pk_layout__ (DO_HASH_LAYOUT_META_SUFFIX). Changing partition count under the same layout hard-throws. Use a new layoutId / objectNamePrefix to reshard after an explicit migration — never silent empty DOs.
createDoPaymentStores({
namespace: env.PAYMENTS_DO,
sharding: { kind: "hash", partitions: RECOMMENDED_HASH_PARTITIONS },
});Claims and transactions
Prefer engine-level single-statement:
INSERT … ON CONFLICT DO UPDATE … WHERE … RETURNING …Multi-statement only inside storage.transactionSync — sync callback, no await. Never BEGIN/COMMIT via sql.exec. Never unprotected get-then-set.
Pattern: claim → commit → external work → complete with lease token. Do not await provider HTTP inside transactionSync. Consume SqlStorageCursor (toArray) before any await — no snapshot isolation.
Guarantees
DO_STORAGE_ADAPTER_MANIFEST (name: "cloudflare-do"):
| Field | Value |
|---|---|
coordinationScope |
multi-host (partitioned) |
durability |
durable |
consistency.claims |
strong (within partition) |
consistency.readAfterWrite |
strong (within one DO instance) |
staleReadsPossible |
false (within partition) |
Optional alarms (default-off)
createAlarmScheduler / ensureAlarmQueueSchema. One alarm per DO + due queue table. Handlers are at-least-once and must re-check lease/claim state. Bounded retries + backoff/jitter.
Alarms are not wired to failWebhook — webhook recovery is pull-only (listRetryable). Calling alarm() without a handler only re-schedules; it does not drain.
Failure paths
| Event | Behavior |
|---|---|
| Crash / eviction after claim, before complete | Lease until expiry; reclaim with new token on that partition |
| Crash after provider work, before complete | Uncertain — markIndeterminate if lease still valid; never invent failure |
| Stale token after reclaim | StoreLeaseLostError |
listDue with kind: "key" |
StoreUnsupportedFeatureError — prefer hash for recovery pollers |
Worker-client withTransaction |
StoreUnsupportedFeatureError |
| One global DO / omitted sharding | Forbidden — must pass sharding |
KV-only new_classes |
Unsupported — no storage.sql |
Never fulfill in onWebhookVerified. Fulfill after inbox claim on paid rematched events only. See Webhooks.