0005: Sign in on the server; keep tokens out of the browser
Status: accepted by the owner (Oct 16, 2026).
Context
The web app must call the API as a signed-in user. The two usual designs are:
- Tokens in the browser. The page signs in directly and holds the access token in JavaScript. Simple, but any script that runs on the page (a compromised dependency, an injected snippet) can read the token and use it from anywhere. This is the classic XSS token-theft problem.
- Server-side session. The web app’s own server signs in, keeps the tokens, and gives the browser only a sealed cookie that JavaScript cannot read.
PennyPlan holds financial behaviour data for people with little margin for error, so we choose the second.
Decision
- The Next.js server signs in with Auth0 using the authorization-code flow as a confidential client. The client secret exists only as a server environment variable and is never sent to a browser or committed.
- The session lives in an encrypted,
HttpOnly,Secure,SameSitecookie. The Auth0 access and refresh tokens are never visible to page JavaScript. - The browser calls same-origin routes on the web app’s own server, which attach the access token and forward the call to the API. The API still validates the token exactly as before; it neither knows nor cares that a server sits in front. Token refresh happens on the server.
- The API’s CORS list stays strict. Browsers do not call it directly, so it needs no cross-origin access for the app.
Offline
Offline-first and server-side sessions fit together, with one rule to get right:
- Data is cached locally (IndexedDB) and the outbox works with no signal and no session.
- The cookie gates the sync, not the logging. Sending the outbox needs a valid session.
- If the cookie expired while the person was offline, nothing may be lost. On the first
401the app stops syncing, keeps the outbox untouched, tells the person “your entries are saved on this phone; sign in to send them”, and resumes from the same outbox after sign-in. If the new sign-in is a different account, the entries are not sent under it. The details are in Offline sync.
Consequences
| What follows | Why it matters |
|---|---|
| A cookie now authenticates browser requests, so we must defend against cross-site request forgery | Use SameSite cookies, check the request origin on every state-changing route, and never accept a token from the cookie on the API directly |
| Auth0 needs a Regular Web Application (with a client secret), not a single-page app | The secret goes in the host’s environment settings only. It is rotated if it is ever exposed |
| Local data in IndexedDB is readable by page scripts | Tokens are protected; cached data is not. A strict content-security policy limits what scripts can run, and sign-out clears the local data |
| The service worker must not cache signed-in API responses carelessly | A shared phone must not show the previous person’s data |
| One more hop for every request | Acceptable, and the hop is on the same platform as the web app |
Not decided here
How long a session lasts, and how the refresh token’s lifetime interacts with a person being offline for a long time. These need real usage to tune.
Last updated on