Your own user-management backend — auth, login page, and admin dashboard — running entirely on Cloudflare. One click to deploy. Free and open source.
A self-hosted, Supabase-Auth-style stack you fully own:
- 🔐 Backend — Better Auth on a Cloudflare Worker, storing users & organizations in D1 and sessions in KV. Email + password and social login (Google, Apple, GitHub, Microsoft).
- 🖥️ Hosted login pages — built from better-auth-ui (shadcn/ui components made for Better Auth), themed in Cloudflare's classic orange with light & dark mode. Sign-in, sign-up, passkeys, and the full auth flow at
/auth/*, showing exactly the providers you configure. - 📊 Admin dashboard — Better Auth Studio to browse, create, ban, role, and delete users and organizations — behind an admin-only gate.
Everything is one Worker. No servers, no external database, no third-party auth vendor. It runs comfortably inside the Cloudflare free tier.
🌐 authkit.democra.ai — landing page, screenshots & the launch film (EN / 中文)
| Sign-in methods | Email + password, email codes (OTP), passkeys (WebAuthn), Google, Apple, GitHub, Microsoft (each social provider turns on automatically once you provide its client id + secret) |
| Data | Users, accounts, sessions, organizations & members — in your own Cloudflare D1 database |
| Sessions | Stored in Cloudflare KV; optional cross-subdomain SSO cookie |
| Admin UI | Better Auth Studio at the Worker root, gated to admins only |
| End-user UI | Hosted better-auth-ui pages at /auth/* (sign-in, sign-up, …) in Cloudflare orange, light & dark |
| Cost | $0 on the Cloudflare free tier for typical small/medium apps |
| Ownership | 100% yours — your Cloudflare account, your database, your code, MIT-licensed |
┌──────────────────────────────────────────────┐
End users ─────────► │ Cloudflare Worker (this repo) │
(Google / password) │ │
│ /auth/* → login pages │
Your app ──────────► │ (better-auth-ui) │
(SDK / redirect) │ /api/auth/* → Better Auth (public) │
│ │
│ / and everything → Better Auth Studio │
You (admin) ───────► │ else → admin dashboard │
│ (role=admin only) │
└───────────┬───────────────────┬──────────────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ D1 (SQL) │ │ KV (KV) │
│ users/orgs │ │ sessions │
└─────────────┘ └─────────────┘
One Worker serves three surfaces on three path groups. The auth API is public (that's how your users sign in); everything else is gated to admins.
- Click Deploy to Cloudflare above.
- Cloudflare clones this repo into your GitHub account and provisions a D1 database and a KV namespace for you automatically.
- When prompted, paste one secret:
BETTER_AUTH_SECRET— 32+ random characters that sign your sessions. Generate one with:openssl rand -base64 33
- (Optional) any social-login client ids/secrets you want on from day one — you can also add these later.
- Deploy. Your Worker goes live at
https://cloudflare-auth-kit.<your-subdomain>.workers.dev.
The database schema is created automatically on first request — there is no migration step to run. AUTH_URL is auto-detected from the request, so it works on the workers.dev URL out of the box.
Every push to your new repo redeploys automatically (Cloudflare Workers Builds is wired up for you).
The dashboard is locked to admins, so bootstrap the first one once:
- Temporarily allow sign-up. In the Cloudflare dashboard → your Worker → Settings → Variables, set
ALLOW_SIGNUP = trueand redeploy (orwrangler deploy). - Visit
https://<your-worker-url>/auth/sign-upand sign up with your email + password (or a social provider). - Make yourself admin — two ways, pick one:
- Easiest: set the
ADMIN_EMAILSvariable to your email (comma-separated for several). Anyone in that list is treated as an admin. - Or promote the row directly:
wrangler d1 execute auth-kit-db --remote \ --command "UPDATE user SET role='admin' WHERE email='you@example.com';"
- Easiest: set the
- Turn sign-up back off: set
ALLOW_SIGNUP = false(or remove it) and redeploy. New users can still arrive via social login and the admin dashboard; only public password self-registration is closed. - Visit your Worker root — you're in the dashboard.
The deploy button does not set a custom domain (that step is always manual). To serve auth from auth.yourdomain.com:
- Add your domain to Cloudflare (if it isn't already).
- Worker → Settings → Domains & Routes → Add → Custom Domain → e.g.
auth.yourdomain.com. Cloudflare issues the certificate automatically. - That's it —
AUTH_URLauto-detects the new origin. (If you prefer to pin it, set theAUTH_URLvariable tohttps://auth.yourdomain.com.) - Sharing login across subdomains (SSO): set
COOKIE_DOMAIN = .yourdomain.comso a session created atauth.yourdomain.comis valid atapp.yourdomain.com. List your app origins inTRUSTED_ORIGINS(comma-separated) so they may call the auth API with credentials.
A provider switches on the instant both of its secrets are present. Set them in Worker → Settings → Variables (as secrets), then redeploy.
| Provider | Variables | Redirect URI to register with the provider |
|---|---|---|
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
<AUTH_URL>/api/auth/callback/google |
|
| GitHub | GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET |
<AUTH_URL>/api/auth/callback/github |
| Apple | APPLE_CLIENT_ID, APPLE_CLIENT_SECRET, APPLE_APP_BUNDLE_IDENTIFIER |
<AUTH_URL>/api/auth/callback/apple |
| Microsoft | MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET |
<AUTH_URL>/api/auth/callback/microsoft |
Signing in with a social provider creates the user automatically — no separate sign-up, exactly like Supabase Auth. Register the redirect URI above in each provider's console (replace <AUTH_URL> with your Worker/custom-domain URL).
Passkeys (WebAuthn) work out of the box — no configuration, no extra service:
- Sign in with your password at
/auth/sign-in. - You'll see a Passkeys card — click Add Passkey and follow your browser/OS prompt (Touch ID, Windows Hello, phone, security key).
- Next time, click Sign in with Passkey instead of typing a password.
By default a passkey is bound to the exact hostname. If you run several subdomains against one user base and want a single passkey to work on all of them, set PASSKEY_RP_ID to the parent domain (e.g. example.com) before anyone registers a passkey — the relying-party id is baked into each credential and can't be changed later. PASSKEY_RP_NAME sets the display name in the passkey prompt.
Prefer codes over passwords? Configure an email backend and the login page grows a Send code flow (6-digit code, 10-minute expiry, stored hashed, rate-limited):
- Resend (any recipient): set the
RESEND_API_KEYsecret and theEMAIL_FROMvar. - Cloudflare Email Routing (free; delivers only to destination addresses verified on your account — ideal for admin sign-in): uncomment the
send_emailbinding inwrangler.jsoncand setEMAIL_FROMto an address on your zone.
To go fully passwordless, set PASSWORD_LOGIN = false — the password form disappears and the API rejects password sign-ins; users authenticate with email codes, passkeys, or social login. (Do this after you can receive codes or have registered a passkey, or you'll lock yourself out.)
Both the login pages and the admin dashboard ship English and Simplified Chinese, defaulting to Chinese with a one-click 中 / EN toggle in the corner (remembered per browser).
- The login pages use better-auth-ui's built-in
localization— proper i18n. - The Studio dashboard has no upstream i18n, so the kit injects a small DOM-translation overlay (
i18n/studio-overlay.js, served at/studio-i18n.js). Edit that file's dictionary to adjust wording or add another language; untranslated strings simply stay English.
Everything is themed in Cloudflare's classic orange (#F6821F) — the login pages via Tailwind CSS variables, and the Studio dashboard via theme/studio-cloudflare.css, injected after Studio's own stylesheet so it re-points the accent chain (--primary, --ring, --accent, --chart-*) plus the chart bars, active nav tab, and primary buttons. It applies in both light and dark mode. Buttons use near-black ink on orange (7.66:1); white on this orange fails contrast.
Your application talks to the public auth API. Use the Better Auth client pointed at your Worker:
import { createAuthClient } from "better-auth/client";
export const authClient = createAuthClient({
baseURL: "https://auth.yourdomain.com", // your Worker / custom domain
});
// e.g. social sign-in
await authClient.signIn.social({ provider: "google" });
// or email + password
await authClient.signIn.email({ email, password });
// read the current user
const { data } = await authClient.getSession();Or just send users to the built-in page: https://auth.yourdomain.com/auth/sign-in?redirectTo=/ (the legacy /login path works too). After signing in, users land on redirectTo.
git clone https://github.com/democra-ai/cloudflare-auth-kit
cd cloudflare-auth-kit
npm install # also copies the Studio UI into ./public
echo 'BETTER_AUTH_SECRET="'$(openssl rand -base64 33)'"' > .dev.vars
echo 'ALLOW_SIGNUP="true"' >> .dev.vars
npm run dev # http://localhost:8787Then open http://localhost:8787/auth/sign-up, sign up, and promote yourself:
wrangler d1 execute auth-kit-db --local \
--command "UPDATE user SET role='admin' WHERE email='you@example.com';"The schema auto-creates on first request locally too.
npm run db:create # creates the D1 + KV, prints their ids
# paste the printed ids into wrangler.jsonc (database_id / kv id)
wrangler secret put BETTER_AUTH_SECRET
npm run deployVariables (Worker → Settings → Variables — plain text unless noted):
| Name | Required | Purpose |
|---|---|---|
BETTER_AUTH_SECRET |
✅ (secret) | Signs sessions. 32+ random chars. |
AUTH_URL |
— | Canonical origin. Empty = auto-detect the request origin (recommended). |
ADMIN_EMAILS |
— | Comma-separated emails allowed into the dashboard (besides role=admin users). |
ALLOW_SIGNUP |
— | true opens public password sign-up. Leave off in production. |
COOKIE_DOMAIN |
— | e.g. .yourdomain.com to share sessions across subdomains. |
PASSKEY_RP_ID |
— | Parent domain for cross-subdomain passkeys (e.g. yourdomain.com). Empty = request hostname. Fix before first registration. |
PASSKEY_RP_NAME |
— | Display name in the passkey prompt. |
TRUSTED_ORIGINS |
— | Extra app origins allowed to call the auth API with credentials. |
GOOGLE_* / GITHUB_* / APPLE_* / MICROSOFT_* |
— (secret) | Social login credentials. |
Bindings (auto-provisioned by the deploy button):
| Binding | Type | Stores |
|---|---|---|
DB |
D1 | users, accounts, sessions metadata, organizations, members |
KV |
KV | active login sessions |
ASSETS |
Static assets | the login page + Studio dashboard UI |
- The dashboard is admin-only. The Worker checks every non-public request for a Better Auth session with
role=admin(or anADMIN_EMAILSmatch) before handing off to Studio. The auth API (/api/auth/*) and the/auth/*login pages are intentionally public. - Keep
ALLOW_SIGNUPoff in production unless you want open password registration. Social login and admin-created users are unaffected by it. - Rotate nothing into git. Secrets live in Cloudflare, never in the repo.
.dev.varsis git-ignored. - Want an extra layer? Put the Worker behind Cloudflare Access to require SSO before anyone even reaches the login page (useful for internal-only tools).
Both UIs come from actively developed open-source projects, pinned as npm dependencies — the admin dashboard from Better Auth Studio and the login pages from better-auth-ui. Update by bumping versions:
npm update better-auth-studio better-auth
npm run build # re-copies the Studio UI and rebuilds the login pages into ./public
npm run deployWe also maintain a continuously-synced fork at democra-ai/better-auth-studio that mirrors upstream daily.
src/index.ts— the Worker: schema auto-migrate, public auth API + login pages, admin gate, Studio mount.src/auth.ts— Better Auth configured over D1 + KV, with data-driven social providers.src/studio-api.ts— a complete read/write API handler backing the Studio dashboard on Workers (reads via Drizzle, writes via Better Auth's admin/organization APIs).src/db/— the generated schema and the boot-time init SQL.login/— the login pages: a small Vite + React app around better-auth-ui, themed in Cloudflare's brand orange (#F6821F). Built into./publicautomatically (wrangler runsnpm run buildbeforedevanddeploy).wrangler.jsonc— bindings, variables, and the custom build command.
MIT. Built on the excellent Better Auth, Better Auth Studio, and better-auth-ui projects.