A centralized, self-hosted authentication server,
the one thing every app you build can point at.
Runs on your own Cloudflare account. No per-user bill, no third party holding your users' data.
Infra is open source, published by Pherus. Pherus uses it, but Infra is not Pherus's product: it's its own project, and anyone can deploy their own instance
This is a Turborepo monorepo (bun workspaces):
| Package | What it is |
|---|---|
infra/ |
The auth engine. The real betterAuth() instance (all plugins, D1/KV/R2), the admin dashboard, and only the admin/owner-facing pages: /setup, admin /sign-in, /forgot-password. See infra/README.md. |
www/ ("Infraccount") |
The end-user "my account" app, the same idea as myaccount.google.com. It runs no auth server of its own; it's a pure client of infra via better-auth/react's createAuthClient. Hosts the OAuth provider's pages (/sign-in, /create-account, /two-factor, /consent, /forgot-password, /reset-password) plus profile, security (2FA, passkeys, active sessions), and wallets (saved mobile-money numbers, transaction history, receipts). |
shared/ui (@infra/ui) |
The one UI kit, consumed by both apps. Neither app owns its own copy of components. |
plugins/r2 (@infra/r2) |
Object storage, extracted into a standalone package with server and client exports so www (or any third-party consumer) can call it too, not just infra's own in-process auth.api.*. |
plugins/payment (@infra/payment) |
PawaPay mobile-money payments, extracted the same way. |
Infra used to be a single app doing everything: the admin dashboard and the hosted OAuth login/consent/sign-up pages a connected app's users see. That coupled two very different audiences, the instance owner managing the platform and any end user of any connected app, into one surface.
Splitting them means infra only ever needs to authenticate its own admins/owners, and www/Infraccount is the one place an end user (of any app pointed at this instance) signs in, manages passkeys/2FA, and manages payments. The same shape as how Google separates the internal admin console from myaccount.google.com.
bun install # installs every workspace package
cp infra/.env.example infra/.env.local # infra's secrets, see comments in the file
cp www/.env.example www/.env.local # www's config (points at infra's URL)
bun run dev # starts infra (:3000) and www (:3001) togetherWhenever you edit an .env/.env.local file, re-run bun run type-gen and fully restart the dev server. Vite's own file-watcher restart isn't enough for Cloudflare Worker bindings/secrets to pick up the change.
bun run build # production build, every package
bun run typecheck # tsc --noEmit, every packageSee Getting Started for prerequisites and first-run setup, or the full docs site for architecture, the OAuth provider, and payments.
infra and www are each their own Cloudflare Worker, deployed independently:
bun run --cwd infra deploy
bun run --cwd www deploydocs/ stays at the repo root, not nested under infra/, since GitHub Pages serves it as a root-level /docs folder.
Contributions are welcome. See CONTRIBUTING.md for setup, code conventions, and how to open a pull request. This project follows a Code of Conduct. Found a security issue? See SECURITY.md, please don't open a public issue for it.