Cloudflare backend and accounts
Deploy the Preflight API on Cloudflare Workers with Clerk sign-in, teams and shares
The Preflight API is one Cloudflare Worker (src/worker.ts, configured by
wrangler.jsonc). It serves the browser sign-in page, issues Preflight API
tokens, manages teams and stores shares.
| Part | Holds |
|---|---|
| Clerk | user accounts, GitHub and Google sign-in, teams (Clerk Organizations) and invitation emails |
D1 preflight | hashed API tokens, one-time sign-in codes, share metadata, rate-limit counters |
KV SHARES | the public pointer from a share code to its payload, with a TTL for expiring shares |
R2 preflight-shares | share payloads |
Local configuration management works offline without this backend. The
hosted service is https://api.preflight.arhun.fr; the CLI and the desktop
use it unless PREFLIGHT_API_URL or the desktop's Backend URL says otherwise.
Deploy, step by step
1. Create the Clerk application
- In the Clerk dashboard, create an application named Preflight.
- Under Configure → SSO connections, enable GitHub and Google. For the production instance, give each one your own OAuth credentials: create a GitHub OAuth app and a Google OAuth client, and paste the redirect URI that Clerk shows for each provider.
- Under Configure → Organizations, enable Organizations. Keep the default
roles
org:adminandorg:member; Preflight shows them as admin and member. - Under Configure → Paths, set the sign-in page to
https://api.preflight.arhun.fr/auth/sign-in. Invitation emails then bring people to the Preflight page. - Create the production instance with the domain
preflight.arhun.frand add the DNS records that Clerk lists (theclerk.frontend API record and the email records). Wait until Clerk shows every record as verified. - Under Configure → API keys, copy three values from the production
instance:
- the publishable key (
pk_live_…), which is public; - the secret key (
sk_live_…); - the JWT public key, in PEM format, for networkless verification.
- the publishable key (
2. Create the Cloudflare resources
Sign in once with npx wrangler login, then run from the repository root:
npx wrangler d1 create preflight
npx wrangler kv namespace create SHARES
npx wrangler r2 bucket create preflight-sharesPut the D1 database_id and the KV id that these commands print into
wrangler.jsonc. The committed values are placeholders.
3. Set the variables and secrets
In wrangler.jsonc, set CLERK_PUBLISHABLE_KEY to the pk_live_… key. Keep
PREFLIGHT_PUBLIC_URL at https://api.preflight.arhun.fr. Then store the
secrets:
npx wrangler secret put CLERK_SECRET_KEY # sk_live_…
npx wrangler secret put CLERK_JWT_KEY # the PEM public key, optionalWithout CLERK_JWT_KEY, the Worker verifies sessions against Clerk's JWKS
endpoint. Never set PREFLIGHT_DEV_AUTH in production: it exists for local
tests, and the Worker ignores it on any host other than loopback.
If Clerk keys are missing, the Worker fails closed: /api/health reports
"authentication": "unconfigured" and sign-in, accounts and publishing
answer HTTP 503. Existing share links still open.
4. Apply the migration and deploy
npm run backend:migrate # wrangler d1 migrations apply preflight --remote
npm run backend:deploy # wrangler deploy5. Attach the custom domain
The arhun.fr zone must be on the same Cloudflare account. In
wrangler.jsonc, uncomment the routes entry for api.preflight.arhun.fr
and run npm run backend:deploy again. Wrangler creates the DNS record and
the certificate. Then check:
curl https://api.preflight.arhun.fr/api/health
# {"service":"preflight","authentication":"clerk"}6. Rate limits
wrangler.jsonc declares three Workers Rate Limiting
bindings, per client IP:
| Binding | Routes | Limit |
|---|---|---|
RL_AUTH | /auth/*, /api/auth/* | 30 per minute |
RL_SHARES | /api/shares* | 60 per minute |
RL_PUBLIC | /s/* | 120 per minute |
Without these bindings, the Worker counts in D1 with the same limits. For a
second layer, add a rule in Security → WAF → Rate limiting rules for the
zone, for example: URI path starts with /auth/, 100 requests per 10 seconds
per IP, action Block.
7. Scheduled purge
The cron trigger in wrangler.jsonc (17 * * * *) runs every hour. It
deletes expired shares (R2 payload, then KV pointer, then D1 row), expired
sign-in codes, expired tokens and old rate-limit counters. Expired shares are
also removed when someone opens them, and KV drops their pointer by TTL.
How sign-in works
preflight auth login(or Sign in with browser in the desktop) starts an HTTP listener on127.0.0.1on a random port. It opens/auth/sign-inwithredirect_uri=http://127.0.0.1:PORT/callback, a randomstateand a PKCEcode_challenge(S256).- The page loads Clerk's sign-in component. After sign-in, the person
confirms the connection. The page sends the Clerk session token, the
challenge and the state to
/auth/exchange. - The Worker verifies the session, stores a one-time code (five minutes,
single use) and redirects the browser to the loopback address with
codeandstate. The Worker accepts no other redirect address. - The client sends the code and its
code_verifierto/auth/tokenand receives a Preflight API token:pfk_plus 32 random bytes. The Worker stores only its SHA-256 hash, with the user, a label and dates. The token expires after 90 days without use; each use extends it.
The CLI keeps the token in the OS keychain (macOS Keychain, Windows
Credential Manager, Linux Secret Service). The desktop keeps it in its main
process, encrypted with the OS when possible; it never reaches the renderer.
preflight auth login --no-browser prints the URL instead, and you paste the
code that the page shows.
API
All /api routes except share reads take Authorization: Bearer pfk_…. A
Clerk session JWT is also accepted, for web pages.
| Route | Purpose |
|---|---|
GET /api/account | the signed-in user |
GET /api/tokens, DELETE /api/tokens/:id | list or revoke your API tokens |
POST /api/auth/logout | revoke the token in the request |
GET /api/orgs, POST /api/orgs, GET /api/orgs/:id | your teams, create one, members |
GET|POST /api/orgs/:id/invitations, DELETE /api/orgs/:id/invitations/:inv | invitations (admins) |
PATCH|DELETE /api/orgs/:id/members/:user, POST /api/orgs/:id/leave | roles, removal, leaving |
GET /api/invitations, POST /api/invitations/:id/accept|reject | invitations for you |
GET /api/shares[?team=ORG], POST /api/shares, DELETE /api/shares/:code | your shares or a team's |
GET /api/shares/:code, GET /s/:code | read a share (no sign-in) |
Admins manage members, roles and invitations. A team always keeps one admin: the last admin cannot leave, be removed or be demoted. Clerk sends the invitation email. Errors are JSON: 400 for invalid input, 401, 403, 404, 429, 503 when not configured, and 500 with a generic message for server faults.
Local verification
npm run check
npm run backend:smoke # local Wrangler, D1/KV/R2, PREFLIGHT_DEV_AUTH=1
npm run smoke:worker # production config without secrets: fails closedbackend:smoke applies the migration to a local D1 database. It signs in
with the dev identity through the same PKCE exchange, then checks tokens,
teams, invitations, roles, shares, the scheduled purge and logout. With
PREFLIGHT_DEV_AUTH=1 on a loopback host, /auth/sign-in shows a name and
email form instead of Clerk. No Clerk account and no deployment are needed.