React-Admin data providers backed by Cloudflare D1 and Turso (libSQL), each powered by a thin REST Worker. Ship a full admin CRUD experience over either database in minutes — pagination, filtering, sorting, search, soft delete, bulk operations, and field transforms, all behind a configurable allow-list.
Both backends are Simple-REST dialect providers, so they drop in anywhere
ra-data-simple-rest would. They share one engine
(core-rest-worker) and one
provider implementation, so behavior is identical and maintenance is shared.
Each database has a Worker + provider pair:
| Package | What it is |
|---|---|
d1-rest-worker |
Cloudflare Worker (Hono) exposing the REST API over your D1 database. |
ra-cloudflare-d1 |
React-Admin data provider that talks to the D1 Worker. |
turso-rest-worker |
Cloudflare Worker (Hono) exposing the REST API over your Turso database. |
ra-turso |
React-Admin data provider that talks to the Turso Worker. |
create-d1-rest-worker |
Interactive CLI that scaffolds a ready-to-deploy D1 Worker, with optional schema auto-discovery. |
create-turso-rest-worker |
Interactive CLI that scaffolds a ready-to-deploy Turso Worker, with optional schema auto-discovery. |
core-rest-worker |
The shared, DB-agnostic engine + the RestWorkerDb adapter contract (for adding other backends). |
rest-worker-types |
Shared TypeScript types consumed by the other packages. (internal) |
npx create-d1-rest-worker
cd d1-rest-worker
pnpm install
pnpm exec wrangler secret put API_KEY # the Bearer token clients will use
pnpm run deployAdd --auto-discover to have the CLI introspect your D1 database via the
Cloudflare API and generate per-table column/filter/sort/search configuration
automatically.
pnpm add ra-cloudflare-d1import { createD1DataProvider } from "ra-cloudflare-d1";
export const dataProvider = createD1DataProvider({
apiUrl: "https://YOUR_WORKER_URL/api",
apiKey: "sk_...",
});That's it — getList, getOne, getMany, create, update, delete, and
their *Many/bulk variants are all supported.
Prefer Turso? Scaffold a Worker with the CLI (mirrors the D1 flow, and introspects your schema by querying Turso over libSQL):
npx create-turso-rest-worker --auto-discover
cd turso-rest-worker
pnpm install
pnpm exec wrangler secret put API_KEY
pnpm exec wrangler secret put TURSO_AUTH_TOKEN
pnpm run deployOr write the Worker by hand — it takes your database URL and auth token directly:
import { createTursoRestApi } from "turso-rest-worker";
export default {
async fetch(request, env) {
return createTursoRestApi(
{
apiKey: env.API_KEY,
corsOrigins: ["https://admin.example.com"],
resources: {/* same shape as the D1 example below */},
},
{ url: env.TURSO_CONNECTION_URL, authToken: env.TURSO_AUTH_TOKEN },
).fetch(request);
},
};On the client:
import { createTursoDataProvider } from "ra-turso";
export const dataProvider = createTursoDataProvider({
apiUrl: "https://YOUR_WORKER_URL/api",
apiKey: "sk_...",
});Security: The
apiKeyis sent from the browser and is retrievable by visitors — treat it as a public credential, not a secret. To limit exposure, setcorsOriginsto your admin panel's exact origin(s) instead of"*"; browsers will block cross-origin requests because every API call is preflighted (theAuthorizationheader triggers it). This stops other websites from abusing the key but does not prevent server-side use (curl, scripts). For production, put the Worker behind Cloudflare Access or JWT validation. See docs/quick-start.md.
- Simple REST compatible — drop-in for
ra-data-simple-rest. - Allow-list security — only operator-declared tables/columns are selectable, filterable, sortable, or searchable; all identifiers are validated and values are SQL-parameterized.
- Optional rate limiting — Cloudflare's native Rate Limiting binding, zero latency, works on the Free plan. Defaults to per-API-key limits.
- Pagination, filtering & sorting — inclusive range pagination with a
configurable
maxPerPagecap; operator suffixes (_gt,_gte,_lt,_lte,_contains,_startsWith,_endsWith),INarrays, andqfull-text-ish search. - Soft delete — timestamp or boolean soft-delete columns, excluded by default
and includable via
?includeDeleted=true. - Bulk operations —
updateMany/deleteManychunked over a single batched request, with a permissive fallback to individual requests. - Field transforms — server-side boolean/date/JSON coercion, optional to mirror on the client.
- Schema endpoint —
GET /__schemaintrospects configured tables for tooling. - TypeScript-first — strict, type-checked, with published
.d.tsand maps.
Prefer to write the Worker yourself? It's one function call:
import { createD1RestApi } from "d1-rest-worker";
export default {
async fetch(request, env, ctx) {
return createD1RestApi({
apiKey: env.API_KEY,
corsOrigins: ["https://admin.example.com"], // whitelist your admin UI
// rateLimit: { binding: env.API_RATE_LIMITER }, // optional, needs wrangler binding
resources: {
posts: {
tableName: "posts",
idField: "id",
selectableFields: ["id", "title", "body"],
filterableFields: ["id", "title"],
sortableFields: ["id", "title"],
searchableFields: ["title", "body"],
},
},
}).fetch(request, env, ctx);
},
};The default D1 binding name is "DB"; override it with the second argument:
createD1RestApi(config, { dbBinding: "MY_DB" }).
Both backends are SQLite-flavored, but a few engine differences are worth knowing:
- Bulk-operation atomicity differs.
updateMany/deleteManyrun in a single batched request. D1 batches are not transactional (partial success is possible); libSQL/Turso batches run in a transaction and roll back on failure. Either way the worker falls back to per-statement requests if the batch fails. - Turso integer mode. The Turso adapter uses the default
intMode: "number", so integers come back as JS numbers (matching D1). Configuringbigint/stringmodes is not coerced by the adapter. - BLOB columns on Turso. BLOBs arrive as
ArrayBuffer/Uint8Array, whichJSON.stringifycannot serialize. The Turso REST path does not normalize them today — avoid BLOB columns in exposed resources, or pre-serialize them. (D1 returns BLOBs as already-serialized values and is unaffected.) - Public API key.
apiKeyis sent from the browser and is retrievable by visitors — treat it as public. RestrictcorsOriginsand, for production, put the Worker behind Cloudflare Access or JWT validation.
- Quick start
- Deployment options — CLI, template, or manual
- Configuration reference — worker & provider config, rate limiting, bulk-operation semantics
- Filter operators
- Soft delete
- Migration guide — coming from
ra-data-simple-rest
This is a pnpm + turbo monorepo (Node 24, TypeScript, Vitest). From the repo root:
pnpm install
pnpm build # tsc -p tsconfig.build.json per package
pnpm typecheck # depends on ^build
pnpm test # vitest run (integration test is opt-in via INTEGRATION=1)
pnpm lint # ESLint (type-checked)
pnpm format # prettier -w .The integration test spins up workerd via getPlatformProxy; run it locally
with INTEGRATION=1 pnpm --filter d1-rest-worker test.
Releases are managed with Changesets:
add a changeset (pnpm changeset), and merging the auto-generated "Version
Packages" PR publishes everything under packages/** to npm.