Truly simple queue service.
PlainQ is a small, self-contained queue server. One binary gives you a gRPC API, a CLI, a terminal UI, and a built-in admin dashboard — backed by embedded SQLite or PostgreSQL when you need to scale out.
# Build, then start the server (SQLite by default)$ make build$ ./plainq serve --auth.jwt.secret="$(openssl rand -hex 32)"# In another shell — create, send, receive$ QID=$(./plainq create my-queue)$ ./plainq send -message='hello, plainq' "$QID"$ ./plainq receive -ack "$QID"Everything in the box
PlainQ is intentionally boring on the inside, so the surface stays small and the operations stay quiet.
One binary, no broker fleet
Run ./plainq serve and you have a queue. No Zookeeper, no separate broker cluster, no operational sprawl.
gRPC API + CLI + TUI
The same surface, scripted or interactive. Eight typed RPCs, a full CLI, and a rich Bubble Tea terminal dashboard.
Houston admin UI
An Astro + React dashboard for queues, accounts, RBAC, and metrics — served straight from the same binary.
Pick your storage
Embedded SQLite by default — Litestream-friendly for cheap replication. Switch to PostgreSQL for a shared backend.
Auth that's actually built in
JWT sessions, refresh tokens, RBAC, and OAuth/OIDC hooks (Kinde, Auth0, Okta, WorkOS) ship with the server.
Built for humans and agents
-json everywhere and schema introspection make the CLI a first-class tool for scripts and AI agents alike.
One surface, scripted or wired
Drive queues from the terminal, or generate a client SDK straight from the published protobuf schema. Same semantics either way.
CLI
# Flags go BEFORE the positional queue id$ plainq create \ -visibility-timeout=30 \ -max-receive-attempts=5 \ -drop-policy=dead-letter \ my-queue$ plainq send -message='{"job":"resize"}' "$QID"$ plainq receive -batch=10 -ack "$QID"# Machine-readable output for scripts and agents$ plainq list -json | jq '.queues[].queue_name'gRPC
// The wire API is published to the Buf Schema Registry:// buf.build/plainq/schemaservice PlainQ { rpc ListQueues(ListQueuesRequest) returns (...); rpc DescribeQueue(DescribeRequest) returns (...); rpc CreateQueue(CreateQueueRequest) returns (...); rpc PurgeQueue(PurgeQueueRequest) returns (...); rpc DeleteQueue(DeleteQueueRequest) returns (...); rpc Send(SendRequest) returns (...); rpc Receive(ReceiveRequest) returns (...); rpc Delete(DeleteRequest) returns (...);}Ship it anywhere
From a laptop to a cluster, the same binary follows you. Pick the smallest thing that works.
Single binary
make build produces ./plainq. Copy it to a box, run serve, and you are done — SQLite lives in one file.
Docker
An optimized multi-stage image: Bun builds Houston, Go builds a static binary, shipped on distroless:nonroot.
Kubernetes (Helm)
A StatefulSet + PVC for SQLite, or a Deployment + HPA when you point it at PostgreSQL. JWT secret from a Secret.
# Distroless, static binary, nonroot$ docker run --rm -p 8080:8080 -p 8081:8081 \ -v plainq-data:/data plainq:dev serve \ -storage.path=/data/plainq.db \ -auth.jwt.secret="$(openssl rand -hex 32)"# Production-grade chart with StatefulSet + PVC$ helm install plainq deploy/helm/plainq \ --set auth.jwtSecret="$(openssl rand -hex 32)"A queue, in under a minute.
From a fresh clone to a message round-trip. No services to provision, no YAML to write first.
$ make build && ./plainq serve \ --auth.jwt.secret="$(openssl rand -hex 32)"