The field app is not a second account
BuilderHelp has a Next.js system of record and an Expo field app. Office staff work in the browser; crews photograph receipts and answer RFIs on a phone at the site. Those are two surfaces, not two products. The cheap failure is treating the field app as a separate membership — its own password reset, its own user rows, its own idea of who is logged in. Auth is one of the seams where "we already have users" turns into a second identity system if nobody draws the line.
Two clients, one user of record
The web app owns the account. That is the rule that keeps four surfaces from becoming four directories of people. When someone joins a company in BuilderHelp, there is one user record, one set of roles, and one place entitlement is decided. The field app authenticates as that person. It does not invent a parallel crew account that later has to be linked by email and hope.
This sounds obvious until a mobile sprint starts with "just ship a login screen." A login screen is fine. A second registration flow, a second password store, or a second "create account" path that writes outside the system of record is not. The phone is a client. The identity lives where the rest of the product already decided it lives.
Cookies on the web, secure storage on the phone
The transport differs even when the identity does not. On the web we keep sessions server-side and hand the browser an HttpOnly cookie — the Backend-for-Frontend pattern that keeps tokens out of JavaScript. On the phone, Authorization Code with PKCE is the default for a public client: short-lived access tokens in memory, refresh tokens in the Keychain or Keystore, never in plain AsyncStorage or UserDefaults.
Copying the web cookie model onto mobile, or stuffing a long-lived bearer token into local storage because it was easier in week one, is how you get sessions that cannot be revoked cleanly and credentials that survive a device hand-me-down. Same person. Different storage rules. The API still validates the same subject.
The API trusts the identity, not the client
Once both surfaces talk to the same API, authorization has to be about the user and their role, not about which binary called. A field-app build that sends a "client=mobile" header must not unlock a shortcut past the same checks the dashboard uses. If the office can only see jobs for their company, the phone must fail the same way when the token is for someone else — including when that someone else is a curious coworker with a borrowed device.
BuilderHelp's web app is the system of record; the field app and the OCR service are clients. That ownership line only holds if every mutating request carries a verified identity from the shared auth path. A service account for OCR is fine. A soft-trusted mobile build that "already logged in once" is not.
Refresh is where sessions drift apart
Most auth bugs we see after launch are not wrong passwords. They are refresh-token lifetimes that diverged: the browser session lasts a workday, the phone silently keeps a month-old refresh token, or one surface rotates refresh tokens and the other still expects reuse. Rotate on use, detect reuse, and revoke the family when reuse appears. That is how a stolen phone session stops being an invisible second login.
Logout has to mean the same thing everywhere you can promise it. Revoke refresh tokens server-side. Do not only clear local state on the device that happened to tap the button. If office admins can disable a crew member, the next API call from that person's phone should fail closed — not succeed until the access token happens to expire.
What we check before we ship a second surface
Before a mobile client leaves prototype status, we want four answers in writing: where the user of record lives, which OAuth (or session) clients exist and whether they are public or confidential, where refresh tokens are stored on each surface, and what admin disable or password reset does to every active session. If any of those answers is "we will reconcile later," the field app is already becoming a second account.
This is not an argument against native or Expo clients. It is an argument for treating identity as platform work that lands before the first production login screen. Feature flags and paywalls can still gate screens — that is a different note — but the person behind the flag has to be the same person the dashboard already knows.