Dentolize · Regional DB Split Walkthrough
On this pageBusiness viewTechnical view

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 up globalPrisma.userRegionMapping by normalized email.
  • handleCompanyLookup (:152-210) — POST /api/lookup-company, looks up companyLoginNameMapping by 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, runs SIGN_IN against that client, then on success calls switchToRegion(...) 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_NAME against 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.js persists the "currently active region URL" in localStorage (dentolize_active_region_url, 7-day TTL).
  • packages/clinic-web/src/shared/utils/regionAccountsStorage.js maintains a list of remembered logins per region in localStorage (dentolize_region_accounts): upsertStoredRegionAccount adds/refreshes an entry on login; entries are sorted by last-used.
  • packages/clinic-web/src/components/dashboard/layout/Header/HeaderProfileMenu.js merges 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 calls switchToRegion(...) and does a full page reload to swap the Apollo client; selecting a same-region account runs a SWITCH_USER mutation 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-based AccountStore (used by both web via localStorage and mobile via AsyncStorage), keyed under dentolize_accounts — a separate, more portable multi-account primitive that currently coexists with clinic-web's regionAccountsStorage.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.
  • packages/auth-server/src/utils/companyRoutingHint.js encodes/decodes a company id into a short hint; packages/clinic-mobile/src/shared/utils/publicLinkRouting.js appends 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 calls lookupPublicRouteRegion(...), 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.