Queen MQ
Server setup

One C++ binary. PostgreSQL underneath. Stateless on top.

The Queen server is a single statically-linked C++17 binary built on uWebSockets and the libpq async API. It serves the HTTP API and the Vue 3 dashboard from the same process, on the same port.

Docker

Multi-arch image (linux/amd64 + linux/arm64), pushed to Docker Hub on every release. Current stable: 0.14.1 — pin with smartnessai/queen-mq:0.14.1 for reproducible deployments.

development
docker network create queen

docker run --name qpg --network queen \
  -e POSTGRES_PASSWORD=postgres \
  -p 5433:5432 -d postgres

docker run -p 6632:6632 --network queen \
  -e PG_HOST=qpg -e PG_PASSWORD=postgres \
  -e NUM_WORKERS=2 -e DB_POOL_SIZE=5 \
  -e LOG_LEVEL=info \
  smartnessai/queen-mq:latest
production-ish single-node
docker run -d --name queen --restart unless-stopped \
  -p 6632:6632 \
  -v /var/lib/queen-buffers:/var/lib/queen/buffers \
  -e PG_HOST=db.internal \
  -e PG_USER=queen \
  -e PG_PASSWORD=$PG_PASSWORD \
  -e PG_DB=queen \
  -e PG_USE_SSL=true \
  -e NUM_WORKERS=10 \
  -e DB_POOL_SIZE=150 \
  -e SIDECAR_POOL_SIZE=50 \
  -e DEFAULT_SUBSCRIPTION_MODE=new \
  -e LOG_LEVEL=info \
  smartnessai/queen-mq:latest

PostgreSQL requirements

Environment variables (essentials)

Full list lives in server/ENV_VARIABLES.md. These are the ones you almost always touch.

VariableDefaultWhat it controls
PORT6632HTTP listener port.
HOST0.0.0.0HTTP bind address.
NUM_WORKERS10uWebSockets event-loop threads.
PG_HOSTlocalhostPostgreSQL hostname.
PG_USERpostgresPostgreSQL username.
PG_PASSWORDpostgresPostgreSQL password.
PG_DBpostgresDatabase name.
PG_USE_SSLfalseTLS to PostgreSQL.
DB_POOL_SIZE150Async connection pool for the hot path.
SIDECAR_POOL_SIZE50Sidecar pool for batch/transaction work.
DEFAULT_SUBSCRIPTION_MODE"" (all)Default for new consumer groups: "" or "new".
LOG_LEVELinfotrace · debug · info · warn · error.
FILE_BUFFER_DIR/var/lib/queen/buffers (Linux)Disk failover buffer location.
QUEEN_ENCRYPTION_KEY,64 hex chars (AES-256-GCM at-rest payload encryption).

JWT authentication

Queen supports HS256 (shared secret), RS256 (RSA), and EdDSA (Ed25519), including external IDPs via JWKS auto-discovery. Routes are gated by role:

LevelRoutesRequired role
PUBLIC/health, /metrics, /metrics/prometheus, / (dashboard)none
READ_ONLYGET /api/v1/status/*, /api/v1/resources/*any valid token
READ_WRITEPOST /api/v1/push, GET /api/v1/pop/*read-write or admin
ADMIN/api/v1/system/*, DELETE operationsadmin
HS256, internal services
export JWT_ENABLED=true
export JWT_ALGORITHM=HS256
export JWT_SECRET=$(openssl rand -hex 32)
RS256 / EdDSA, external IDP via JWKS
export JWT_ENABLED=true
export JWT_ALGORITHM=auto                                  # or RS256 / EdDSA
export JWT_JWKS_URL=https://your-idp.com/.well-known/jwks.json
export JWT_ISSUER=https://your-idp.com/
export JWT_AUDIENCE=queen-api
producerSub anti-impersonation

With JWT enabled the server stamps the validated sub claim onto every pushed message as producerSub. Clients cannot set this field , the server always derives it from the verified JWT. It's exposed on pop responses and admin message APIs for downstream attribution.

Auth proxy — login over the dashboard

The Queen server speaks Bearer JWT only, there is no built-in username/password login form. For machine traffic that's exactly what you want. But the Vue 3 dashboard is served from the same port, and you usually want a human-friendly login page in front of it. That's what queen-mq-proxy is for.

It's a small Node.js (Express) reverse proxy that sits in front of Queen, gives you a login page, validates credentials, mints a short-lived HS256 JWT into an HTTP-only cookie, and forwards every authenticated request to Queen with Authorization: Bearer <jwt>. It also supports SSO passthrough: point it at an external IDP's JWKS URL and it will accept your existing RS256 / EdDSA tokens and pass them through to Queen unchanged. Users live in a queen_proxy schema on the same PostgreSQL Queen already uses, so there is no extra database to operate.

Topology: browser → queen-mq-proxy (:3000) → queen-mq (:6632). The proxy also strips oversized cookie / referer headers before forwarding, which avoids “Request Header Fields Too Large” errors from Queen on long-running browser sessions.

Run with Docker

shell, alongside Queen
docker run -d --name queen-proxy --network queen \
  -p 3000:3000 \
  -e PG_HOST=qpg \
  -e PG_DB=postgres \
  -e PG_USER=postgres \
  -e PG_PASSWORD=postgres \
  -e QUEEN_SERVER_URL=http://queen:6632 \
  -e JWT_SECRET=$(openssl rand -hex 32) \
  -e JWT_EXPIRES_IN=24h \
  -e NODE_ENV=production \
  smartnessai/queen-mq-proxy:latest

# Create the first admin user (interactive prompts).
docker exec -it queen-proxy node src/create-user.js

Open http://localhost:3000/login, log in, and you'll land on the dashboard at /. The queen_proxy.users and queen_proxy.sessions tables are created automatically on first start.

Roles & method gating

The proxy enforces role-based method access on top of whatever Queen itself does:

RoleAllowed HTTP methodsUse case
admin All methods Operators. Full read/write/destroy.
read-write GET, POST, PUT, DELETE Standard developer access.
read-only GET only Monitoring / read-only dashboard users.

The CLI (src/create-user.js) can also mint a long-lived bearer token at user creation time (24h / 7d / 30d / 1y / never), useful for service accounts that hit the proxy from CI or microservices.

SSO passthrough (optional)

Set EXTERNAL_JWKS_URL and the proxy will verify incoming bearer tokens against your IDP's JWKS (RS256, EdDSA, etc.), normalize the claims (sub, preferred_username, roles), and pass the original token through to Queen unchanged. This way a single token works across the proxy and the API, and you don't run a parallel user database for humans who already authenticate against your IDP.

SSO env
export EXTERNAL_JWKS_URL=https://idp.example.com/.well-known/jwks.json
export EXTERNAL_ISSUER=https://idp.example.com
export EXTERNAL_AUDIENCE=queen-mq

Internal-token verification still works in parallel; the proxy tries the external token first, falls back to the internal HS256 cookie if the external verification fails. Useful while migrating users.

Sign in with Google (OAuth 2.0)

In addition to local username/password and JWKS passthrough, the proxy can drive a full OAuth 2.0 Authorization Code flow with Google itself, so the login page exposes a Sign in with Google button. The proxy verifies Google's id_token against oauth2/v3/certs, resolves (or auto-provisions) a row in queen_proxy.users, and then mints the same internal HS256 cookie as the password flow, so RBAC and token-forwarding to the Queen server work unchanged.

1. Create the OAuth client

In Google Cloud Console → APIs & Services → Credentials, create an OAuth 2.0 Client ID of type Web application:

Save and copy the Client ID and Client secret. Best practice is one OAuth client per environment, so a leaked stage secret can't be replayed against prod.

2. Provide the env vars

The proxy reads six GOOGLE_* variables. The recommended pattern is a single Kubernetes Secret consumed via envFrom:

kubectl create secret
kubectl create secret generic queen-proxy-google -n queen \
  --from-literal=GOOGLE_CLIENT_ID='<client-id>.apps.googleusercontent.com' \
  --from-literal=GOOGLE_CLIENT_SECRET='<client-secret>' \
  --from-literal=GOOGLE_REDIRECT_URI='https://queen.example.com/api/auth/google/callback' \
  --from-literal=GOOGLE_ALLOWED_DOMAINS='example.com' \
  --from-literal=GOOGLE_AUTO_PROVISION='true' \
  --from-literal=GOOGLE_DEFAULT_ROLE='read-only'

Then enable the Google block in the chart values; the StatefulSet adds the secret to envFrom automatically.

proxy/helm/prod.yaml
googleAuth:
  enabled: true
  secretName: queen-proxy-google   # default: queen-mq-proxy-google-<env>

3. Egress — let the pod reach Google

The callback exchanges the authorization code by calling https://oauth2.googleapis.com/token from the pod. If your cluster uses NetworkPolicies that block outbound traffic by default, that call will time out with ConnectTimeoutError. Add an egress label that matches a policy permitting public-internet egress (or at least the googleapis.com hosts), e.g. np.egress.internet: 'true' on the proxy pod template.

4. User resolution

After verifying the id_token the proxy looks up the local user in this order:

  1. by google_sub if previously linked — sign in;
  2. else by verified email (links the Google identity to the existing local user) — sign in;
  3. else if GOOGLE_AUTO_PROVISION=true — create a new user with role GOOGLE_DEFAULT_ROLE;
  4. else — deny with ?error=not_provisioned.

Linking by email is gated on Google reporting email_verified=true to prevent takeover by a third party who registers a Google account at someone else's address. Promotion to read-write / admin stays a manual UPDATE on queen_proxy.users.

Common pitfalls

Helm chart

Deploy as a sibling release to queen-mq in the same namespace. The chart we run lives at proxy/helm/; it ships a StatefulSet, a ClusterIP Service, an HTTPRoute (Gateway API), and a GKE HealthCheckPolicy hitting /health/ready.

proxy/helm/values-prod.yaml
replicas: 2

image: smartnessai/queen-mq-proxy
imageTag: 0.0.13

# External hostname routed to the proxy via the Gateway API.
proxyHostName: queen.example.com

resources:
  limits:    { memory: 200Mi }
  requests:  { memory: 200Mi }
proxy/helm/templates/statefulset.yaml (excerpt)
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: {{ .Release.Name }}
  namespace: {{ .Release.Namespace }}
  labels: { run: {{ .Release.Name }} }
spec:
  replicas: {{ .Values.replicas }}
  selector:
    matchLabels: { run: {{ .Release.Name }} }
  template:
    metadata:
      labels: { run: {{ .Release.Name }} }
    spec:
      terminationGracePeriodSeconds: 5
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels: { run: {{ .Release.Name }} }
      containers:
        - name: proxy
          image: {{ .Values.image }}:{{ .Values.imageTag }}
          imagePullPolicy: Always
          ports:
            - { containerPort: 3000, protocol: TCP }
          resources: {{- toYaml .Values.resources | nindent 12 }}
          envFrom:
            - secretRef: { name: postgres-queen-owner }   # PG creds
            - secretRef: { name: queen-mq-proxy }         # JWT_SECRET, optional EXTERNAL_*
            # Optional, mounted only when .Values.googleAuth.enabled is true.
            # Defaults to queen-mq-proxy-google-{{ .Values.env }} unless
            # .Values.googleAuth.secretName is set.
            # - secretRef: { name: queen-proxy-google }   # GOOGLE_*
          env:
            - { name: PG_DB,            value: queen }
            - name: QUEEN_SERVER_URL
              value: http://queen-mq.{{ .Release.Namespace }}.svc.cluster.local:6632

An HTTPRoute (Gateway API) maps {{ .Values.proxyHostName }} to the proxy's port 3000, and a HealthCheckPolicy probes /health/ready. Apply, then bootstrap the first admin user from inside a running pod:

install + bootstrap
helm template queen-mq-proxy ./proxy/helm \
  -f ./proxy/helm/prod.yaml \
  --namespace queen | kubectl apply -n queen -f -

kubectl exec -n queen -it queen-mq-proxy-0 -- node src/create-user.js

Environment variables

VariableDefaultNotes
PG_HOST / PG_PORT / PG_DB localhost / 5432 / postgres Same convention as the Queen server. Schema queen_proxy is created on first start.
PG_USER / PG_PASSWORD postgres / postgres Use envFrom + a Kubernetes Secret in production.
PG_USE_SSL off Set to any non-empty value to enable TLS to PostgreSQL.
QUEEN_SERVER_URL http://localhost:8080 Where authenticated traffic is forwarded.
JWT_SECRET Required. HS256 secret for internally-issued tokens. Generate with openssl rand -hex 32.
JWT_EXPIRES_IN 24h Use 'never' for service-account tokens minted via the CLI.
EXTERNAL_JWKS_URL Optional. Enables SSO passthrough against this IDP.
EXTERNAL_ISSUER / EXTERNAL_AUDIENCE Optional. Validate iss / aud claims on external tokens.
GOOGLE_CLIENT_ID Optional. Sets together with GOOGLE_CLIENT_SECRET and GOOGLE_REDIRECT_URI to enable the Sign in with Google button.
GOOGLE_CLIENT_SECRET OAuth 2.0 client secret from Google Cloud Console.
GOOGLE_REDIRECT_URI Must exactly match an authorized redirect URI on the OAuth client, e.g. https://queen.example.com/api/auth/google/callback.
GOOGLE_ALLOWED_DOMAINS Comma-separated allowlist matched against the Google hd claim or the email domain. Empty = allow any verified email.
GOOGLE_AUTO_PROVISION false If true, create a local user on first Google login. If false, the user must already exist in queen_proxy.users (matched by verified email).
GOOGLE_DEFAULT_ROLE read-only Role assigned to auto-provisioned Google users (admin, read-write, or read-only).
PORT 3000 Listen port.
NODE_ENV development Set to production to enable the Secure cookie flag.

Multi-instance cluster (UDP sync)

Queen servers are stateless, you can run multiple instances pointing at the same PostgreSQL. Two optional UDP layers make horizontal scaling actually fast:

three-instance cluster
# Server A
export QUEEN_UDP_PEERS="queen-b:6633,queen-c:6633"
export QUEEN_UDP_NOTIFY_PORT=6633
export QUEEN_SYNC_ENABLED=true
export QUEEN_SYNC_SECRET=$(openssl rand -hex 32)   # shared by all instances

# Server B
export QUEEN_UDP_PEERS="queen-a:6633,queen-c:6633"
export QUEEN_SYNC_SECRET=...same...

# Server C, same pattern
export QUEEN_UDP_PEERS="queen-a:6633,queen-b:6633"

Self-detection is automatic: each server excludes itself from the peer list. The cluster works fine without UDP, long-poll consumers just fall back to a slightly longer wake-up interval.

Kubernetes

We run Queen MQ on Kubernetes as a StatefulSet + headless Service. The chart we run in production lives at helm/; a flat manifest is at server/k8s-example.yaml. The headless service gives each pod a stable DNS name, which is what the UDP sync layer needs to find its peers, and what client-side affinity routing keys off.

values.yaml — production

The actual values file we ship in production: 3 replicas, peer DNS via the headless service, libqueen knobs raised for a 16+ core PostgreSQL.

helm/values-prod.yaml
replicas: 3

image: smartnessai/queen-mq
imageTag: 0.14.1

# Postgres database. Queen owns its schema and stored procedures.
db: queen
dbPoolSize: 10
sidecarPoolSize: 30

numWorkers: 1
logLevel: info

# Pop pacing (long-poll backoff).
popWaitInitialIntervalMs: 1000
popWaitBackoffThreshold: 1
popWaitBackoffMultiplier: 5.0
popWaitMaxIntervalMs: 10000

# Retention.
partitionCleanupDays: 7
retentionBatchSize: 1000
retentionInterval: 600000

# libqueen tuning (PG cluster >= 16 cores). See LIBQUEEN_TUNING.md.
queenPushMaxConcurrent: 24
queenAckMaxConcurrent: 16
queenPopMaxConcurrent: 16
queenVegasMaxLimit: 32
queenVegasBeta: 12
queenPushPreferredBatchSize: 50
queenPushMaxHoldMs: 20
queenPushMaxBatchSize: 500

# UDP peer sync (StatefulSet pod DNS via headless service).
udpPeers: queen-mq-0.queen-mq-headless.queen.svc.cluster.local,queen-mq-1.queen-mq-headless.queen.svc.cluster.local,queen-mq-2.queen-mq-headless.queen.svc.cluster.local
udpNotifyPort: 6633

# PVC per pod — holds the disk push-buffer for PG-down failover.
storageSize: 10Gi

resources:
  limits:    { memory: 1000Mi }
  requests:  { memory: 1000Mi }

StatefulSet template

One container per pod. Probes hit /health on 6632 (HTTP); the pod also listens on UDP {{ .Values.udpNotifyPort }} for peer wake-ups. A PVC mounted at /var/lib/queen/buffers holds the disk push-buffer if PostgreSQL goes away. Postgres credentials and JWT/encryption secrets are pulled via envFrom, never templated into the manifest.

helm/templates/statefulset.yaml (excerpt)
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: {{ .Release.Name }}
  namespace: {{ .Release.Namespace }}
  labels: { run: {{ .Release.Name }} }
spec:
  replicas: {{ .Values.replicas }}
  serviceName: {{ .Release.Name }}-headless
  selector:
    matchLabels: { run: {{ .Release.Name }} }
  template:
    metadata:
      labels: { run: {{ .Release.Name }} }
    spec:
      terminationGracePeriodSeconds: 40
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels: { run: {{ .Release.Name }} }
      containers:
        - name: queen
          image: {{ .Values.image }}:{{ .Values.imageTag }}
          imagePullPolicy: Always
          command: ['./bin/queen-server']
          ports:
            - { containerPort: 6632, protocol: TCP }
            - { containerPort: {{ .Values.udpNotifyPort }}, protocol: UDP }
          volumeMounts:
            - { mountPath: /var/lib/queen/buffers, name: buffers }
          resources: {{- toYaml .Values.resources | nindent 12 }}
          startupProbe:   { httpGet: { path: /health, port: 6632 }, periodSeconds: 5,  failureThreshold: 12 }
          livenessProbe:  { httpGet: { path: /health, port: 6632 }, periodSeconds: 10, failureThreshold: 3  }
          readinessProbe: { httpGet: { path: /health, port: 6632 }, periodSeconds: 5,  failureThreshold: 3  }
          envFrom:
            - secretRef: { name: queen-prod }            # JWT, encryption keys
            - secretRef: { name: postgres-queen-owner }  # PG creds
          env:
            - { name: PG_DB,                     value: "{{ .Values.db }}" }
            - { name: NUM_WORKERS,               value: "{{ .Values.numWorkers }}" }
            - { name: DB_POOL_SIZE,              value: "{{ .Values.dbPoolSize }}" }
            - { name: SIDECAR_POOL_SIZE,         value: "{{ .Values.sidecarPoolSize }}" }
            - { name: LOG_LEVEL,                 value: "{{ .Values.logLevel }}" }
            - { name: PARTITION_CLEANUP_DAYS,    value: "{{ .Values.partitionCleanupDays }}" }
            - { name: RETENTION_INTERVAL,        value: "{{ .Values.retentionInterval }}" }
            # libqueen adaptive engine
            - { name: QUEEN_PUSH_MAX_CONCURRENT, value: "{{ .Values.queenPushMaxConcurrent }}" }
            - { name: QUEEN_ACK_MAX_CONCURRENT,  value: "{{ .Values.queenAckMaxConcurrent }}"  }
            - { name: QUEEN_POP_MAX_CONCURRENT,  value: "{{ .Values.queenPopMaxConcurrent }}"  }
            - { name: QUEEN_VEGAS_MAX_LIMIT,     value: "{{ .Values.queenVegasMaxLimit }}"     }
            # UDP sync layer
            - { name: QUEEN_SYNC_ENABLED,        value: "true" }
            - { name: QUEEN_UDP_PEERS,           value: "{{ .Values.udpPeers }}" }
            - { name: QUEEN_UDP_NOTIFY_PORT,     value: "{{ .Values.udpNotifyPort }}" }
            - name: QUEEN_SYNC_SECRET
              valueFrom: { secretKeyRef: { name: queen-sync, key: secret } }
  volumeClaimTemplates:
    - metadata: { name: buffers }
      spec:
        accessModes: [ReadWriteOnce]
        resources: { requests: { storage: {{ .Values.storageSize }} } }
        storageClassName: standard-rwo

Service: ClusterIP + headless

Two services. The ClusterIP is what your apps hit; sessionAffinity: ClientIP keeps related calls (push then pop, ack chains) on the same pod, which plays nicely with the per-pod partition-id LRU. The headless service gives each pod the stable queen-mq-N.queen-mq-headless.<ns>.svc.cluster.local name the UDP peer list points at.

helm/templates/service.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}
  namespace: {{ .Release.Namespace }}
spec:
  selector: { run: {{ .Release.Name }} }
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP: { timeoutSeconds: 300 }
  ports:
    - { name: http, port: 6632, targetPort: 6632, protocol: TCP }

---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}-headless
  namespace: {{ .Release.Namespace }}
spec:
  clusterIP: None
  selector: { run: {{ .Release.Name }} }
  ports:
    - { name: http,       port: 6632,                       targetPort: 6632,                       protocol: TCP }
    - { name: udp-notify, port: {{ .Values.udpNotifyPort }}, targetPort: {{ .Values.udpNotifyPort }}, protocol: UDP }

We render and apply with vanilla helm template, no Tiller, no chart hub:

install
helm template queen-mq ./helm \
  -f ./helm/prod.yaml \
  --namespace queen | kubectl apply -n queen -f -

High-throughput tuning

Defaults are tuned for ~1 KB messages on a 4-core node. For a 32-core box pushing ~50k msg/s, raise:

throughput knobs
export NUM_WORKERS=20
export DB_POOL_SIZE=300
export SIDECAR_POOL_SIZE=100
export SIDECAR_MAX_ITEMS_PER_TX=2000
export RESPONSE_BATCH_SIZE=200
export RESPONSE_BATCH_MAX=1000

# v0.13+ adaptive engine, raise per-type concurrency ceilings if needed
export QUEEN_PUSH_MAX_CONCURRENT=24
export QUEEN_ACK_MAX_CONCURRENT=16
export QUEEN_POP_MAX_CONCURRENT=16

See server/README.md for the full tuning guide and the rationale behind these defaults.

Failover & durability