A production-ready OAuth 2.1 + OpenID Connect authorization server, built for the Cloudflare edge.
Current release baseline: 0.5.3. See CHANGELOG.md.
Public auth server package: @eetr/auth.
eetr-auth is a self-hostable OAuth 2.1 / OIDC authorization server that runs entirely on Cloudflare's edge platform — no VMs, no containers, no servers. The auth server product is published as @eetr/auth, and the repo ships as an npm monorepo with two Cloudflare Workers and a client library ready to publish.
graph LR
APP["Your App"] -->|OAuth 2.1 / OIDC| AUTH
subgraph CF["Cloudflare Edge"]
AUTH["@eetr/auth\nNext.js Worker"]
HASHER["argon-hasher\nRust/Wasm Worker"]
D1[("D1\nDatabase")]
R2[("R2\nStorage")]
AUTH -->|service binding| HASHER
AUTH --- D1
AUTH --- R2
end
LIB["@eetr/eetr-auth-client"] -->|npm| APP
| Category | Highlights |
|---|---|
| OAuth 2.1 | Authorization Code + PKCE (S256), Client Credentials, Refresh Token with rotation |
| OpenID Connect | OIDC discovery, JWKS endpoint, /userinfo, ID tokens (RS256) |
| Authentication | Password (Argon2id), Passkeys (WebAuthn) with multiple devices per user, Google sign-in |
| Multi-factor | Email OTP and authenticator-app (TOTP) — offered during sign-in and self-managed in settings |
| Passkey self-service | Enroll, rename, remove, and verify-on-this-device, all from user settings |
| User flows | Registration, email verification, password reset |
| Theming | Light, dark, and system themes across the sign-in and admin UI, with no-flash init |
| Admin | Dashboard for users, clients, tokens, audit log, site settings |
| Infrastructure | Cloudflare D1 (SQLite), R2 (object storage), Terraform provisioning |
| Security | Argon2id hashing in isolated WASM worker, HMAC-SHA256 request signing, PKCE mandatory |
| Auth server package |
@eetr/auth — deployable OAuth 2.1 / OIDC server |
| Client library |
@eetr/eetr-auth-client — token management, typed API, JWT validation |
eetr-auth/
├── apps/
│ ├── auth/ # @eetr/auth - Next.js 16 OAuth/OIDC server
│ └── argon-hasher/ # Rust/Wasm password hashing worker
├── packages/
│ └── eetr-auth-client/ # @eetr/eetr-auth-client (TypeScript, publishable)
├── infra/ # Terraform (D1 + R2) and the Wrangler config template
├── scripts/ # Setup/deploy/db tooling (run via root npm scripts)
├── db/ # D1 schema snapshot + versioned migration patches
└── docs/ # Architecture, features, deployment, UX guidelines
Infrastructure (infra/), tooling (scripts/), and database files (db/) live at
the repo root and operate on the auth Worker; the ops npm run commands
(setup:*, db:*, infra:*, jwt:*) are defined in the root package.json.
- Node.js 20+
- Rust +
wasm32-unknown-unknowntarget - Wrangler CLI v4+
- Terraform CLI v1.5+
- A Cloudflare account
git clone https://github.com/eetr-ai/eetr-auth.git
cd eetr-auth
npm installTerraform and Wrangler both read a Cloudflare API token from the environment — there is no provider block, so without this terraform apply fails with "must provide exactly one of api_key, api_token…". Create a Custom token (Cloudflare dashboard → My Profile → API Tokens), scoped to your account, with: D1 → Edit, Workers R2 Storage → Edit, Workers Scripts → Edit, and (optional) Account Settings → Read. Then export it in the shell you run the install from:
export CLOUDFLARE_API_TOKEN=your_token_here
export CLOUDFLARE_ACCOUNT_ID=your_account_id # same as account_id in terraform.tfvarsKeep both exported for the rest of the flow — Terraform, setup:remote, and Wrangler all reuse them. Set CLOUDFLARE_ACCOUNT_ID if your token can access more than one account, or Wrangler may deploy to the wrong one (a code: 10000 error whose URL shows an unexpected account id). See infra/terraform/README.md for the full permission rationale.
Fill in infra/terraform/terraform.tfvars (see DEPLOYMENT.md for each variable), then:
cd infra/terraform && terraform init && terraform apply && cd -Run these from the repository root. argon-hasher must be deployed before the auth Worker:
npm run deploy:argon-hasher
npm run setup:remotenpm run setup:remote renders the Wrangler config, provisions secrets and JWT/JWKS material, applies the
fresh database schema, deploys the auth Worker, and seeds the bootstrap admin.
See the Deployment guide for the complete step-by-step guide, including first-login hardening and DNS/JWKS setup.
npm install @eetr/eetr-auth-clientimport { validateJwt } from '@eetr/eetr-auth-client'
const payload = await validateJwt(
accessToken,
'https://auth.yourdomain.com/api/jwks.json'
)import { TokenManager, fetchOIDCDiscovery } from '@eetr/eetr-auth-client'
const discovery = await fetchOIDCDiscovery('https://auth.yourdomain.com')
const manager = new TokenManager({
issuerUrl: 'https://auth.yourdomain.com',
clientId: 'your-client-id',
tokenEndpoint: discovery.token_endpoint,
})
const token = await manager.getAccessToken() // auto-refreshes when expiredimport { getUserInfo } from '@eetr/eetr-auth-client'
const user = await getUserInfo(accessToken, discovery.userinfo_endpoint)sequenceDiagram
participant App as Your App
participant Auth as @eetr/auth
participant Hasher as argon-hasher
App->>Auth: GET /api/authorize (code_challenge, scope)
Auth->>App: Redirect → /login
App->>Auth: POST /login (username, password)
Auth->>Hasher: POST /verify [service binding]
Hasher-->>Auth: { valid: true }
Auth->>App: Redirect → callback?code=xxx
App->>Auth: POST /api/token (code, code_verifier)
Auth-->>App: { access_token, refresh_token, id_token }
sequenceDiagram
participant Svc as Backend Service
participant Auth as @eetr/auth
Svc->>Auth: POST /api/token (client_credentials, scope)
Auth-->>Svc: { access_token }
📖 Full documentation: eetr-ai.github.io/eetr-auth — a Fumadocs site (source in apps/docs/) that publishes on each release.
| Section | Description |
|---|---|
| Getting started | Quick start, deployment, local dev, and the Cloudflare template guide |
| Architecture | System design, bindings, flows, and the argon2id hashing worker |
| Features | OAuth/OIDC grants, clients & DCR, auth, tokens, admin |
| Guides | MCP + DCR, WAF rate limiting, MFA/TOTP, SPA integration |
| Reference | Endpoint and configuration reference |
| CHANGELOG.md | Monorepo release history |
cd apps/auth
cp .env.example .env.local
cp .dev.vars.example .dev.vars
npm run jwt:generate-local-cert
npm run db:migrate
npm run db:create-admin:local
npm run devServer runs at http://localhost:3000.
npm test # run the full suite across workspaces
npm run test:coverage # same, with a V8 coverage report per workspaceCI runs the suite on every PR and posts a coverage comment per workspace
(apps/auth, eetr-auth-client) via the CI workflow.
The README coverage badge is the aggregate line coverage across both workspaces,
refreshed on every push to main by the
Coverage Badge workflow. That workflow
writes the badge value to a public gist
using the GIST_SECRET repo secret.
Maintainers:
GIST_SECRETcurrently holds a GitHub token withgistscope. To rotate it to a least-privilege credential, mint a classic PAT with only thegistscope and rungh secret set GIST_SECRET --repo eetr-ai/eetr-auth. No workflow change needed.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) via OpenNext |
| Runtime | Cloudflare Workers |
| Database | Cloudflare D1 (SQLite) |
| Storage | Cloudflare R2 |
| Password hashing | Argon2id (Rust → WASM, isolated Worker) |
| Token signing | RS256 JWT via jose
|
| Sessions | NextAuth.js v5 |
| Passkeys |
@simplewebauthn/browser + @simplewebauthn/server
|
| Resend | |
| Infrastructure | Terraform (Cloudflare provider) |
| Testing | Vitest |
| Client library | TypeScript ESM, jose only dependency |
MIT