How Tinybird auth works in Admaxxer — admin token reads, per-workspace isolation, and the dormant JWT path
Admaxxer’s analytics surface is built on Tinybird — a column-store with a SQL pipe layer over ClickHouse. This page documents how the Admaxxer server authenticates against Tinybird, why session-authenticated reads use a shared admin token rather than a per-request JWT, how every pipe call is fenced to one workspace by parameter, and what the dormant JWT-mint path is reserved for. If you’re wondering “is my data isolated from another Admaxxer customer’s?” or “why does the source code mint JWTs that go unused?”, this is the canonical answer.
TL;DR
- Every session-authenticated server-side analytics read uses the workspace-wide admin token (
TINYBIRD_ADMIN_TOKEN) injected asAuthorization: Bearer <token>on the outbound HTTPS call tohttps://api.us-east.aws.tinybird.co. The token never leaves the Admaxxer server — the browser only ever talks to Admaxxer’s/api/v1/analytics/*routes, never directly to Tinybird. - Per-workspace isolation is achieved at the pipe parameter layer. Every pipe SQL file in
tinybird/pipes/declaresworkspace_idas a parameter and includesWHERE workspace_id = {{ workspace_id }}in its query body. The Admaxxer server resolves the activeworkspace_idfrom the session cookie, validates the user’s membership, and only then constructs the pipe URL. A user with no membership in workspace X cannot trigger a query that reads workspace X. - The JWT-mint path in
server/lib/tinybird/jwt.tsexists but is dormant in production: the Tinybird workspace doesn’t currently have a matching JWT secret registered, so any JWT we sign is rejected with “invalid authentication token. Workspace with ID … not found”. The path is preserved for the future widget surface where we’ll need browser-issued read tokens scoped to one pipe + one workspace. See GL#298.
The two canonical data flows
Admaxxer’s analytics has two distinct caller shapes and they auth against Tinybird differently.
Flow A — session-authenticated server-side reads (every dashboard endpoint)
This is the path used by /api/v1/analytics/summary, /summary-series, /source-medium, /marketing-acquisition/utm-coverage, the attribution drill-down, and every other endpoint that returns dashboard JSON to the React app.
- Browser issues a request to e.g.
GET /api/v1/analytics/summary?since=…&until=…with the session cookie attached. - Admaxxer’s Express middleware
isAuthenticatedresolves the session, attachesreq.user, and passes the request to the route handler. - The route handler calls the
withTinybirdAndWebsite()helper inserver/lib/analytics/withTinybirdAndWebsite.ts. This helper:- Resolves the active
workspace_idfrom the session. - Confirms a pixel website exists for that workspace (otherwise short-circuits with
data_status: 'no_pixel_website'). - Reads
TINYBIRD_ADMIN_TOKENfromprocess.env— if missing, returns 503missing_configrather than calling Tinybird.
- Resolves the active
- The helper builds the pipe URL:
https://api.us-east.aws.tinybird.co/v0/pipes/<pipe_name>.json?workspace_id=<wsid>&since=…&until=…and issues the GET withAuthorization: Bearer <TINYBIRD_ADMIN_TOKEN>. - Tinybird parses the request, applies the pipe’s SQL with
workspace_idbound to the URL value, and returns column-store-aggregated rows in JSON. - The Admaxxer server normalizes the response and ships it back to the browser.
The browser never sees the Tinybird host or the admin token. From the browser’s perspective Admaxxer is a regular JSON API.
Flow B — the dormant JWT path (browser-side reads, currently unused)
The token-vending routes at server/routes/pixelMetrics.ts and server/api/v1/pixel.ts mint short-lived JWTs that, in principle, would let a browser fetch directly from Tinybird without proxying through Admaxxer. The mint is implemented in server/lib/tinybird/jwt.ts:
- HS256-signed with
TINYBIRD_JWT_SECRET(must be ≥ 32 bytes). Mint throws if the env var is unset. - Default 120-second TTL (max 300 seconds) — not cached, freshly minted on every request as a simple replay defence.
- workspace_id is server-derived, never accepted from user input. It’s baked into
scopes[].fixed_params.workspace_id— Tinybird appliesfixed_paramsserver-side and ignores any overriding query param, so a compromised LLM or malicious client cannot switch workspaces at the HTTP layer. - Pipe name is whitelisted against a registered set at compile time — only known pipes can be requested.
Why it’s dormant in production: the matching JWT signing secret has not yet been registered with Tinybird’s workspace settings, so every JWT we sign is rejected with “invalid authentication token. Workspace with ID … not found.” The token-vending routes still mint — we cannot expose the admin token to the browser, that would leak full-workspace admin credentials — but the dashboards backed by /api/pixel/metrics/* remain inert until the secret is rotated. See server/lib/tinybird/jwt.ts for the full TO RE-ENABLE checklist.
Why we keep the path: the embeddable widget surface (currently in alpha) needs browser-issued tokens. When that ships, rotating the workspace secret to match TINYBIRD_JWT_SECRET turns Flow B back on with no code change.
Per-workspace data isolation — how it actually works
Tinybird is a single shared workspace from Admaxxer’s perspective — every Admaxxer customer’s pixel events land in the same datasource (events_v2) tagged with a workspace_id column. Multi-tenancy is enforced at the pipe layer, not the storage layer. Three guarantees combine to make this safe:
1. Every pipe declares workspace_id as a parameter
Open any pipe definition in tinybird/pipes/:
NODE summary_node
SQL >
%
SELECT
sum(revenue) AS revenue_total,
sum(spend) AS spend_total,
count() AS event_count
FROM events_v2
WHERE workspace_id = {{ String(workspace_id) }}
AND ts BETWEEN {{ DateTime(since) }} AND {{ DateTime(until) }}
The {{ String(workspace_id) }} binding is enforced by Tinybird’s pipe templating layer — if workspace_id is missing from the request, the pipe returns a 400. There’s no “forgot the WHERE” failure mode possible — the pipe author had to declare the parameter to use it.
2. Admaxxer always supplies workspace_id from the session
The withTinybirdAndWebsite() helper resolves workspace_id from the user’s authenticated session (req.user.activeWorkspaceId) and confirms the user has a row in workspace_members for that workspace. The helper signature does not accept a workspace_id argument from request bodies or query params — it’s always derived. Even a maliciously-crafted request body can’t coerce it to point at a different workspace.
3. JWT mints bake workspace_id into fixed_params
For the dormant JWT path (Flow B above), workspace_id is not a free query param — it’s a fixed_param on the JWT scope. Tinybird applies fixed_params server-side and refuses to override them from the URL. So even if the browser tries ?workspace_id=other-id, Tinybird ignores it and uses the JWT’s baked-in value. This is why the JWT path is safe to expose to a browser despite browsers being untrusted clients.
The combined effect: a query for workspace A’s data cannot be made to return workspace B’s data, regardless of which auth path is in use.
Worked example — a single summary call
Suppose user alice@example.com is a member of workspace ef8eab5a-3ba9-44da-ab3c-086b94701935 and opens the dashboard. Here’s the call chain end-to-end:
- Browser:
GET /api/v1/analytics/summary?since=2026-04-01&until=2026-04-29with cookiesession=…. - Admaxxer Express →
isAuthenticatedresolves the session, attachesreq.user = { id: '…', activeWorkspaceId: 'ef8eab5a-3ba9-44da-ab3c-086b94701935' }. - Route handler calls
withTinybirdAndWebsite({ req, pipe: 'summary', params: { since, until } }). - Helper:
workspaceId = req.user.activeWorkspaceId;token = process.env.TINYBIRD_ADMIN_TOKEN. - Helper builds:
https://api.us-east.aws.tinybird.co/v0/pipes/summary.json?workspace_id=ef8eab5a-3ba9-44da-ab3c-086b94701935&since=2026-04-01&until=2026-04-29. - Helper calls fetch with
Authorization: Bearer <TINYBIRD_ADMIN_TOKEN>,Accept: application/json. - Tinybird parses, runs
summary.pipewithworkspace_idbound, returns:{ "data": [ { "revenue_total": 12345.67, "spend_total": 4321.00, "event_count": 8124 } ], "meta": […] } - Admaxxer normalizes and returns to the browser.
If alice attempts to forge a request claiming a different workspace_id (e.g. by editing localStorage or modifying the request), the session middleware ignores it — workspace_id is read from the session record in Postgres, not the request payload. The helper never reads workspace_id from anything mutable by the browser.
Environment variables — what’s in Coolify
TINYBIRD_ADMIN_TOKEN— required. The single workspace-scoped admin token Admaxxer uses for every server-side analytics read. Stored in Coolify env, never in the repo. Rotation is a one-line UI flip in Coolify followed by a deploy.TINYBIRD_HOST— defaults tohttps://api.us-east.aws.tinybird.co. Override for non-AWS-us-east-1 workspaces. Admaxxer’s production workspace lives in AWS us-east-1.TINYBIRD_JWT_SECRET— required for the dormant JWT path (Flow B above) to mint signed tokens. Even though the path is dormant, the mint code throws on startup if this env is missing — rather than silently failing later.ADMAXXER_ALLOW_TB_ADMIN— gates the destructive admin operations inserver/lib/tinybird/admin.ts(createDatasource, createPipe, createToken, rotateToken). Default false — only set totruefor explicit maintenance windows. Every call also writes an audit row toadmin_actions_log.
Why a shared admin token instead of per-request JWTs?
Three reasons, in priority order:
- The JWT secret rotation hasn’t happened yet. The Tinybird workspace was provisioned with a pre-existing JWT signing secret that doesn’t match Coolify’s
TINYBIRD_JWT_SECRET. Until that’s rotated to match, every JWT we mint is rejected. Falling back to the admin token kept the dashboards working through R31 — an evergreen choice when your alternative is “the dashboards are broken until ops rotates a secret.” - The admin token never leaves the server. The risk profile is identical to any other server-side credential (Stripe secret, Anthropic API key, Postgres password). Admaxxer’s log scrubbers strip
Authorizationheaders; the token never appears in stack traces, error reports, or audit logs. - Per-workspace isolation is achieved by the pipe parameter, not the auth token. A JWT scoped to one workspace would also filter by workspace_id at the pipe layer. The auth choice is independent of the isolation guarantee — both paths route through
WHERE workspace_id = …, regardless of whether the token is admin or JWT.
Once the JWT secret is rotated, Admaxxer will move the browser-side widget surface onto Flow B and keep server-side dashboard reads on Flow A. Two-path equilibrium, each used where it’s the best fit.
FAQ
Q: Is my workspace’s data isolated from other Admaxxer customers?
Yes. Every pipe filters by workspace_id, and Admaxxer always supplies your workspace_id from the authenticated session. A query for your data cannot return another customer’s rows, and vice versa. The unit-test suite at server/lib/tinybird/jwt.test.ts pins the cross-workspace boundary with explicit cases.
Q: Does the Admaxxer server share the admin token with the browser?
No. The admin token lives in process.env.TINYBIRD_ADMIN_TOKEN on the server only. Every browser-bound response is regular JSON; the Tinybird host and the bearer token are not exposed to client code. The browser talks only to Admaxxer’s own /api/v1/* routes.
Q: Why does the source code have a JWT-mint path that’s currently rejected?
Because the future widget surface needs it. Browser-issued tokens for embeddable dashboards must be JWTs — an admin token would leak workspace-wide admin access if exposed to a browser. The JWT mint will work as soon as Tinybird’s workspace JWT secret is rotated to match TINYBIRD_JWT_SECRET in Coolify.
Q: What happens if TINYBIRD_ADMIN_TOKEN is missing or invalid?
Every dashboard endpoint returns data_status: 'missing_config' with HTTP 503 — the React app shows an inert shimmer state with a banner directing the workspace owner to contact support. We deliberately don’t fall through to a no-auth Tinybird call (which would be a security smell). Missing config is an ops issue surfaced explicitly, not a silent degradation.
Q: How do I rotate the admin token?
Two steps. (1) In Tinybird workspace settings, generate a new admin token. (2) Paste the new value into Coolify’s TINYBIRD_ADMIN_TOKEN env var and redeploy — takes ~3 minutes. The old token can be revoked from the same Tinybird settings page once the deploy lands. Pipe SQL is unaffected by token rotation.
Related
Bring your own Anthropic key (BYOK) · Analytics deep dive · Developer & API · Documentation home