Skip to content

Message broker events

Requires a license with the messagebroker feature. See pricing.

Every time an entry is created, updated or deleted, LyEve publishes one message to your broker: NATS JetStream, Apache Kafka or RabbitMQ. Your services subscribe to the topics they care about and react, without polling the API and without LyEve calling each of them. A broker outage never slows or fails a content write.

StepWhat happens
RecordThe event is recorded right after the content write is saved, with an event_id.
PublishWithin about a second it is published to the broker. Only one replica publishes at a time.
RetryA refused publish is retried after 1 second, then 2, 4 and so on, up to 5 minutes between attempts.
Give upAfter 300 attempts, about a day of broker outage, the event is marked dead.

Delivery is at least once from the moment the write is saved:

  • Duplicates are possible. A publish whose acknowledgment never arrived is sent again with the same event_id. Deduplicate on it.
  • Order is not guaranteed. The oldest events go first, but a retried event can arrive after a newer one.
  • A crash can lose one event, between saving the write and recording it.
  • Sent events are kept 24 hours and dead events 7 days. After that, retention is your broker's.

Publishes are acknowledged. NATS waits for JetStream and reconnects without a limit. RabbitMQ waits up to 10 seconds for the broker's confirmation, and a reject or a timeout counts as a failed attempt. Kafka uses an idempotent producer that waits for every in-sync replica.

This uses NATS. You need an admin token for a super admin in TOKEN (the quickstart shows how to get one), a NATS server with JetStream, and the nats command line tool.

  1. Check what the instance publishes to now:

    Terminal window
    curl http://localhost:3001/api/admin/messagebroker/status \
    -H "Authorization: Bearer $TOKEN"

    With no broker configured, backend is empty and connected still reads true. Events are then discarded, although published still counts them.

  2. Point the instance at NATS and restart it:

    Terminal window
    EVENT_BUS=nats
    NATS_URL=nats://nats.internal:4222
  3. Subscribe to everything in your tenant (here default):

    Terminal window
    nats sub "lyeve.tenant.default.>"
  4. Create an entry, such as a post:

    Terminal window
    curl -X POST http://localhost:3002/api/v1/content/post \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"data": {"title": "Broker check", "slug": "broker-check"}}'

    The subscriber prints a message on lyeve.tenant.default.post.after_create with the entry's fields in data.

  5. Read the status again. backend is nats, connected is true, and published has grown. A super admin sees the same under Settings > Message broker in the admin console.

Each message is the content event as JSON:

FieldValue
event_idA UUID for this event. A retry sends the same value.
typeafter_create, after_update or after_delete
schemaThe content type
dataThe entry's current fields
old_dataThe previous values, when the event carries them
record_idThe entry's id
tenant_idThe owning tenant
instance_idThe instance that raised the event
timestampRFC 3339 time of the event
sourceWhere the event came from, when set

The event_id also travels as a transport header, so a consumer can deduplicate without decoding the body: Nats-Msg-Id and an event_id header on NATS (JetStream also deduplicates on Nats-Msg-Id within the stream's duplicate window), an event_id record header on Kafka, and the AMQP message_id property and an event_id header on RabbitMQ.

A payload over 1 MiB is refused before it reaches the broker.

Topics are named by tenant, content type and event:

lyeve.tenant.{tenant_id}.{schema}.{event_type}

An event that carries no tenant goes to lyeve.{schema}.{event_type}.

BackendWhere an event goesSubscribe to one tenant
NATSSubject lyeve.tenant.{tenant_id}.{schema}.{event_type} on the JetStream streamlyeve.tenant.acme.>
KafkaTopic {KAFKA_TOPIC_PREFIX}lyeve.tenant.{tenant_id}.{schema}.{event_type}. With the default prefix that is lyeve.lyeve.tenant.acme.product.after_create.the prefixed topics
RabbitMQDurable topic exchange lyeve.events, routing key lyeve.tenant.{tenant_id}.{schema}.{event_type}bind a queue with lyeve.tenant.acme.#

On RabbitMQ, a message no queue accepts goes to the lyeve.events.dead exchange and the lyeve.events.dlq queue, which keeps messages for 7 days and holds at most 10,000.

GET /api/admin/messagebroker/status reports the backend and how publishing has gone since the process started. It needs the super_admin role, and the counters cover the whole instance, not one tenant.

A status answer and its fields
{
"backend": "nats",
"connected": true,
"degraded": false,
"target": "nats://nats.internal:4222",
"stream": "LYEVE_EVENTS",
"tls": true,
"published": 1284,
"failed": 0,
"dropped": 0,
"last_publish_at": "2026-10-01T09:12:44Z",
"outbox": {
"enabled": true,
"available": true,
"pending": 0,
"dead": 0,
"oldest_pending_age_seconds": 0
}
}
FieldMeaning
backendnats, kafka, rabbitmq, or empty or noop when nothing is configured
connectedWhether a publisher is set up. It reads true with no broker too.
degradedThe license no longer carries messagebroker, so events are dropped on purpose
targetWhere events go, with credentials removed
stream, topic_prefixThe NATS stream or the Kafka topic prefix
tlsWhether TLS is on
published, failed, droppedEvents handed to the backend, events it refused, and events with nowhere to go
last_publish_at, last_errorThe last publish, and the last refusal with its topic, error and time
outbox.enabled, outbox.availableWhether events are recorded before they are published, and whether the counts below could be read
outbox.pending, outbox.deadEvents waiting to be sent, and events that ran out of attempts
outbox.oldest_pending_at, outbox.oldest_pending_age_secondsHow long the oldest waiting event has waited

A caller without super_admin gets 403.

  • pending grows while published stands still. The broker is down or refusing events. Read last_error.
  • published grows but no consumer sees anything. backend is empty or noop, so events are discarded. Set EVENT_BUS.
  • dropped grows. The license lapsed (degraded is true), or an event could not be recorded for sending.
  • The feature does not start after setting EVENT_BUS. The value is not one of the four, the backend could not be reached, or a setting is missing, such as half of NATS_USER and NATS_PASSWORD or SASL credentials for the chosen mechanism. The startup log names it, and the rest of the instance runs.
  • A broker on a private certificate fails to connect. Set the backend's *_TLS_CA_FILE to the CA that signed it.

Two flow pieces use the same connection, under the flow's tenant (lyeve.tenant.{tenant}.):

  • The Publish a message node (messagebroker.publish) sends one message with a subject, a payload, optional headers and an optional partition key. The payload is capped at 1 MiB. With no broker configured the run fails.
  • The Message received trigger (messagebroker.message) starts a flow when a message arrives on a subject pattern such as orders.>. Without a group, every replica runs every message. With a group, the replicas share the messages and each one runs once.

See Flows.

Each setting is read from the environment, the configuration file or the configuration page of the admin console. A broker change saved in the console reconnects without a restart, and a failed reconnect keeps the old connection. Only the settings for the selected backend apply.

Every setting
VariableWhat it doesDefault
EVENT_BUSnoop, nats, kafka or rabbitmq. Empty or noop discards events.noop
NATS_URLNATS server URLnats://127.0.0.1:4222
NATS_STREAMJetStream stream, created with subjects lyeve.> if it does not existLYEVE_EVENTS
NATS_TOKENStatic auth tokennone
NATS_USER, NATS_PASSWORDUser and password. Set both or neither.none
NATS_NKEY_SEED_FILEPath to an NKey seed filenone
NATS_CREDS_FILEPath to a JWT credentials filenone
KAFKA_BROKERSComma-separated broker listlocalhost:9092
KAFKA_TOPIC_PREFIXPrefix added in front of every topic namelyeve.
KAFKA_REQUIRED_ACKSLeave it at all. none and leader stop the feature from starting, because the idempotent producer needs all.all
KAFKA_SASL_MECHANISMplain, scram-sha-256 or scram-sha-512. Empty means no SASL.none
KAFKA_SASL_USER, KAFKA_SASL_PASSWORDSASL credentials, required when a mechanism is setnone
RABBITMQ_URLAMQP URL. A URL with the default guest user is refused.required for rabbitmq
NATS_TLS, KAFKA_TLS, RABBITMQ_TLSTLS for that backend, at least TLS 1.2. false turns it off, and a value that is not a boolean keeps it on.true
NATS_TLS_CA_FILE, KAFKA_TLS_CA_FILE, RABBITMQ_TLS_CA_FILEPEM file with the CA that signed the broker's certificate. When set, only that CA is trusted.system roots

When more than one NATS credential is set, the first of these wins: the credentials file, the NKey seed file, the token, the user and password, then credentials in the URL.

Certificate verification is never skipped. A CA file that cannot be read is logged and ignored, and the system roots verify the broker instead, so a broker on a private certificate then fails to connect. If you set LYEVE_PLUGINS to choose which features start, include messagebroker. See Licensing and tiers.

The broker is for your systems. LyEve replicas stay in step with each other whether or not one is configured. See Scaling.

  • Webhooks: signed HTTP calls for the same changes.
  • Event replay: publish a past window again.
  • Flows: publish or receive broker messages in a flow.
  • Tenants: what the tenant segment of a topic means.