queen_proxy is a second Rust binary that stands in front of a broker and turns it into
something you can hand to other people. Both are Apache-2.0, so the multi-tenant service is
yours to run: for internal teams, for customers, or under a product of your own.
The broker already scopes queues by tenant on its own, behind QUEEN_TENANCY_HEADER
(broker-native tenancy). What the proxy adds is the
reason to trust that header: it spends the client’s credential at the edge, strips it, and
injects the tenant itself. Around that it holds what a shared broker has no place holding.
Per-cluster API keys and human logins
(credentials), plan limits on rate, size and count
(quotas), a route classifier that forwards nothing it cannot
name (endpoints), and usage counted from the per-item
statuses the broker actually returned (metering).
The shape
A cell is one proxy, one broker, one PostgreSQL for the messages. The proxy needs a second PostgreSQL of its own, pxdb, carrying the tenants, clusters, keys, plans and usage of the whole fleet. The proxy reads it through a 30-second cache, which is what keeps a pxdb outage from stopping the data plane.
Three nouns carry the model:
- Tenant: the organisation. Owns users and clusters.
- Cluster: the tenant-visible Queen. One hostname, one plan, one queue namespace.
- Cell: one physical stack, on one failure domain.
A cluster lives on exactly one cell and never spans two. Every limit rests on that: a cluster’s traffic can only arrive at the one proxy fronting its cell, so the counters are plain in-process state, exact with no Redis and nothing to coordinate. One proxy per cell is the assumption the code is written against.
Routing is a DNS label. The proxy reads the cluster slug from the first label of the Host
header, so acme-prod.eu1.example.com is the cluster acme-prod, and one wildcard record
covers every cluster on the cell.
Bring up a cell
-
Mint the cell secret. The broker holds an HMAC secret; the cell row holds a token signed with it. The same string does not go in two places.
CELL_JWT_SECRET=$(openssl rand -hex 32) b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; } header=$(printf '{"alg":"HS256","typ":"JWT"}' | b64url) claims=$(printf '{"sub":"queen-proxy","role":"admin"}' | b64url) sig=$(printf '%s.%s' "$header" "$claims" | openssl dgst -binary -sha256 -hmac "$CELL_JWT_SECRET" | b64url) CELL_SECRET="$header.$claims.$sig"The
adminrole is required, because the proxy forwards queue and consumer-group deletions. There is noexpon purpose: nothing refreshes this value, so rotate it by minting a new one. -
Run the broker, on a network policy that admits the proxy and nobody else.
JWT_ENABLED=true JWT_ALGORITHM=HS256 JWT_SECRET="$CELL_JWT_SECRET" QUEEN_TENANCY_HEADER=true QUEEN_KV_TRUSTED_PROXY=true ./bin/queenThe second of those two is not optional and the broker exits 1 without it. The tenant header is opaque and validated against nothing, and key/value state is addressable purely by name, so a forged header reads and writes another tenant’s state knowing only that name. Queues survive a forged header because a caller must still name a queue and an ack carries a lease id it cannot guess; the KV has no such id. Since the KV is part of the engine on every cell and no flag can take it away, the requirement is unconditional: setting
QUEEN_KV_TRUSTED_PROXY=trueis you stating that step 3 is what supplies the header and strips the client’s. If that is not true of your deployment, do not run with the header. -
Run the proxy.
PXDB_HOSTis the only variable it refuses to start without, and the six migrations that create thequeen_proxyschema are embedded in the binary and applied at boot. GenerateQUEEN_PROXY_JWT_SECRETonce and reuse it: sessions are signed with it, so changing it logs every human out.SESSION_SECRET=$(openssl rand -hex 32) docker run -d --name queen-proxy -p 6711:6711 -e PXDB_HOST=10.0.3.20 -e PXDB_PASSWORD=change-me -e QUEEN_PROXY_JWT_SECRET="$SESSION_SECRET" -v queen-proxy-spool:/app/spool ghcr.io/queen-mq/queen-proxy:latestcurl -s http://127.0.0.1:6711/healthz{"enforce":false,"status":"ok","tenant_header":true}How to run it in production, with Docker or Kubernetes, and why it stays on one replica: Proxy.
-
Register the cell. No SQL function creates one: cells are fleet infrastructure.
base_urlmust behttp://, an assertion that broker traffic is cell-internal with TLS terminated at the proxy edge.psql -h 10.0.3.20 -U postgres -d queen_proxy -v cell_secret="$CELL_SECRET" <<'SQL' INSERT INTO queen_proxy.cells (slug, region, base_url, class, cell_secret) VALUES ('eu1-shared-a', 'eu1', 'http://10.0.3.11:6632', 'shared', :'cell_secret'); SQL -
Point a wildcard DNS record at the proxy, so every cluster slug on this cell resolves to it.
Onboard a tenant
Nothing over HTTP creates a tenant. Every mutation of tenants, clusters, users, keys and roles
is a SQL function in the queen_proxy schema, which is what keeps the audit row and the cache
invalidation from being optional. One call does the whole onboarding, and unwinds completely if
any part of it raises.
SELECT queen_proxy.bootstrap_tenant(
'acme', -- tenant slug
'Acme Inc', -- tenant name
'acme-prod', -- cluster slug, and the subdomain
'free', -- plan code
(SELECT id FROM queen_proxy.cells WHERE slug = 'eu1-shared-a'),
'ops@acme.example', -- admin email
'initial-password', -- NULL for an OAuth-only admin
'default' -- API key name
);It returns {tenant_id, cluster_id, user_id, api_key}. The key is plaintext, shown exactly
once, and only its sha256 hash is stored; a re-run on the same slugs returns the existing ids
with a null key.
That key is everything a client needs. Same routes, same SDKs, same wire format as a bare broker, with the hostname doing the routing:
curl -s -X POST http://acme-prod.eu1.example.com:6711/api/v1/push \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"items":[{"queue":"orders","partition":"cust-42","payload":{"hello":"world"}}]}'The plan named in that call decides the numbers: requests and messages a second, queues, partitions per queue, parked long polls, payload and batch size, retained bytes and retention window. Four plans ship seeded, and a single cluster is raised above its plan with a jsonb column rather than a forked plan (the data model).
Afterwards, the same schema carries the rest of the lifecycle, one function per operation so the audit row and the cache invalidation are never optional:
-- another human on the tenant, and their role on one cluster
SELECT queen_proxy.create_user(:tenant_id, 'dev@acme.example', NULL, 'google');
SELECT queen_proxy.grant_cluster_role(:cluster_id, 'dev@acme.example', 'admin');
-- a second API key, for a service that should be revocable on its own
SELECT queen_proxy.issue_api_key(:cluster_id, 'ingest-worker', :key_hash, ARRAY['push']);A password hash of NULL is the OAuth-only user: they exist, they have a role, and they can
only arrive through Google.
Human logins with Google
API keys are for programs. People sign in to the dashboard, and the proxy speaks OAuth for that: Google and GitHub, plus local bcrypt passwords. Google needs two variables.
GOOGLE_CLIENT_ID=...apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=...
GOOGLE_ALLOWED_DOMAINS=acme.example,acme.io # optional, comma separatedIn the Google Cloud console the authorised redirect URI is <public base>/auth/google/callback,
and getting that base right is the one thing that breaks a first setup. The proxy uses
QUEEN_PROXY_PUBLIC_URL when it is set, otherwise it rebuilds the URL from the request’s Host
and X-Forwarded-Proto. With QUEEN_PROXY_AUTH_HOST on and no public URL set, OAuth is
switched off rather than guessed, which is the correct behaviour for a dedicated login host and
a confusing one if you did not mean it.
GOOGLE_ALLOWED_DOMAINS restricts who may sign in, by the account’s hosted domain or its email
domain. A single entry is also passed to Google as the hd hint, so the account chooser shows
that domain first.
What happens when someone arrives is worth knowing exactly, because two of the three outcomes are refusals:
- The Google identity is already linked to a user: they are logged in.
- No link, but the verified email matches a user: the identity is linked to that user. An unverified email never links, which is what stops an account being claimed by someone who merely typed the address.
- Neither: the login is denied, unless
QUEEN_PROXY_AUTOPROVISIONis on, in which case a user is created with the role inQUEEN_PROXY_DEFAULT_ROLE.
And the part that makes this safe to leave on: a session is not cluster-scoped. The role
inside the session token is a placeholder, and the real permission is resolved live from the
cluster role rows. A brand-new Google user with no grant_cluster_role can sign in and see
nothing at all, so auto-provisioning hands out an account, never an authorisation.
Turn enforcement on
QUEEN_PROXY_ENFORCE defaults to false, and that is shadow mode: every rate and quota
decision is computed, logged and metered, and the request proceeds anyway. Size caps and the
two push-block quotas are hard regardless of the flag
(quotas). Watch a real workload through it, then set it to
true and confirm on /healthz, which reports the flag precisely because a boot log survives
the process that wrote it.
One proxy per cell, one cell per cluster, one SQL call per tenant, and a 429 contract the SDKs
already honour.
Data model
Tables, statuses, the seeded plans, and every provisioning function.
Quotas and rate limits
What is enforced, what only warns, and what ignores shadow mode.
Isolation
What the design guarantees, and which broker surfaces a tenant must never reach.
Dashboard
The same bundle, served by the proxy with real users and per-cluster roles.