This diagram covers mapbox-cli and how it connects to Mapbox's cloud
services and to the user's machine. It does not cover how Mapbox's cloud
API services work internally. The CLI has no cloud infrastructure of its
own (no VPC, load balancer, or IAM roles). It is a local client that talks
directly to public Mapbox API endpoints.
There are four request-response cases that happen at runtime. Each one crosses trust boundaries differently, so each gets its own diagram. Legend (shared by all four): 🔒 means the arrow crosses the network (HTTPS/TLS). Arrows without 🔒 stay on the local machine (same process or same disk, no network). Dashed arrows happen once or in one direction only (a compile-time embed, or a check done only at startup).
flowchart LR
user(["User"])
subgraph local["User's local machine"]
bin["mapbox binary"]
loopback["Local loopback listener<br/>127.0.0.1:<port>/callback<br/>one-time, localhost-only"]
creds["credentials.json<br/>~/.mapbox/ (or $MAPBOX_CONFIG_DIR)<br/>file 0600, dir 0700 on Unix"]
end
subgraph cloud["Mapbox Cloud · api.mapbox.com"]
oauth["OAuth endpoints (PKCE)<br/>authorize / register / token"]
end
user -- "mapbox login" --> bin
bin -- "① start listener + open browser" --> loopback
bin -- "② 🔒 HTTPS GET /authorize (via browser)" --> oauth
oauth -- "③ 🔒 redirect: code + state" --> loopback
loopback -- "④ hand code back (in-process)" --> bin
bin -- "⑤ 🔒 POST /token (code + code_verifier)" --> oauth
oauth -- "⑥ 🔒 access / refresh token" --> bin
bin -- "⑦ write credentials.json" --> creds
Steps ①–⑥ cover dynamic client registration, PKCE
(code_verifier/code_challenge), state validation against CSRF, and the
one-time loopback listener. See src/auth.rs.
Step ⑦ is a plain JSON file, not an OS keychain — there is no keychain,
Credential Manager or Secret Service integration in this CLI. auth.rs's
write_private is the only thing that writes it:
- The directory —
~/.mapbox, or whateverMAPBOX_CONFIG_DIRnames — is created andchmoded to0700before anything is written into it. - The contents go to a scratch file beside the target, opened with
create_newand mode0600, so the file is owner-only from the instant it exists and a symlink planted in its place is never written through. - The mode is pinned again with
set_permissionsafter the write, becauseopen(2)'s mode is masked by the umask. - The scratch file is
fsynced and then renamed overcredentials.json, so a crash or a concurrent reader never sees a half-written file, and the mode of whatever it replaced is irrelevant — a rename swaps in a whole new inode.
Two limits follow from that. The 0700/0600 hardening is Unix-only; off
Unix the file carries whatever permissions the OS gives it. And on every
platform the token is readable by anything running as the same user — file
permissions are the whole of the protection.
load_fresh_credentials() runs on every command, including ones
authenticated via --token/MAPBOX_ACCESS_TOKEN, so a missing or
unreadable store reports "no stored credentials found" rather than failing
the command.
flowchart LR
user(["User"])
subgraph local["User's local machine"]
bin["mapbox binary"]
store["Stored credentials<br/>(credentials.json — see Diagram 1)"]
out["Terminal stdout<br/>JSON output; non-2xx → exit(1)"]
end
subgraph cloud["Mapbox Cloud · api.mapbox.com"]
api["Mapbox APIs (13 services)<br/>accounts, directions, feedback, fonts,<br/>geocoder, isochrone, map-match, matrix,<br/>search, sprites, static, styles, tilesets"]
end
user -- "mapbox styles ... / mapbox tilesets query ... / etc." --> bin
store -. "① read stored token;<br/>auto-refresh if expiring within 5 min" .-> bin
bin -- "② 🔒 HTTPS + ?access_token=…" --> api
api -- "③ 🔒 JSON response" --> bin
bin -- "④ print result" --> out
Covers every existing mapbox <service> <operation> command (src/executor.rs).
The token is read from wherever Diagram 1 stored it. It is refreshed in
place if it's close to expiring, then sent as an access_token query
parameter. It is not sent as an Authorization header, because that
matches how these APIs have always been called.
The request also carries User-Agent: mapbox-cli/<version>, plus telemetry
markers today: os/, arch/, a command/<group> naming the command group
that ran, an agent/ marker when an AI coding agent is detected, and a
couple more (src/telemetry.rs builds the full list). This is how Mapbox's
logs identify this CLI's traffic. src/http.rs is the only place an HTTP
client is built, so every request goes through it. Everything past the
version is gated on MAPBOX_CLI_NO_TELEMETRY: set it and only the product
token ships.
flowchart LR
user(["User"])
subgraph local["User's local machine"]
bin["mapbox binary"]
store["Stored credentials<br/>(see Diagram 1)"]
tcli["tilesets<br/>(PyPI mapbox-tilesets — installed<br/>separately by the user, not by us)"]
out["Terminal stdout/stderr"]
end
subgraph cloud["Mapbox Cloud · api.mapbox.com"]
tapi["Tilesets API"]
end
user -- "mapbox tilesets-cli ..." --> bin
store -. "① read, refreshed if stale" .-> bin
bin -- "② exec(2): argv forwarded verbatim,<br/>token injected as an environment variable" --> tcli
tcli -- "③ 🔒 HTTPS + token" --> tapi
tapi -- "④ 🔒 response" --> tcli
tcli -- "⑤ writes to the terminal directly" --> out
Implemented in src/tilesets_cli.rs. Everything after mapbox tilesets-cli
is passed to the tilesets binary unchanged, including flags. So
mapbox tilesets-cli --help prints the Tilesets CLI's own help. If
tilesets isn't installed, the CLI prints install instructions instead of
running it.
Three points matter here:
- The token is passed by environment variable, never by argv.
tilesetslooks for its token in this order: the--tokenflag, thenMAPBOX_ACCESS_TOKEN, thenMapboxAccessToken. We set theMAPBOX_ACCESS_TOKENenvironment variable on the child process. Putting the token in argv instead would expose it to every other local user throughps, so we rejected that option. Nothing is injected at all if the environment already has a token under either nametilesetsreads. - Token precedence follows the same order as the rest of the CLI: flag,
then environment, then stored credentials. The environment variable
stays an override, because it's the only option available to a script,
container, or CI job, and because comparable tools use the same order.
This has a cost: a token left in a shell profile can shadow a later
mapbox auth loginindefinitely, showing up as a plainNot foundfrom the API instead of a clear auth error. We reduce that risk with a warning when the environment token and the stored login belong to different accounts, rather than by changing the order. An explicit--tokengiven after the subcommand name is still forwarded and still wins, sincetilesetsprefers its own flag. - No new network boundary. On this path,
mapboxmakes no requests of its own except a token refresh, which is the same OAuth exchange already covered by Diagram 1. Every other network crossing shown is onetilesetswould make anyway. On Unix, the proxy usesexecinstead of spawning a subprocess, so this process is replaced by the child. No wrapper process is left holding a token in memory.
A stored token now drives the Tilesets API here, so its scope matters. A
mapbox auth login token carries tilesets:list and tilesets:write,
which cover this in practice.
One thing is not done, and won't be: mapbox does not install
tilesets. It only checks whether the binary is reachable, and prints
install instructions if it isn't.
flowchart LR
bin["mapbox binary<br/>(the run that just finished)"]
subgraph local["User's local machine"]
child["mapbox binary<br/>(detached child,<br/>MAPBOX_INTERNAL_TELEMETRY_SEND=1)"]
end
subgraph cloud["Mapbox Cloud · events.mapbox.com<br/>(not api.mapbox.com)"]
events["Mapbox Events<br/>POST /events/v2"]
end
bin -- "① spawn detached, event<br/>on stdin (never argv or env)" --> child
child -- "② 🔒 HTTPS POST" --> events
Built in src/telemetry_event.rs, delivered by src/telemetry_sink.rs. One
cli.command event per run — what ran, how it ended, the environment
markers described above — goes to Mapbox Events, a different host from the
one every other diagram here talks to. MAPBOX_CLI_NO_TELEMETRY skips this
entirely, same as Diagram 2.
Step ① is detached and non-blocking: the parent spawns a copy of itself in
this special mode and exits without waiting on it, so a dead network or a
slow endpoint never adds to a command's run time. The event goes over the
child's stdin rather than its argv or environment, for the same
ps-visibility reason Diagram 3 gives. The child is not handed the user's
token at all.
Step ② is sent with the CLI's own pk. token only, never the user's
(src/cli_token.rs): MAPBOX_CLI_TOKEN when set to a pk. token,
otherwise the one every build fetches from
https://cli.mapbox.com/config/token.json and keeps in
~/.mapbox/cli-token.json until the Tokens API says it is dead
(src/cli_token/fetch.rs). With no usable CLI token the event is dropped,
and a send that fails is not tried again. Three failures in a row (a fetch
with no usable token, or a refusal with no different token served) quiet
telemetry for an hour, a count kept per caller in that same file. A new
token saved by any caller ends every caller's count.