Region-Aware Login & Multi-Account
Business view
Because clinic data now lives in one of several regional databases, the app can no longer just "log in" against a single fixed server — it first has to work out which server owns the account. That lookup happens automatically and (almost always) invisibly, in under a second, before the login form is even submitted.
On top of that plumbing, this PR ships a real, visible feature: the profile menu now supports multiple simultaneous logins. A person who works across a Saudi clinic and a European clinic — or an owner who also wants a receptionist's view open — can add a second account from the profile menu without logging out of the first, and switch between them instantly. The switcher works whether the two accounts are in the same region or different regions.
Public-facing links also had to be taught the same lesson: a patient timeline link or an appointment QR code used to just be a URL on one shared server. Now, whichever region actually owns that patient's data, the link needs to resolve to the right one — so QR codes and shareable links carry a small encoded hint that lets the recipient's browser find the right region without a slow database-wide search.
Technical view
Region lookup (packages/auth-server/src/routes/lookup.js, 718 lines)
Five REST handlers, each rate-limited per IP and each always returning HTTP 200 with a region (real or fake) so the endpoint can't be used to enumerate valid emails/companies:
handleLookup(:85-145) —POST /api/lookup, looks upglobalPrisma.userRegionMappingby normalized email.handleCompanyLookup(:152-210) —POST /api/lookup-company, looks upcompanyLoginNameMappingby clinic login name.handlePublicRouteLookup(:441-562) —POST /api/lookup-public-route, resolves a region for public entities (patient timeline, appointment, invoice, lab order, etc.). Tries a cached company routing hint first (see below), then falls back to scanning every configured region's database for the id.handleResetPasswordTokenLookup/handleVerificationTokenLookup(:569-718) — same pattern for password-reset and email-verification tokens.
Anti-enumeration: getFakeRegion(key, secret, regionList) (:68-79) HMAC-SHA256s the lookup key with LOOKUP_HMAC_SECRET and deterministically maps it to a region — so an unknown email always "resolves" to the same fake region rather than visibly failing, and a stranger can't tell real accounts from nonexistent ones by watching lookup responses.
Client login flow
packages/clinic-web/src/components/auth/Login.js and LoginWithCompany.js both call a shared helper (getClientForRegion) that: looks up the region via lookupRegion/lookupByCompanyLoginName (from @dentolize/clinic-mobile/.../regionLookup or @dentolize/common/auth/regionLookup.ts), compares it to the currently active region, and — if different — builds a new Apollo client for that region's GraphQL endpoint before the sign-in mutation ever runs.
Login.js(email/password): on submit, looks up the region, gets a client for it, runsSIGN_INagainst that client, then on success callsswitchToRegion(...)and navigates. There's no visible "region not found" state, because the lookup always "succeeds" (real or fake); user-visible failures are the normal downstream ones (auth.userNotFound,auth.userDisabled, 2FA errors, rate limiting).LoginWithCompany.js(clinic login name + username): a two-step wizard. Step 0 resolves the clinic login name to a region and verifies the company exists there (CHECK_COMPANY_NAMEagainst the resolved client); step 1 collects username/password and logs in against that region's client.
Multi-account storage and the profile menu
packages/clinic-web/src/shared/utils/regionStorage.jspersists the "currently active region URL" inlocalStorage(dentolize_active_region_url, 7-day TTL).packages/clinic-web/src/shared/utils/regionAccountsStorage.jsmaintains a list of remembered logins per region inlocalStorage(dentolize_region_accounts):upsertStoredRegionAccountadds/refreshes an entry on login; entries are sorted by last-used.packages/clinic-web/src/components/dashboard/layout/Header/HeaderProfileMenu.jsmerges three sources into the profile menu: the active account,user.loggedUsers(server-side "switched" sessions on the same region), and locally stored accounts from other regions. With more than one account, the menu becomes an avatar-based switcher. Selecting a different-region account callsswitchToRegion(...)and does a full page reload to swap the Apollo client; selecting a same-region account runs aSWITCH_USERmutation that flips the server-side session without a region change. Add Account opens a login modal that authenticates a new account without disturbing the current session. Logging out falls back to the next stored account if one exists, rather than always dropping to the login screen.packages/common/src/auth/accountStore.ts(162 lines) implements a more generic, storage-adapter-basedAccountStore(used by both web vialocalStorageand mobile viaAsyncStorage), keyed underdentolize_accounts— a separate, more portable multi-account primitive that currently coexists with clinic-web'sregionAccountsStorage.js. Worth flagging: two multi-account storage systems exist in this PR with different keys and slightly different shapes; they haven't yet been consolidated.
Public links and QR codes
packages/auth-server/src/utils/companyRoutingHint.jsencodes/decodes a company id into a short hint;packages/clinic-mobile/src/shared/utils/publicLinkRouting.jsappends it to generated public links as?c=<hint>.PublicRegionRoute.js(packages/clinic-web/src/components/home/qrCode/PublicRegionRoute.js, 61 lines) wraps public pages (patient timeline, forms, lab orders): on mount it callslookupPublicRouteRegion(...), reading the?c=hint if present, shows a loading spinner while unresolved, switches the active region if needed, and — on lookup failure — fails open and renders against whatever region is already active rather than blocking the page.
Registration across regions
packages/auth-server/src/resolvers/publicAuthMutations.js (485 lines) exports one mutation, register, that picks a target region from the signup country (selectRegistrationRegion), deduplicates retries via an idempotency key and a RegistrationWorkflow row, reserves global routing rows (userRegionMapping, companyLoginNameMapping, companyRegion), then calls into the regional server (bootstrapRegionalRegistration, imported from @dentolize/server) to actually create the user/company. If the global reservation succeeds but the regional write fails, it's rolled back; if the regional write itself has already completed, it is deliberately not rolled back, so a retry with the same idempotency key can complete safely instead of duplicating state.