BeNomad Tiles
Sessions & tokens#
One HTTP Basic login against the tiles worker returns a JWT valid ~1 hour, and every tile, style, glyph and discovery request must carry it, by cookie, header or query parameter.
You normally write none of this. bemap.MapLibreMap instantiates bemap.TilesAuth for you as soon as ctx.tilesHost is set: it logs in, picks the wire mode, waits for the token before the first tile request, renews ahead of expiry and recovers from a 401. This page is the contract underneath: read it when you integrate raw MapLibre, a native client, or a backend.
At a glance#
- Login:
POST {tilesBase}/api/loginwithAuthorization: Basic base64(login:password)→{ ok, username, token }, plus an HttpOnlysessioncookie. - Token lifetime ~1 hour. Limits: 100 req/s per user,
Range≤ 10 MB, HTTPS only. - Four wire modes on
tilesAuth:auto(default),cookie,header,query. autoresolves once, at construction: same registrable domain astilesHost⇒cookie; cross-site ⇒X-Session-Tokenheader.- Renewal is automatic: proactive 5 minutes before the
expclaim, reactive on401, plusvisibilitychangeand offline handling. - The token is persisted per
ctx.tokenStorage:'session'(default),'local'or'memory', under the keybemap_tiles_token. - Credential-less integrations: a backend token provider, or the v2.0.2
proxyoption.
Usage#
The supported path: set tilesHost and let the SDK own the session:
var ctx = new bemap.Context({
login: 'YOUR_LOGIN',
password: 'YOUR_PASSWORD',
secure: true,
host: 'bemap.benomad.com',
tilesHost: 'mptiles-api.benomad.net'
});
var map = new bemap.MapLibreMap(ctx, 'map');
map.isTokenValid(); // Boolean map.getToken(); // String | null
map.refreshToken(); // Promise<String>Pin the wire mode only when auto cannot be right for your deployment, for example a native WebView that cannot inject headers:
new bemap.Context({ tilesHost: 'mptiles-api.benomad.net', tilesAuth: 'query' }); // shorthand
new bemap.Context({ tilesHost: '…', tilesAuth: { mode: 'header', tokenHeader: 'X-Session-Token' } });
new bemap.MapLibreMap(ctx, 'map', { tilesAuth: { mode: 'cookie' } }); // per-map overrideRaw HTTP: log in, then spend the token. Never inline the credentials:
TOKEN=$(curl -s -X POST "https://mptiles-api.benomad.net/api/login" \
-u "$LOGIN:$PASS" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
# 200 while the session is valid, 401 once it has expired
curl -s -o /dev/null -w '%{http_code}\n' -H "X-Session-Token: $TOKEN" \
"https://mptiles-api.benomad.net/api/status"
# a z/x/y MVT tile with the same token
curl -s -H "X-Session-Token: $TOKEN" \
"https://mptiles-api.benomad.net/default/12/2074/1409.pbf" --output tile.pbfReference#
Session endpoints#
| Endpoint | Method | Auth sent | Response |
|---|---|---|---|
{tilesBase}/api/login | POST | Authorization: Basic base64(login:password) | { ok, username, token } + Set-Cookie: session=…; HttpOnly; Secure; SameSite=None. Token TTL ~1 h |
{tilesBase}/api/status | GET | Current token, any wire mode | 200 session valid / 401 expired or unknown |
{tilesBase}/api/logout | GET or POST | Current token, any wire mode | 200. Clears the cookie session. Does not revoke the JWT, see below |
{tilesBase} is https://<tilesHost>: ctx.getTilesBaseUrl(), plus getTilesLoginUrl() / getTilesStatusUrl() / getTilesLogoutUrl().
Logout does not revoke the token. Verified against the deployed beta worker: /api/logout accepts both GET and POST (both return 200), but the JWT stays usable afterwards, /api/status still returns 200 and tile requests still succeed until the token reaches its own exp. Logout clears the HttpOnly cookie session; it is not a bearer-token revocation. Treat a JWT as valid for its full lifetime once issued, and keep it out of logs and URLs you persist. The SDK sends POST (bemap.TilesAuth.logout({ serverSide: true })); a raw client may use either method.
Limits#
| Limit | Value | Exceeded → |
|---|---|---|
| Token lifetime | ~1 hour | 401 on every subsequent request |
| Request rate | 100 req/s per user | 429 |
Range size | ≤ 10 MB per request | 403 |
| Transport | HTTPS only | Request refused |
Wire modes: tilesAuth#
Set on the Context, overridable field-by-field per map. Accepts the string shorthand ('auto', 'cookie', 'header', 'query') or the full object. An unrecognised mode falls back to auto.
| Field | Default | Meaning |
|---|---|---|
mode | 'auto' | 'auto' | 'cookie' | 'header' | 'query' |
credentials | 'include' | Cookie mode only: 'include' | 'same-origin' | 'omit' |
tokenHeader | 'X-Session-Token' | Header mode only |
tokenParam | 'token' | Query mode only |
mode | How the token rides the request | Cost |
|---|---|---|
auto (default) | Resolved once, when bemap.TilesAuth is constructed: bemap.Context._sameSite() compares the registrable domain (last two labels) of window.location.hostname and tilesHost. Same ⇒ cookie; different ⇒ header. header is the safe fallback, so auto never silently breaks cross-site. | None |
cookie | credentials: <credentials> on every request; no custom header, no query parameter, so a Range GET stays a CORS-simple request. | No preflight. First-party only: a SameSite=None cookie is blocked cross-site by Safari ITP, Firefox TCP and Chrome incognito. |
header | <tokenHeader>: <jwt> on every request. | Works without cookies. Preflight is path-dependent: one cached OPTIONS per slice URL on the 200-slice path, one per request on the Range + no-store path. |
query | ?<tokenParam>=<jwt> appended to the URL. | No preflight, incognito-safe. Token appears in the URL; the browser cache refills on token rotation. |
The resolved mode applies uniformly to the pmtiles Range fetch interceptor, MapLibre's transformRequest (style, glyphs, sprites, z/x/y), bemap.TilesStyle.fetch, and to login / logout.
bemap.TilesAuth(ctx, options)#
Constructed for you by bemap.MapLibreMap. Instantiate it directly only in a raw-MapLibre integration.
| Option | Type | Purpose |
|---|---|---|
onError | Function | Receives a bemap.Error on any auth or network failure |
onToken | Function | Receives { reason, exp }: reason is 'login' or 'refresh', exactly one event per logical operation |
tilesAuth | Object | String | Per-map wire config, merged over ctx.tilesAuth field-by-field |
| Member | Returns | Behaviour |
|---|---|---|
login([login], [password]) | Promise<String> | Defaults to ctx.login / ctx.password. Concurrent calls join the single in-flight promise |
whenTokenReady() | Promise<String|null> | Resolves with the current valid token, otherwise starts (or joins) a login. The synchronisation point the fetch interceptor uses |
getToken() | String | null | The raw JWT |
isValid() | Boolean | false with no token; true when exp is unparseable (assume valid until a 401); otherwise exp > now + 30 s |
refresh() | Promise<String> | Proactive renewal with the original credentials; deduplicated |
getHistory() | Array<{exp:Number}> | Last 3 superseded tokens: exp only, never the JWT |
buildTransformRequest() | Function | MapLibre transformRequest; only stamps requests whose host equals ctx.tilesHost. Captures getToken, not the token value, so renewal is transparent |
handle401(map, [styleSpec]) | Promise<String> | Refresh, then setStyle(styleSpec, { _internal: true }) to retry pending tiles. N simultaneous 401s collapse into one refresh and one reload |
logout([{ serverSide }]) | Promise<void> | Always clears the token, history, storage and renewal timer. serverSide: true also calls the logout endpoint |
destroy() | - | Clears the timer, removes the visibilitychange / online listeners, unregisters from the fetch interceptor |
bemap.TilesAuth.STORAGE_KEY | String | 'bemap_tiles_token' |
bemap.TilesAuth.installFetchInterceptor(auth, tilesHost) | - | Scoped window.fetch wrapper that awaits login and stamps auth on requests to tilesHost. Idempotent; skips /api/login; needed because pmtiles.js bypasses transformRequest |
bemap.TilesAuth.uninstallFetchInterceptor(tilesHost) | - | Unregisters the host; restores the original window.fetch when the registry empties |
Token storage: ctx.tokenStorage#
| Value | Backing store | Lifetime |
|---|---|---|
'session' (default) | sessionStorage | Cleared when the tab closes |
'local' | localStorage | Survives reloads and restarts |
'memory' | none | Never persisted |
Key bemap_tiles_token, value { token, exp }. Storage failures (private mode, quota, disabled) degrade silently to memory-only.
Error codes#
Raised through options.onError when you construct bemap.TilesAuth yourself. The map
creates its own instance internally and routes those errors to an internal channel that
2.0.2 exposes no public subscription for: in practice you observe them in the
console. See Map errors.
| Condition | bemap.Error code |
|---|---|
HTTP 401, or no tilesHost, or missing credentials, or a login response with no token | UNAUTHORIZED |
HTTP 403 | FORBIDDEN |
HTTP 429 | RATE_LIMITED |
| Any other non-2xx, or a transport failure | NETWORK |
navigator.onLine === false at call time | OFFLINE |
Notes#
Renewal timeline. After every successful login the SDK parses the JWT exp claim and schedules a refresh at exp − 5 minutes (clamped to [0, 24 h]); when exp cannot be parsed it falls back to a fixed 55 minutes. A 401 on a tile or style triggers handle401(): refresh, then a setStyle() reload so pending requests retry with the new token. On document.visibilitychange to visible the SDK re-logs in if the token has already expired, refreshes if it expires within 5 minutes, and does nothing otherwise. When navigator.onLine is false the renewal is queued and replayed on the next online event.
Credential-less: backend token provider. Your server holds the credentials, exposes one authenticated endpoint that forwards POST /api/login with the Basic header, and returns the JSON body unchanged; the browser receives only the short-lived JWT and fetches tiles directly from BeNomad. Only the tiles credentials move server-side, see The Context.
Credential-less: proxy (v2.0.2). new bemap.Context({ proxy: 'your-proxy.example.com', tilesHost: '…' }) routes the REST/WMS calls and the tiles login through your server, which injects the credentials; no Authorization ever leaves the browser. Tile bytes still go direct to tilesHost. Because the login response then comes from the proxy origin, the session cookie could never be sent to tilesHost, so the SDK forces the header wire, overriding an explicit tilesAuth: 'cookie' with a one-shot console warning ('query' is honoured). See The Context.
Gotchas#
tokenStoragevalues are'session'/'local'/'memory', not'sessionStorage'/'localStorage'. An unrecognised value silently falls back tosessionStorage.- Never patch
window.fetchyourself to inject the token. The SDK already owns a scoped, host-checked wrapper; a second patch double-stamps requests or drops theRangepolicy. - Pinning
tilesAuth: 'cookie'on a cross-site app will401in Safari, Firefox and Chrome incognito. That is precisely whatautoexists to avoid: leave it unset. getHistory()is diagnostic only. It deliberately keeps just theexpclaim, so logging it cannot exfiltrate a still-valid token. Never loggetToken().- A missing
login/passwordproduces a one-shot console warning, thenUNAUTHORIZED, not a silent blank map. Check the console before debugging CORS. isValid()returnstruewhenexpis unparseable. A non-JWT token is assumed good until the server answers401; the fixed 55-minute renewal covers the rest.logout()without{ serverSide: true }only clears the client. The session stays valid on the worker until it expires.- A
403on a.pmtilesrequest is usually not auth. NoRangeheader, aRangeover 10 MB, or a multi-partRangeare all rejected with403, see Troubleshooting.
See also#
- Overview & access: the access contract in context, environments, integration paths
- Web: PMTiles: where the token is attached on the browser path
- Mobile & fleet z/x/y: the
?token=query wire for native clients - Cache & slices: why the wire mode changes the preflight and cache behaviour
- Troubleshooting:
401/403/ CORS symptom to fix - The Context:
tilesHost,tilesAuth,tokenStorage,proxy - Error handling: the typed-rejection pattern
- Error codes: every
bemap.Errorconstant