Conversation
Carto began watermarking anonymous basemap tiles in Aug 2026. The request still returns HTTP 200 with a valid PNG, so nothing errors - every tile on the Power Map, Compute Frontier, and My Grid just renders with "API KEY REQUIRED" stamped diagonally across it. Reads the key from CARTO_API and appends it to the tile URL as `key=` (Carto ignores `api_key=`). Without the var the maps render exactly as they do now, so this is safe to deploy before the secret is set. The key is delivered as a meta tag injected into index.html at request time, not a build-time VITE_ var and not an inline script: - Leaflet requests tiles from the browser, so a server-only value never reaches them. The key is public by nature on any browser basemap; it should be domain-restricted in the Carto dashboard, not kept secret. - index.html is already re-templated per request and served no-cache, so rotating the key needs a restart, not a rebuild. - Production CSP is `script-src 'self'`, which blocks inline scripts. An inline config script looked correct over curl but was silently dropped by the browser; a meta tag is subject to no such directive and needs no nonce or hash. All three maps now share one BasemapTiles component. It omits maxZoom rather than passing undefined, because an explicit undefined overrides Leaflet's GridLayer default of 18 and makes the layer unbounded - My Grid sets no maxZoom of its own and threw "Attempted to load an infinite number of tiles". Verified in a real browser against dev and production builds: all three maps request keyed tiles (18/18, 12/12, 15/15) with the var set, and clean unkeyed URLs without it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016kmPCuF8Fs4byBLXK7CaKM
Follows the conventions in CLAUDE.md, which I did not have when the first commit on this branch was written: - New files are kebab-case, so BasemapTiles.tsx becomes basemap-tiles.tsx and the three importing pages follow. - CLAUDE.md now carries CARTO_API in the env list and Carto in the data source table, per "when this file and the code disagree, the code wins; fix this file in the same commit." The CARTO_API note records the two traps, since neither is visible from the call site: it is the only env var that reaches the browser, and an inline script would be dropped by the production CSP while curl still showed correct HTML. Claude-Session: https://claude.ai/code/session_016kmPCuF8Fs4byBLXK7CaKM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Carto started watermarking anonymous basemap tiles in Aug 2026. It does not reject the request: it returns HTTP 200 with a valid PNG that has API KEY REQUIRED stamped diagonally across it. So there is no error anywhere, no failed request, nothing in the logs. Every tile on the Power Map, Compute Frontier, and My Grid is just defaced right now.
The fix
Read
CARTO_APIand append it to the tile URL askey=. Carto ignoresapi_key=, so the parameter name matters.Without the env var, the maps render exactly as they do today. This is safe to merge and deploy before the secret is set, and independent of every other open PR.
Why a meta tag and not a
VITE_build varindex.htmlis already re-templated per request and servedno-cache, so injecting there means rotating the key needs a restart, not a rebuild — and the env var keeps the plain nameCARTO_APIinstead ofVITE_CARTO_API.max-age=15552000, so a watermarked tile fetched once sticks in the browser cache for ~180 days.Two bugs caught while verifying
script-src 'self'. The first version injected an inline<script>. The HTML looked correct overcurl, but the browser silently blocked it and the tiles went out unkeyed. Only a real browser against a production build surfaced this. A meta tag is subject to no such directive and needs no nonce or hash.maxZoom={undefined}is not the same as omitting it. react-leaflet forwards the key into Leaflet options, overridingGridLayer's default of 18 withundefined. My Grid sets nomaxZoomof its own, so the layer became unbounded and threwAttempted to load an infinite number of tiles, rendering zero tiles.basemapTileLayerPropsnow omits the key entirely unless a value is given, with a test pinning it.Changes
server/runtime-config.ts— reads and validatesCARTO_API, injects the meta tag; warns on boot when unset or malformedclient/src/lib/basemap.ts— tile URL builder and TileLayer propsclient/src/components/BasemapTiles.tsx— the one place all three maps get tilesserver/static.ts,server/vite.ts).env.example+ README env tableVerification
Typecheck clean, 511/511 tests pass (24 new), production build clean.
Driven in a real headless Chromium against both the dev server and a production build, capturing every outbound tile request:
CARTO_APINot verified: that a real key actually clears the watermark. That needs your key — an invalid key behaves identically to no key, so this is the one step I could not close. To confirm once the secret is set:
Clean tile = key is good. Watermarked = the key is not valid for basemaps.
After merge
Set
CARTO_APIin Replit secrets and redeploy. Hard-refresh once — watermarked tiles already cached in your browser persist for ~180 days otherwise.🤖 Generated with Claude Code
https://claude.ai/code/session_016kmPCuF8Fs4byBLXK7CaKM