Realtime database
Realtime database streams insert, update, and delete changes from selected
schema tables to WebSocket clients. Layeron creates the outbox table, Durable
Object hub, worker bindings, and runtime metadata from the db({ realtime })
declaration. The Database runtime records outbox rows after successful writes
that go through the Layeron Database API.
Configure Tables
Section titled “Configure Tables”Realtime database uses the schema primary key by default. Add key when the
table uses a different identity column for event payloads.
import { backend } from "@layeron/core"import { db, table, text } from "@layeron/modules"
const app = backend()
const database = db({ name: "main", schema: { posts: table({ id: text().primaryKey(), tenantId: text().notNull().index(), title: text().notNull(), }), }, realtime: { tables: { posts: { events: ["insert", "update", "delete"], filters: ["tenantId"], requiredFilters: ["tenantId"], }, }, },})
app.use(database)Use true for the default table settings:
db({ name: "main", schema: { posts: table({ id: text().primaryKey(), title: text().notNull(), }), }, realtime: { tables: { posts: true, }, },})Realtime tables are private by default. Private tables require a short-lived
subscription grant before a client can open the WebSocket. Use
access: "public" only for tables whose row changes are safe for every client
to receive.
Connect
Section titled “Connect”Issue a grant from a normal route after the route has checked the current user or tenant:
app.post("/realtime/posts/token", async (request) => { const auth = await requireAuth(request)
return await database.realtime.authorize({ table: "posts", filters: { tenantId: auth.tenantId, }, events: ["insert", "update"], ttlSeconds: 300, actor: { kind: "user", id: auth.userId, tenantId: auth.tenantId, }, })})The result includes a WebSocket URL, the JWT grant, and the protocols used by the browser WebSocket constructor:
{ "url": "wss://api.example.com/__layeron/realtime", "protocols": [ "layeron.realtime.v1", "layeron.jwt.<token>" ], "expiresAt": "2026-05-31T00:05:00.000Z"}Connect with the returned URL and protocols:
const grant = await fetch("/realtime/posts/token", { method: "POST",}).then((response) => response.json())
const socket = new WebSocket(grant.url, grant.protocols)
socket.addEventListener("message", (event) => { const change = JSON.parse(event.data) console.log(change)})The grant binds the database namespace, database name, table, filters, events, actor metadata, and expiration time. A client cannot change the table or filters during the WebSocket handshake.
Public Tables
Section titled “Public Tables”Public tables can connect with query parameters because they do not require a grant:
const socket = new WebSocket( "wss://api.example.com/__layeron/realtime" + "?namespace=default" + "&database=main" + "&table=announcements",)Use this form only with access: "public":
db({ name: "main", schema: { announcements: table({ id: text().primaryKey(), title: text().notNull(), }), }, realtime: { tables: { announcements: { access: "public", }, }, },})Public Query Parameters
Section titled “Public Query Parameters”| Parameter | Required | Description |
|---|---|---|
namespace | Optional | Database namespace. Defaults to default. |
database | Required | Database module name. |
table | Required | Subscribed table name. |
filters | Optional | JSON object of equality filters using columns declared in realtime.tables[table].filters. Required filters still apply. |
Event Payload
Section titled “Event Payload”Each message has this shape:
{ "type": "database.change", "seq": 1, "namespace": "default", "database": "main", "table": "posts", "event": "insert", "key": { "id": "post_1" }, "old": null, "new": { "id": "post_1", "tenantId": "tenant_1", "title": "Hello" }, "shard": "DB_SHARD_0000", "requestId": "req_123", "createdAt": "2026-05-25T00:00:00.000Z"}old contains the row before an update or delete. new contains the row
after an insert or update.
Generated Database Objects
Section titled “Generated Database Objects”Layeron adds a migration named layeron_002_realtime after the schema
migration. It creates:
layeron_db_realtime_outbox- an index for pending outbox rows
The runtime publishes pending outbox rows after successful database writes, then
marks each row with published_at.