Hosted · Live demo · Self-host · Contributing
The open-source Firebase alternative on Cloudflare. Every project gets its own
Durable Objects, so one tenant's data is physically separate from the next —
isolation by architecture, not by a WHERE clause.
- Auth — Better Auth per project: email/password, social sign-in, roles and permissions, project-signed JWTs.
- Database — JSON collections (no schema) and typed SQL tables, live queries on both, regional read replicas, export/import, 30-day rollback.
- Remote Config — feature flags and tuning values your app reads at startup, targeted by country, role, version, or rollout percentage — flipped without shipping a release.
- Storage — buckets of files on R2: public/auth/owner access modes, signed URLs, multipart uploads, a file browser in the console.
- Hosting — static sites and Workers at
<app>.cfbase.dev, deployed from the CLI or on every git push.
Use it hosted at cloudflarebase.com, or run the whole stack on your own Cloudflare account. Same code either way.
Sign up, create a project, point your app at its id:
const baseUrl = 'https://cloudflarebase.com/api/projects/<project-id>';
// auth -> `${baseUrl}/auth` db -> `${baseUrl}/db`Add your app's origin under the project's Settings — that list is the CSRF allowlist, and an unlisted origin gets a 403.
To host the front end too:
npm install -g @cloudflarebase/cli
cloudflarebase login
cloudflarebase init # links this directory to a project + app
cloudflarebase deploy # -> https://<app>.cfbase.devOr connect the GitHub repo from the Hosting page and every push deploys.
Branches serve at <app>-<branch>.cfbase.dev. Limits: 5 projects per org,
5 branches per project, 10 apps per project.
git clone https://github.com/cloudflarebase/cloudflarebase.git
cd cloudflarebase && npm install
npm run dev # localhost:5173/dashboard — no secrets, demo mode onDeploy the whole stack to your account:
npx wrangler login
npm run deploy:all # one Worker per agent + the dashboard
npx wrangler secret put CONSOLE_SETUP_TOKEN # 24+ chars — then claim the console at /loginThe setup token proves you control the deployment — without it, a fresh
install would belong to whoever loads /login first. It also reclaims a
console you do not own.
Everything else is optional and degrades cleanly when absent:
- Storage needs R2 — create a bucket and add the
BUCKETbinding described inagents/storage/wrangler.jsonc. - Hosting needs Workers for Platforms (paid); deploys 503 without it.
- Auth-event charts need Analytics Engine, a free dashboard toggle; then
add the two lines shown in
agents/auth/wrangler.jsonc. - Google/GitHub sign-in, email, and Sentry are opt-in secrets.
Prefer buttons? One per Worker, same order as deploy:all:
auth ·
db ·
storage ·
hosting ·
dashboard
The console is optional — each agent is a normal npm package:
cloudflarebase init my-backend # scaffolds a Worker with auth
cd my-backend
cloudflarebase add db # documents + SQL tables, live queries on both
cloudflarebase add storage # buckets of files on R2
cloudflarebase deployadd merges the agent's wrangler config into yours without overwriting
anything you set, exports its Durable Object classes from your entrypoint, and
adds a type assertion so a missing binding fails at compile time.
Better Auth, per project — so its client works unmodified:
const authClient = createAuthClient({ baseURL: `${baseUrl}/auth` });
await authClient.signUp.email({ name, email, password });Browsers get a cookie; everything else uses the set-auth-token bearer. That
signed-in user's token is what the other agents verify — no ambient API key.
Documents need no schema; a collection exists the moment you write to it:
const db = createDbClient({ baseUrl: `${baseUrl}/db`, getToken });
const posts = db.collection('posts');
await posts.create({ title: 'Hello', votes: 1 });
await posts.query({ orderBy: [{ field: 'votes', direction: 'desc' }], limit: 25 });Tables are schema-first: declare typed columns once, then query them with
real SQL through drizzle (cloudflarebase schema generate emits the schema):
const sql = drizzleTable({ baseUrl: `${baseUrl}/db`, table: 'todos', getToken });
await sql.select().from(todos).orderBy(desc(todos.created_at));Each table is its own Durable Object — its own 10 GB, its own thread, its own replicas — so SQL is single-table by design: filters, aggregates, subqueries, and CTEs yes; DDL and cross-table joins no.
Joins happen in a view: declare 2–5 member tables and a read-only replica
follows their change logs into one SQLite, where a plain SELECT can join
them. Views are eventually consistent (~3s) — built for reporting reads,
not invariants.
Realtime is a subscribe on any collection or table query: a snapshot,
then added/modified/removed deltas as writes land, multiplexed over one
WebSocket per client:
posts.subscribe(
{ orderBy: [{ field: 'votes', direction: 'desc' }], limit: 25 },
{ onSnapshot: render, onChange: (change, docs) => render(docs) }
);The switch you reach for while production is on fire — flip it in the console and every client obeys, no deploy:
const config = db.remoteConfig({ defaults: { signupsOpen: true } });
await config.fetch(); // never throws — offline keeps the defaults
if (!config.get('signupsOpen')) {
form.replaceWith('Signups are paused — back soon.');
}Buckets of files with per-bucket access modes:
import { createStorageClient } from '@cloudflarebase/storage/client';
const storage = createStorageClient({
baseUrl: 'https://cloudflarebase.com/agents/storage-agent/<project-id>',
getToken
});
const files = storage.from('avatars');
await files.upload('me.png', file); // any size — large files go multipart automatically
// A URL a browser can hold — drops straight into <img src>
const { signedUrl } = await files.createSignedUrl('me.png', { expiresIn: 3600 });
const { objects, folders } = await files.list({ prefix: '', folders: true });With no user to relay — a cron, queue consumer, or webhook — mint a
service key (cloudflarebase key create, or the project's Settings
page). Each agent ships an ./admin client over it:
import { createDbAdmin } from '@cloudflarebase/db/admin';
import { createAuthAdmin } from '@cloudflarebase/auth/admin';
import { createStorageAdmin } from '@cloudflarebase/storage/admin';
const db = createDbAdmin(); // url, project, and key from the environment
const post = await db.collection('posts').get(id);
await db.collection('posts').patch(id, { votes: post.data.votes + 1 });
// Seed or migrate accounts with no sign-up flow
await createAuthAdmin().createUser({ email, password, name });Service keys are scoped to one project, bypass access modes like an operator,
and are refused on any request carrying an Origin header — a key pasted into
frontend code fails on the first request instead of shipping in a bundle. The
same routes work over raw HTTP; every project serves its OpenAPI document at
/api/projects/<id>/openapi.json.
npm run check # svelte-check
npm run lint # prettier + eslint
npm test # Playwright against real workerdReport vulnerabilities privately via SECURITY.md. Keep
DEMO_MODE unset anywhere real users live.
Apache-2.0. Cloudflarebase is an independent project, not affiliated with or endorsed by Cloudflare, Inc. See NOTICE.
