Earning the Map's Trust
Before the map draws a single tile, it has to earn AGOL's trust: the WebMap and its feature services are private, and every request the ArcGIS SDK makes needs a token. You already know the house rule — secrets never reach the browser — so the token gets minted by the BFF and handed to the SDK ready-made:
useArcGISAuth → GET /api/arcgis/token → ArcGIS OAuth (client_credentials)
│ │
│ └── secret from Key Vault (prod) or .env.local (dev)
└── IdentityManager.registerToken() for each whitelisted server
The one rule for consumers: any component that renders an <arcgis-map> or creates ArcGIS
layers directly must call useArcGISAuth() and gate rendering on isReady. Render the map
before a credential exists and the WebMap's own layer-metadata requests fire first, failing with
ArcGIS error 499 "Token Required" — and those failures are not retried.
The hook
hooks/useArcGISAuth.ts is the only thing that should touch IdentityManager. It fetches the
token via useArcGISToken, then on every token change re-registers it for each URL in
ARCGIS_AUTHENTICATED_SERVER_URLS, passing expires: tokenData.expiresAt so the SDK expires the
credential in step with the real token. Re-registering overwrites the previous credential for the
same prefix, which is what makes token refresh safe in a tab left open overnight.
It returns three things: isReady (true once a token has been registered at least once), isError,
and tokenRef — a ref holding the current token string, needed for StreamLayer, whose wss://
connections bypass IdentityManager entirely and take the token as a custom parameter instead.
Registration is prefix-matched, so https://services8.arcgis.com/.../rest/services covers every
FeatureServer beneath it. The list in data/constants.ts currently spans the ArcGIS Online portal
root, the sharing/rest endpoint, the master WebMap item, the hosted feature-service root, and two
us4-iot.arcgis.com entries for real-time streaming.
Minted once, refreshed client-side
The token is a client-credentials grant, identical for every visitor, so the server mints it once
and caches it for the process: lib/server/arcgisToken.ts asks ArcGIS for a 720-minute (12 h)
token, re-mints ten minutes before expiry, shares one in-flight mint between concurrent callers,
and backs off for 30 s after a failure. The root layout inlines the cached token into the HTML
(bounded by an 800 ms wait) and RootLayoutClient seeds it into React Query, so the map mounts
on first paint. pages/api/arcgis/token.ts answers from the same cache with
Cache-Control: no-store and exists mainly for refreshes. The client ID and secret are cached
separately by getArcGISCredentials, once per server instance.
All refresh logic lives in queries/useArcGISToken.ts. Both staleTime and refetchInterval are
computed from the expiresIn the API actually returned, minus a 5-minute buffer, floored at
60 seconds. That indirection matters: because the cached token may already be hours old when a
tab receives it, the nominal 12 hours is an upper bound, not a promise — trusting it would let a
token expire under a long-lived tab. The full lifecycle, bootstrap included, is in the ArcGIS
chapter's Token Handshake.
| Setting | Value | Why |
|---|---|---|
staleTime / refetchInterval | (expiresIn − 300s), min 60s | Refresh before real expiry, not nominal expiry |
refetchIntervalInBackground | true | A hidden tab left open for 12h still refreshes |
refetchOnWindowFocus / OnReconnect | false | The interval already covers it |
retry / retryDelay | 3, exponential backoff capped at 30s | Transient OAuth failures |
gcTime | stale time + 60s | Outlive the refresh window |
Credentials
In production (NODE_ENV=production) the secret comes from Azure Key Vault via
DefaultAzureCredential — managed identity in Azure, Azure CLI locally. Set AZURE_KEY_VAULT_NAME
and store the pair as ESRIClientId and ESRISecret.
Everywhere else it reads .env.local:
ARCGIS_CLIENT_ID=...
ARCGIS_CLIENT_SECRET=...
Missing either throws ArcGIS client credentials not configured from the token route — a 500 at
/api/arcgis/token, which surfaces as a map that never leaves its loader.
To mint a new pair: ArcGIS Online → Organization → Settings → Security → OAuth 2.0 → Add, and choose the Server application type (client-credentials grant requires it).
When the map won't load
- Stuck on the loader.
isReadynever flipped. Check the Network tab for/api/arcgis/token— a 500 means credentials; a pending request means Key Vault access. - Error 499 / "Token Required" on layer requests. Something rendered a map without gating on
isReady, or the layer's server isn't inARCGIS_AUTHENTICATED_SERVER_URLS. - One layer missing, everything else fine. Almost always the suppressed-challenge path above.
Add the FeatureServer base URL — without the trailing
/0,/1layer index — to the whitelist. - Works for 12 hours, then breaks. Refresh isn't re-registering. Confirm the console shows a
fresh
Registered token for:line per server after each refetch.
Next: Tuning the Map, the chapter reference sheet — then Graphic Notifications.