Finance Manager has exactly two authentication modes. API_AUTH_MODE=private
is the default and verifies Cloudflare Access identity before every read/write.
API_AUTH_MODE=public deliberately permits unauthenticated reads/writes for a
demo or local development. Missing private configuration or an invalid mode
fails closed. There is no legacy shared API key and no automatic public fallback.
The frontend uses VITE_API_AUTH_MODE with the same value. The deployment CLI
sets it from API_AUTH_MODE; this is public configuration, not a credential.
The ledger remains single-user; Access does not add separate user databases.
-
Set an explicit HTTPS
API_URL. Prefer a custom hostname on the same site as the frontend:finance.example.comandapi.finance.example.com. The CLI configures its Worker custom-domain route and disables workers.dev for that configuration. You can instead explicitly use the Worker's workers.dev URL; the CLI keeps that endpoint enabled, with private JWT verification unchanged. Protect the chosen hostname with Access. Preview URLs stay disabled, and the CLI does not discover a private API URL automatically. Cross-site workers.dev cookies can be blocked by browsers; a same-site custom domain avoids this. -
In Cloudflare Zero Trust, configure an Access self-hosted application covering the entire API hostname, not just
/auth/login. Use an Allow policy for your exact email and your existing email one-time-code login. Do not add Bypass or Service Auth policies for financial endpoints. Keep the frontend protected too, including any alternate Pages addresses that serve its assets. Alternatively protect the whole API Worker with an equivalent user policy. -
Copy the Access team HTTPS origin and API application's Application Audience (AUD) tag. Keep the MCP application/audience separate. Set:
API_AUTH_MODE=private API_URL=https://api.finance.example.com ALLOWED_ORIGINS=https://finance.example.com API_ACCESS_TEAM_DOMAIN=https://your-team.cloudflareaccess.com API_ACCESS_AUD=your-api-application-audience API_ACCESS_ALLOWED_EMAIL=you@example.com
ALLOWED_ORIGINSaccepts comma-separated exact HTTPS origins, plus explicit HTTP loopback origins such ashttp://localhost:5173for a local frontend. Arbitrary remote HTTP origins, wildcards, paths and trailing slashes are not supported. These settings are not secrets. Existing ignored files are preserved; remove oldAPI_SECRET/VITE_API_KEYentries from local files yourself. The CLI removes the obsoleteAPI_SECRETentry from saved deployment configuration when deploying API/client. -
Configure Access's CORS handling so allowed OPTIONS preflight requests bypass Access and reach the Worker. OPTIONS carries no cookie and returns no finance data. Actual requests must still require Access. The Worker returns the exact allowed origin,
Access-Control-Allow-Credentials: true, allowed methodsGET, POST, PUT, PATCH, DELETE, OPTIONS, and headersContent-Type, X-Client-Date. If Access answers preflight itself, configure these same values there. -
Configure Access cookies to permit same-site cross-origin requests (the two HTTPS subdomains above), and choose the desired session duration. For a workers.dev API on a different site, select SameSite=None; this still cannot override a browser's third-party-cookie blocking. Deploy API and client together with the same mode using
npm run deploy. Access policy setup remains a Cloudflare dashboard operation; the CLI does not create it.
The frontend checks /auth/session before loading private financial data. If
the browser has no API cookie yet, Sign in navigates to the API's
/auth/login?returnTo=.... Cloudflare establishes that hostname's cookie, then
the authenticated Worker redirects back to an exact allowed frontend origin,
preserving the page URL. Cloudflare may reuse your existing team login; depending
on policy it may ask for another code. No code is required for each API request.
On expiry, financial writes fail without retries. Sign in opens another tab so the original financial form stays mounted; return to it and use Retry connection, then explicitly retry the save. A network failure can look like an expired Access session, so retry is available too. Session cookies/tokens are never copied into browser storage. Financial API responses are not cached by the service worker; offline app-shell navigation does not grant access to the API.
A local frontend can use the private production API without frontend Access protection. The API independently requires the user's valid Access session.
- Add the exact browser origin, such as
http://localhost:5173, to the productionALLOWED_ORIGINSlist alongside the production frontend's HTTPS origin. HTTP is accepted only for explicit localhost/loopback origins; the API URL and Access team domain still require HTTPS. Redeploy the API to apply it. - In
client/.env.local, setVITE_API_AUTH_MODE=privateand setVITE_API_DOMAINto the production API hostname (withouthttps://). Start only the client withnpm run dev -w client; no local API is needed. - The private remote target bypasses Vite's
/apiproxy. Requests and Sign in go directly to the remote API hostname so the browser sends its Access cookie, then the login callback returns to the allowed localhost page. - Sign in to the API when prompted, then retry. No frontend Access login is required on localhost. Financial reads/writes affect the production ledger.
These are cross-site browser requests even if the production frontend/API share one site. The API cookie needs SameSite=None, and browser third-party-cookie blocking can still prevent it. In that case, permit the API's cookies in the browser or use HTTPS frontend/API hostnames on the same site for development. Do not disable production authentication or put a token in frontend storage.
Deploy the public demo as a separate Pages project, API Worker, and D1 database
with only synthetic data. Use API_AUTH_MODE=public; the CLI builds the matching
public frontend, skips private Access configuration, and can discover its
workers.dev URL. Do not protect the demo frontend/API with Access. Public mode
does not make MCP public: its existing independent Access verification remains
required. Do not connect a public API to the private ledger, even if frontend
privacy masking is enabled. Public mode allows the API's existing mutations.
For local development copy api/.dev.vars.example and client/.env.example
without replacing existing files. They explicitly set public mode and
ALLOWED_ORIGINS=http://localhost:5173. There is no private-mode bypass flag.
The compiled Worker integration suite tests private mode with signed synthetic
Access JWTs and disposable D1, without Cloudflare credentials.
This is a breaking authentication change. Existing private deployments must configure API Access, its chosen hostname, mode and identity settings before deploying the updated API/client. Existing public demos must explicitly select public mode. The old key cannot authorize either mode. Deploy API and client together, then refresh the frontend/PWA to load the new bundle. Existing local configs need the new mode settings; missing settings do not enable public access.
-
In a signed-out/incognito browser, the private frontend and API hostname must require Access login. Direct
/accounts,/export, and mutation requests must not return financial data or apply changes. Inspect alternate Worker URLs to ensure previews are disabled and workers.dev is disabled when using a custom domain, or protected by Access when it is the explicitly chosen API address. -
From the frontend, sign in, inspect
/auth/session(200 withmode: private), load accounts, and save a harmless edit to a synthetic account. Verify the request includes the API session cookie and noX-API-Key. -
Allow a session to expire: reads/saves must fail, a sign-in/retry prompt must appear, and the unsaved form must survive reauthentication in another tab.
-
Check preflight without credentials (substitute your own non-secret domains):
curl -i -X OPTIONS https://api.finance.example.com/accounts \ -H 'Origin: https://finance.example.com' \ -H 'Access-Control-Request-Method: POST' \ -H 'Access-Control-Request-Headers: Content-Type,X-Client-Date'
Expect a successful preflight with the exact origin and credentials header. An actual unauthenticated request with that same Origin must be challenged or rejected; a copied old API key must not help.
-
In public mode, verify keyless reads/writes work only against the isolated demo database. Public mode still enforces browser origin policy, not user identity.
- CORS/network error: check the API hostname, cookie settings, preflight policy, exact frontend origin and whether the API cookie exists. Cross-site domains may be blocked by browser third-party-cookie policies.
- 401: assertion missing, invalid, expired, or scoped to another application. Sign in to the API again and verify team origin and API AUD. Copying the email header or an MCP token cannot establish API identity.
- 403: wrong allowed email or origin. Match Access policy and API email.
- 503 AUTH_CONFIG_INVALID: missing/invalid private settings or unsupported mode. Fix configuration; never switch private finances to public to clear it.
References: Cloudflare JWT verification, Access CORS and cookies, Worker Access protection.