Region discovery
Before a user signs in, the app does not know which region holds their data. The discovery service answers that one question. It is small, global, has no login, and sits in front of every sign-in. Read this before you touch app start-up or sign-in.
How it works
First launch
Sign-in: find the user's home region
A user who signed up in region A may open the app while travelling in region B. Region B's database does not have them. So sign-in asks discovery first.
Sign-up: write the registry
Who caches what
| Layer | What it caches | How long | Why |
|---|---|---|---|
| App | The resolved API endpoint | Until it fails or the user signs out | One discovery call per install, not per request |
| Discovery | The region map | Polled from AppConfig on a timer | The map is never read on the request path |
| Discovery | HMAC results | Small in-memory LRU cache | Fewer KMS calls |
Discovery /health | Nothing | - | Returns 503 until the region map is loaded, so the load balancer does not send traffic too early |
LRU (least recently used) is a cache that throws out the oldest unused entry when it is full.
Why it has no login, but checks attestation
Discovery runs before sign-in, so there is no user token to check. But an open
/discover/by-identity would let anyone ask "is this email registered?" millions
of times. That is called enumeration.
So every request carries an attestation token. Attestation is the platform vouching that the request came from our real app on a real device, or from a real browser.
The client tells discovery which kind it is with a client-type header. Discovery picks the matching check. Rate limiting per IP is a second layer, not the main one.
In our system, mobile attestation was not switched on at first. A config list let both Android and iOS through unverified, which left per-IP rate limiting as the only guard for mobile. Every region you add makes that gap bigger, so close it before going multi-region.
Two more protections:
- The app hashes the email before sending. Discovery then mixes in a secret with KMS. The plain email never reaches discovery's memory or logs, and the secret never leaves KMS.
- The KMS key is a multi-Region key: the same key material in every region, so every region computes the same digest for the same email.
How to build it
- Two routes only:
POST /discoverandPOST /discover/by-identity. Plus/health. - Store the region map as JSON in AppConfig. Adding a region is a config change, not a code change. Validate it with a schema when you load it.
- Key the map by country, not only by region code. Each region entry lists the countries it serves.
- Refuse to start until config, AWS credentials and the region map all load.
- Keep a bypass flag for local development only. The service must refuse to start with it in staging or prod.
- Give the hostname no region in it, for example
discovery.example.com. The app calls it before it knows its region. - Redact identity values and attestation tokens in the logger before you add any route that receives them.
The country to region mapping
Device locale is a language setting, not a location. A user in India with British English settings looks like the UK. Use the CDN's viewer-country header as a second signal, and let the user see and correct their region during onboarding.
Rules
Keep discovery tiny. It has no login and every sign-in depends on it. Every new route makes a bigger target. It stores only hashed identity, home region, identity type and two timestamps.
Clients fail closed. If discovery fails, retry with backoff and keep the cached endpoint. If there is no cache, show an error. Never fall back to a hardcoded API address.
Run attestation for real in staging. If only production checks attestation, production is the first place a broken check is seen.
Never log the identity or the HMAC digest. The digest is a stable lookup key. Treat it like a session token.
Mistakes we made
- 401s on every request from a header name mismatch. The app sent its platform in one header. Discovery read a different header name, found nothing, and returned 401 "missing client type". The app hid the failure on
/discoverby falling back to a built-in endpoint, so it looked fine. But/discover/by-identityhad no fallback, and sign-in calls it first. Email sign-in was impossible in production, and an app-store review failed on it. Instead: test withcurlusing exactly the headers the client sends, and accept the old header name on the server too, so builds already in users' hands keep working. - A 401 storm from our own server. A web app's server-side rendering called discovery as a "web" client. A browser check token cannot exist on a server, so every call failed. The caller also deleted failed entries from its cache, so every page render tried again. Result: about 79% of discovery traffic in one day was 401s. User impact was near zero, because the caller fell back to a correct default. Instead: give server callers their own client type with a shared secret, check it in constant time, fail closed when the secret is missing, and back off on failure rather than retrying every time.
- Nobody saw the storm in our logs. A logger setting read a field the web framework does not have, so every log line showed
statusCode: null. The failure rate was only visible in load balancer metrics. Instead: check that your request logs show real status codes, and alarm on load balancer 4xx rates. - Dev and staging skipped attestation. Both ran with the bypass flag, so the attestation code only ever ran in production. Instead: at least staging runs the same checks as production.
- Missing country to region mapping. Clients sent a country code like
DE. The map was keyed by region codes. An unknown code silently fell back to the default region with HTTP 200 and no log. With one region this is correct. With two, every foreign user lands in the wrong country. We found it in review before the second region existed. Instead: list countries per region, and make "unmapped country" an explicit, logged policy. - Mobile fell back to the default region; web did not. When discovery failed, the mobile app used a compiled-in address. One flaky first launch abroad and a user signs up in the wrong region, permanently. The web client had no fallback and retried with backoff. Instead: copy the web behaviour.
- Discovery ran in one region. That region going down means sign-in is down worldwide, and far-away users pay a long round trip before their first real call. The registry table and the region map were single-region too. Instead: before the second region, run discovery in each region behind Route 53 latency routing with health checks, make the registry a DynamoDB global table, and generate each region's AppConfig map from one source in Terraform.
- A test-build route that only worked until sign-in. We used discovery to send one app version (the build under store review) to a separate test stack. Routing only
/discoverwas not enough, because/discover/by-identityreplaces the endpoint at sign-in. Instead: apply any routing rule in both routes, and make every failure path return production.
Checklist
- My client calls discovery before any API call and caches the result.
- My client shows an error instead of falling back to a hardcoded address.
- I tested with
curlusing the exact headers my client sends. - Server-side callers use the server client type, not "web".
- Staging runs attestation for real.
- Logs redact identity values and tokens, and show real status codes.