Skip to main content

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​

LayerWhat it cachesHow longWhy
AppThe resolved API endpointUntil it fails or the user signs outOne discovery call per install, not per request
DiscoveryThe region mapPolled from AppConfig on a timerThe map is never read on the request path
DiscoveryHMAC resultsSmall in-memory LRU cacheFewer KMS calls
Discovery /healthNothing-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​

  1. Two routes only: POST /discover and POST /discover/by-identity. Plus /health.
  2. 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.
  3. Key the map by country, not only by region code. Each region entry lists the countries it serves.
  4. Refuse to start until config, AWS credentials and the region map all load.
  5. Keep a bypass flag for local development only. The service must refuse to start with it in staging or prod.
  6. Give the hostname no region in it, for example discovery.example.com. The app calls it before it knows its region.
  7. 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​

Rule

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.

Rule

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.

Rule

Run attestation for real in staging. If only production checks attestation, production is the first place a broken check is seen.

Rule

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 /discover by falling back to a built-in endpoint, so it looked fine. But /discover/by-identity had 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 with curl using 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 /discover was not enough, because /discover/by-identity replaces 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 curl using 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.