Architecture reference · analytics layer · ~6 minute read

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

  1. Every session-authenticated server-side analytics read uses the workspace-wide admin token (TINYBIRD_ADMIN_TOKEN) injected as Authorization: Bearer <token> on the outbound HTTPS call to https://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.
  2. Per-workspace isolation is achieved at the pipe parameter layer. Every pipe SQL file in tinybird/pipes/ declares workspace_id as a parameter and includes WHERE workspace_id = {{ workspace_id }} in its query body. The Admaxxer server resolves the active workspace_id from 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.
  3. The JWT-mint path in server/lib/tinybird/jwt.ts exists 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.

  1. Browser issues a request to e.g. GET /api/v1/analytics/summary?since=…&until=… with the session cookie attached.
  2. Admaxxer’s Express middleware isAuthenticated resolves the session, attaches req.user, and passes the request to the route handler.
  3. The route handler calls the withTinybirdAndWebsite() helper in server/lib/analytics/withTinybirdAndWebsite.ts. This helper:
    • Resolves the active workspace_id from the session.
    • Confirms a pixel website exists for that workspace (otherwise short-circuits with data_status: 'no_pixel_website').
    • Reads TINYBIRD_ADMIN_TOKEN from process.env — if missing, returns 503 missing_config rather than calling Tinybird.
  4. 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 with Authorization: Bearer <TINYBIRD_ADMIN_TOKEN>.
  5. Tinybird parses the request, applies the pipe’s SQL with workspace_id bound to the URL value, and returns column-store-aggregated rows in JSON.
  6. 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:

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:

  1. Browser: GET /api/v1/analytics/summary?since=2026-04-01&until=2026-04-29 with cookie session=….
  2. Admaxxer Express → isAuthenticated resolves the session, attaches req.user = { id: '…', activeWorkspaceId: 'ef8eab5a-3ba9-44da-ab3c-086b94701935' }.
  3. Route handler calls withTinybirdAndWebsite({ req, pipe: 'summary', params: { since, until } }).
  4. Helper: workspaceId = req.user.activeWorkspaceId; token = process.env.TINYBIRD_ADMIN_TOKEN.
  5. 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.
  6. Helper calls fetch with Authorization: Bearer <TINYBIRD_ADMIN_TOKEN>, Accept: application/json.
  7. Tinybird parses, runs summary.pipe with workspace_id bound, returns:
    {
      "data": [
        { "revenue_total": 12345.67, "spend_total": 4321.00, "event_count": 8124 }
      ],
      "meta": […]
    }
  8. 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

Why a shared admin token instead of per-request JWTs?

Three reasons, in priority order:

  1. 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.”
  2. 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 Authorization headers; the token never appears in stack traces, error reports, or audit logs.
  3. 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