Watermarks on My Map: Moving to MapLibre and OpenFreeMap
CARTO basemaps suddenly require an API key and my map filled with watermarks. The story of migrating three Leaflet components to MapLibre GL on OpenFreeMap tiles.
TL;DR
CARTO basemaps now require an API key, leaving a watermark across my map and a 5-million-tiles non-commercial quota. Instead of managing someone's meter, I moved three Leaflet components to MapLibre GL with free unlimited OpenFreeMap; props and test IDs stayed intact, so all E2E tests passed untouched. Dark mode now swaps styles through setStyle instead of a CSS invert hack.
That morning I opened the project and the main map looked wrong. A big gray layer reading "API KEY REQUIRED" sat on top of the tiles. Not one or two tiles; all of them. My first guess was embarrassing: I blamed the browser cache, then an adblock extension. Cleared local storage, tried incognito, hard-refreshed a dozen times. The watermark stayed, calm, as if it belonged there.
Out of guesses, I traced where the tiles came from. The project used CARTO raster tiles, and it turned out they changed their policy: CARTO basemaps now require an API key [2]. Without one, you get exactly that watermark. A free tier still exists, but it no longer shares a room with the word unlimited: 5 million tile requests per month [2], counted across raster and vector services, and the page plainly says the allocation is meant for non-commercial use.
Add a key or switch vendors
Two options on the table. Adding a key meant managing a five-million-tiles-per-month quota and being ready for a conversation when I cross the line. Nothing wrong with that, but this project lives around a map; I didn't want to check someone else's meter every month. A vendor that can change the rules overnight can change other terms too. So I didn't take the key.
I moved to OpenFreeMap. Its public instance is free with no limits on map views or requests [1], no API keys, no registration, no cookies, and commercial use is explicitly allowed [1]. It runs on donations. To me that's the more honest deal: if the service ever stops, it's because funding ran out, not because I was treated as a customer who forgot to pay. Attribution is still required, but with MapLibre as the renderer it gets added automatically.
Migrating three components without touching the tests
Three Leaflet components came along: the main map page with clustering and filters, one mini-map on detail pages, one in the event modal. All of them used to stick raster tiles on the page; now all of them render vector tiles through MapLibre GL. The official MapLibre migration guide [3] sums up the gains: map rotation, vector tiles and globe support, plus faster handling of large datasets because rendering runs on WebGL.
The dependency shuffle was quick: leaflet and leaflet.markercluster out, maplibre-gl in. To keep the migration from leaking into the rest of the app, I locked two things: the components' props and public handles stayed the same, and the data-testid attributes stayed exactly as they were. All four E2E tests on the map page passed without me editing a single line of test code.
Dark mode got a fix too. Tiles used to be darkened with a CSS invert filter hack, with predictable results. MapLibre has an official path through setStyle; you just point at a style that matches the theme:
const map = new maplibregl.Map({
container: el,
style: isDark
? "https://tiles.openfreemap.org/styles/fiord"
: "https://tiles.openfreemap.org/styles/liberty",
});
// swap theme without rebuilding the map
map.setStyle(isDark ? FIORD : LIBERTY);
For light mode I kept liberty. For dark, I first pointed at the style literally named dark. Guessed again, missed again.
The built-in dark style is nearly blind
That dark style rendered cleanly, no console errors. It had exactly one problem: you could barely see it. I captured the dark mode and inspected the pixels: roads drawn in #181818 on a rgb(12,12,12) background. The contrast between them is about 1.03:1 by my own pixel measurement. For reference, WCAG 2.2 asks for at least 4.5:1 contrast on text [4]. Map elements have no official threshold that I could find, but 1.03 needs no debate; a map that human eyes must read doesn't pass with roads nearly the same color as the background.
The fix wasn't repainting layers one by one, it was switching styles. Every component now uses fiord for dark mode, liberty stays for light. The proof is simple: counting unique colors in the before and after dark screenshots, from 227 jumping to 5,302 after moving to fiord. The map got its hierarchy back; roads, labels, and areas are readable again.
Two things stuck from this incident. Free infrastructure is a loan: its terms can change overnight without drama, and a giant watermark on your front page turns out to be the vendor's most effective notification channel. Second, dark mode is a data decision; being dark is not a guarantee of being readable, contrast decides that. Every new style now gets a contrast check before it ships. Two minutes of checking beats one night of a map missing from the product's face.
Sources