Preflight
Preflight

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.

PartHolds
Clerkuser accounts, GitHub and Google sign-in, teams (Clerk Organizations) and invitation emails
D1 preflighthashed API tokens, one-time sign-in codes, share metadata, rate-limit counters
KV SHARESthe public pointer from a share code to its payload, with a TTL for expiring shares
R2 preflight-sharesshare 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

  1. In the Clerk dashboard, create an application named Preflight.
  2. 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.
  3. Under Configure → Organizations, enable Organizations. Keep the default roles org:admin and org:member; Preflight shows them as admin and member.
  4. 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.
  5. Create the production instance with the domain preflight.arhun.fr and add the DNS records that Clerk lists (the clerk. frontend API record and the email records). Wait until Clerk shows every record as verified.
  6. 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.

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-shares

Put 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, optional

Without 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 deploy

5. 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:

BindingRoutesLimit
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

  1. preflight auth login (or Sign in with browser in the desktop) starts an HTTP listener on 127.0.0.1 on a random port. It opens /auth/sign-in with redirect_uri=http://127.0.0.1:PORT/callback, a random state and a PKCE code_challenge (S256).
  2. 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.
  3. The Worker verifies the session, stores a one-time code (five minutes, single use) and redirects the browser to the loopback address with code and state. The Worker accepts no other redirect address.
  4. The client sends the code and its code_verifier to /auth/token and 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.

RoutePurpose
GET /api/accountthe signed-in user
GET /api/tokens, DELETE /api/tokens/:idlist or revoke your API tokens
POST /api/auth/logoutrevoke the token in the request
GET /api/orgs, POST /api/orgs, GET /api/orgs/:idyour teams, create one, members
GET|POST /api/orgs/:id/invitations, DELETE /api/orgs/:id/invitations/:invinvitations (admins)
PATCH|DELETE /api/orgs/:id/members/:user, POST /api/orgs/:id/leaveroles, removal, leaving
GET /api/invitations, POST /api/invitations/:id/accept|rejectinvitations for you
GET /api/shares[?team=ORG], POST /api/shares, DELETE /api/shares/:codeyour shares or a team's
GET /api/shares/:code, GET /s/:coderead 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 closed

backend: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.

On this page