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.
Refresh is entirely client-side
This trips people up, because the endpoint looks like it caches. It does not.
pages/api/arcgis/token.ts calls ArcGIS on every request, asks for a 720-minute (12 h) token,
and responds with Cache-Control: no-store. What is cached server-side is the client ID and
secret — getArcGISCredentials fetches them once per server instance and holds them in memory, so
Key Vault is hit once, not once per token.
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 endpoint may return a token that ArcGIS itself
partially served from cache, the nominal 12 hours is an upper bound, not a promise — trusting it
would let a token expire under a long-lived tab.
| 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.