Queen MQ 1.0.0 is one broker binary plus a set of client SDKs released at the
same version: JavaScript, Python, Go, PHP (Laravel), C++, and a Go command-line
client. The broker reports its version on GET /health and in its boot log; the
value is compiled in from a single file, so the binary, the container tag and the
health document can never disagree.
The HTTP API is the compatibility surface
The contract is the HTTP API: the paths under /api/v1 and /streams/v1, their
methods, their JSON field names, and the status-code behaviour described in
Status codes and error bodies. An SDK is a convenience over
that surface, not a separate contract: anything an SDK can do, a plain HTTP call
can do.
Everything else is an implementation detail and changes without notice:
- the SQL schema, the table shapes, and all stored-procedure names and signatures;
- the routes under
/internal/, which exist for broker-to-broker traffic; - the multi-broker mesh frame format;
- the embedded dashboard bundle;
- the boot log’s field names.
Two consequences follow from taking the JSON field names (rather than their
meaning) as the contract. First, fields are added, not renamed: a client must
ignore fields it does not know. Second, some fields are opaque by contract.
seq and startOff on a pop response kept their names across the storage-engine
rewrite while their meaning changed completely: seq now carries a segment’s
base offset and startOff a frame index within it. A client that treats them as
tokens to hand back is correct; a client that does arithmetic on them is not.
The schema is applied at every boot
The broker owns its own schema. It embeds schema.sql and 23 stored-procedure
files (001–023) into the binary at compile time and applies all of them on
every start, in lexical file order, before it accepts a request.
Three properties make that safe to do on every boot:
- Idempotent statements.
CREATE TABLE IF NOT EXISTS,CREATE OR REPLACE FUNCTION,ADD COLUMN IF NOT EXISTS. Re-applying is a no-op when nothing changed. - Serialised across instances. The apply holds a session advisory lock, so a rolling restart of several brokers cannot run two DDL passes concurrently.
- Fail-fast. Any DDL error aborts the boot with the failing file named in the message. A broker never serves against a schema it could not bring to the expected shape.
The apply only creates. Every table and every function is defined exactly
once, by exactly one file, in its final shape: nothing is dropped, migrated or
folded at boot, and schema.sql carries no upgrade ALTERs: the
CREATE TABLEs declare the final columns directly. Order in the file list is
load-bearing only where a function body has to resolve a table at creation time.
The table files come first, and the partition-counter trigger attachment comes
after both the table it attaches to and the functions it binds.
The deployment model is always-virgin
A deployment starts from an empty database (or an empty schema): the boot apply is the schema, not a migration engine. There is no migration step, no version table, and nothing to run by hand, because there is nothing to migrate. Restarting the same version re-applies the same definitions as a no-op, and a rolling restart of identical binaries is safe.
What the apply does not do is bring an existing database forward. A
CREATE TABLE IF NOT EXISTS against a table that already exists in an older
shape leaves the older shape in place, so pointing a newer build at a database
created by a different build is unsupported: stand up a fresh database and cut
over. The same applies to a downgrade: an older broker would re-apply its
own older definitions over a schema it does not understand. Roll back by
restoring a dump into a fresh database, not by restarting an older image against
the current one.
One rolling-restart detail survives from the file mechanics: a definition whose
return type changed between builds cannot be a CREATE OR REPLACE, so a few are
expressed as a drop followed by a create (the pop family is the notable case),
and the advisory lock does not stop an older instance from serving while a newer
one applies. Roll instances one at a time, and do not point two different broker
versions at one database as a steady state.
Running as a low-privilege user
QUEEN_APPLY_SCHEMA=0 skips the apply entirely. A privileged role then owns the
DDL and pre-applies it, and the broker’s own database user needs no CREATE
rights. The trade is that the ordering guarantee becomes yours: a broker that
skips the apply and finds an older schema fails on the first request that needs a
missing object, not at boot.
With the apply enabled, the database user needs CREATE SCHEMA and nothing else.
The schema requires no extensions: gen_random_uuid() is used as a core
built-in, not the pgcrypto function. It does require PostgreSQL 15 or
newer, because one unique index is declared NULLS NOT DISTINCT. The test
matrix runs against PostgreSQL 16.
Coming from the retired 0.16 broker
The 0.16 broker was a different implementation: C++, and a different storage engine. Replacing it with 1.0.0 is not a data migration, and there is no tool that makes it one.
There is no importer. The migrate subcommand still exists so the CLI entry
compiles, and all it does is log
migration is not supported on the log engine and exit 1. It never opens a
database connection. There are no migration HTTP endpoints either.
The boot apply does not touch old objects. It only creates: nothing is dropped, altered or folded at boot, whatever the database already holds. That is not a compatibility promise, it is the always-virgin model: 1.0.0 expects an empty database, and against one carrying an earlier broker’s tables it would simply create its own objects alongside data it will never read or reconcile.
The practical upgrade is therefore a cutover, not an in-place migration: stand up 1.0.0 against a clean database, move producers over, let the old consumers drain the old broker, and keep a dump of the old database as the record.
Backup and restore are plain pg_dump and pg_restore. The broker offers no
backup endpoint.
Mixed versions in a mesh
A multi-broker deployment can be rolled one instance at a time. Where a frame from an older peer omits information a newer instance expects (the tenant on a wake or invalidation frame), the newer instance fans the effect out rather than guessing: it wakes every tenant holding that queue name and drops every tenant’s cache entry for it. Over-waking costs one empty probe; over-invalidating costs one lazy re-fetch. Neither is a correctness problem, so a mixed-version mesh is safe.