Documentation
D1 adapter
@kynetra/fx-cloudflare ships two D1 adapters: d1Sql for raw parameterised SQL queries and d1Store for a collection-based document-store abstraction. Both implement open port interfaces, so the same application can run against Cloudflare D1 in production and a Postgres database when self-hosted.
Overview#
Cloudflare D1 is a serverless SQLite database that runs at the edge alongside your Workers. Kynetra FX wraps it through two typed adapters that share a common interface contract with the rest of the framework's port system.
d1Sql(db)returns aSqlPort— a thin, typed wrapper over D1's prepared-statement API that handles parameter binding and result mapping.d1Store(db)returns aStorePort— a schemaless document store backed by a singlefx_storetable; suitable for the SaaS kernel, feature flags, or any JSON-document workload.D1_STORE_MIGRATIONis a string constant containing the DDL to create thefx_storetable. Apply it once via Wrangler before usingd1Store.
Note
D1Database binding, which is available on ctx.env.DB (or whatever name you give the binding in wrangler.toml). See Cloudflare Workers for binding setup.Installation#
npm install @kynetra/fx @kynetra/fx-cloudflareDeclare a D1 database binding in your wrangler.toml. The binding name becomes the property name on ctx.env.
[[d1_databases]]binding = "DB"database_name = "my-db"database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"d1Sql — raw SQL#
d1Sql(db) wraps a D1Database binding and returns a SqlPort. Use it when you need full control over your schema, joins, or aggregation queries.
SqlPort interface
| Name | Type | Description |
|---|---|---|
| query<T>(sql, params?) | Promise<T[]> | Run a SELECT query with optional positional parameters. Returns all matching rows typed as T. |
| execute(sql, params?) | Promise<void> | Run an INSERT, UPDATE, DELETE, or DDL statement. Returns nothing. |
import { createFX } from '@kynetra/fx'import { cloudflare, d1Sql } from '@kynetra/fx-cloudflare' const app = createFX({ runtime: 'cloudflare' }) app.get('/users', async (ctx) => { const sql = d1Sql(ctx.env.DB) const users = await sql.query<{ id: string; email: string }>( 'SELECT id, email FROM users WHERE active = ?', [1] ) return ctx.json(users)}) app.post('/users', async (ctx) => { const body = await ctx.jsonBody<{ email: string }>() const sql = d1Sql(ctx.env.DB) await sql.execute( 'INSERT INTO users (id, email, active) VALUES (?, ?, ?)', [crypto.randomUUID(), body.email, 1] ) return ctx.json({ ok: true }, { status: 201 })}) return cloudflare(app)Tip
json_extract, json_object, etc.). Positional parameters use ? placeholders.d1Store — document store#
d1Store(db) returns a StorePort — a collection-based, schemaless document API backed by a single fx_store table. Documents are stored as JSON strings and deserialised automatically on read. This is the persistence layer consumed by the FX SaaS kernel.
StorePort interface
| Name | Type | Description |
|---|---|---|
| get<T>(collection, id) | Promise<T | null> | Fetch a single document by collection name and ID. Returns null if not found. |
| list<T>(collection, filter?) | Promise<T[]> | List all documents in a collection. The optional filter is an object whose keys are matched against top-level document fields. |
| put<T>(collection, record) | Promise<T> | Insert or replace a document. The record must include an id field. Returns the stored document. |
| delete(collection, id) | Promise<void> | Delete a document by collection name and ID. No-ops if the document does not exist. |
import { createFX } from '@kynetra/fx'import { cloudflare, d1Store } from '@kynetra/fx-cloudflare' interface Post { id: string; title: string; body: string } const app = createFX({ runtime: 'cloudflare' }) app.post('/posts', async (ctx) => { const store = d1Store(ctx.env.DB) const body = await ctx.jsonBody<Omit<Post, 'id'>>() const post = await store.put<Post>('posts', { id: crypto.randomUUID(), ...body }) return ctx.json(post, { status: 201 })}) app.get('/posts', async (ctx) => { const store = d1Store(ctx.env.DB) const posts = await store.list<Post>('posts') return ctx.json(posts)}) app.get('/posts/:id', async (ctx) => { const store = d1Store(ctx.env.DB) const post = await store.get<Post>('posts', ctx.params.id) if (!post) return ctx.json({ error: 'not found' }, { status: 404 }) return ctx.json(post)}) app.delete('/posts/:id', async (ctx) => { const store = d1Store(ctx.env.DB) await store.delete('posts', ctx.params.id) return ctx.json({ ok: true })}) return cloudflare(app)Running the SaaS kernel on D1#
The FX SaaS kernel (@kynetra/fx-saas) is built on the StorePort abstraction. Pass d1Store(env.DB) as the store option to createSaasKernel and you get a fully-featured multi-tenant kernel backed by Cloudflare D1.
import { createFX } from '@kynetra/fx'import { cloudflare, d1Store } from '@kynetra/fx-cloudflare'import { createSaasKernel } from '@kynetra/fx-saas' const app = createFX({ runtime: 'cloudflare' }) // Build a fresh kernel per request so each request gets its own// DB binding reference from ctx.envapp.use(async (ctx, next) => { const kernel = createSaasKernel({ store: d1Store(ctx.env.DB) }) ctx.decorate('kernel', kernel) return next()}) app.get('/users/:id', async (ctx) => { const kernel = ctx.get<ReturnType<typeof createSaasKernel>>('kernel') const user = await kernel.users.get(ctx.params.id) if (!user) return ctx.json({ error: 'not found' }, { status: 404 }) return ctx.json(user)}) app.get('/tenants/:tenantId/members', async (ctx) => { const kernel = ctx.get<ReturnType<typeof createSaasKernel>>('kernel') const members = await kernel.members.list({ tenantId: ctx.params.tenantId }) return ctx.json(members)}) return cloudflare(app)Tip
Migration steps#
Before using d1Store, you need to create the fx_store table in your D1 database. The D1_STORE_MIGRATION constant exported from @kynetra/fx-cloudflare contains the exact DDL.
import { D1_STORE_MIGRATION } from '@kynetra/fx-cloudflare' // D1_STORE_MIGRATION is a string you can log or pass to d1 execute:console.log(D1_STORE_MIGRATION)// CREATE TABLE IF NOT EXISTS fx_store (// collection TEXT NOT NULL,// id TEXT NOT NULL,// data TEXT NOT NULL,// PRIMARY KEY (collection, id)// )Install @kynetra/fx-cloudflare
npm install @kynetra/fx-cloudflareAdd the D1 binding to wrangler.toml
[[d1_databases]] as shown in the Installation section. Note the binding name — it becomes ctx.env.DB (or whatever you choose).Create the database (if it does not exist yet)
npx wrangler d1 create my-dbdatabase_id from the output and paste it into wrangler.toml.Apply the fx_store migration
--local for the local dev database and omit it for production.# Local (dev)npx wrangler d1 execute my-db --local --command="CREATE TABLE IF NOT EXISTS fx_store (collection TEXT NOT NULL, id TEXT NOT NULL, data TEXT NOT NULL, PRIMARY KEY (collection, id))" # Productionnpx wrangler d1 execute my-db --command="CREATE TABLE IF NOT EXISTS fx_store (collection TEXT NOT NULL, id TEXT NOT NULL, data TEXT NOT NULL, PRIMARY KEY (collection, id))"Use d1Store in your application
d1Store(ctx.env.DB) directly to your handlers or to createSaasKernel({ store }). No further setup is needed.Note
wrangler d1 migrations apply if you prefer a versioned migration workflow. Create a migrations/ directory and add 0001_fx_store.sql containing the DDL above, then run npx wrangler d1 migrations apply my-db.Self-hosted Postgres alternative#
Because both d1Sql and d1Store implement open port interfaces (SqlPort and StorePort), swapping the persistence layer requires only a one-line change: replace the adapter, keep everything else.
Using a community pgStore adapter
When deploying to your own infrastructure (Node.js, Bun, or Deno), use a communitypgStore adapter that wraps a Postgres connection pool:
import { createSaasKernel } from '@kynetra/fx-saas'import { pgStore } from '@kynetra/fx-postgres' // community adapter import { pool } from './db' // your pg Pool instance // Same kernel API — only the store implementation changesexport const kernel = createSaasKernel({ store: pgStore(pool) })Implementing StorePort yourself
You can also implement StorePort directly for any backend — Redis, DynamoDB, libSQL, or anything else — by satisfying the four-method interface:
import type { StorePort } from '@kynetra/fx-cloudflare' export function myStore(client: MyDatabaseClient): StorePort { return { async get<T>(collection: string, id: string): Promise<T | null> { // fetch and deserialise document const row = await client.findOne(collection, id) return row ? (JSON.parse(row.data) as T) : null }, async list<T>(collection: string, filter?: Record<string, unknown>): Promise<T[]> { const rows = await client.findAll(collection, filter) return rows.map((r) => JSON.parse(r.data) as T) }, async put<T>(collection: string, record: T & { id: string }): Promise<T> { await client.upsert(collection, record.id, JSON.stringify(record)) return record }, async delete(collection: string, id: string): Promise<void> { await client.remove(collection, id) }, }}Pass the result to createSaasKernel exactly as you would d1Store:
import { createSaasKernel } from '@kynetra/fx-saas'import { myStore } from './my-store'import { client } from './db' export const kernel = createSaasKernel({ store: myStore(client) })Note