Last updated 2026-05-28
Store Setup
Pick the durable store driver that fits the deployment. Memory is the test/dev reference; Postgres is the typical production shape; EntDB is the right call if you already run tenant-shard-db.
When you'd care
You are sizing storage for a new environment, deciding which managed Postgres SKU to provision, or wiring notify into an existing EntDB cluster.
memory (default)
No setup. NOTIFY_STORE_DRIVER=memory is the default;
everything lives in process memory and disappears on restart.
docker run --rm \ -p 8080:8080 -p 8081:8081 -p 9090:9090 \ -e NOTIFY_AUTH_DEV_MODE=true \ ghcr.io/elloloop/notify:latestUse this for tests, demos, and CI fixtures. It is the conformance reference — every other driver is verified against memory's behaviour.
postgres
Use a managed Postgres 14+ (we test against
postgres:16.13-alpine3.23). Auto-migration is on by
default and applies the schema under
pg_advisory_lock so concurrent boots serialise.
Provisioning a database
# Anything that can run CREATE DATABASE / CREATE USER.CREATE USER notify WITH PASSWORD 'notify';CREATE DATABASE notify OWNER notify;GRANT ALL PRIVILEGES ON DATABASE notify TO notify;Wiring it in
docker run --rm \ -p 8080:8080 -p 8081:8081 -p 9090:9090 \ -e NOTIFY_STORE_DRIVER=postgres \ -e NOTIFY_POSTGRES_DSN='postgres://notify:notify@db.internal:5432/notify?sslmode=require' \ -e NOTIFY_POSTGRES_AUTOMIGRATE=true \ -e NOTIFY_AUTH_JWT_SECRET=$(openssl rand -hex 32) \ -e NOTIFY_INTERNAL_TOKEN=$(openssl rand -hex 32) \ ghcr.io/elloloop/notify:0.1.0What auto-migrate does
On first boot it creates notify_notifications,
notify_devices and the
notify_schema_migrations bookkeeping table. Subsequent
boots see the ledger and skip already-applied versions.
CREATE TABLE notify_notifications ( id uuid PRIMARY KEY, tenant_id text NOT NULL, user_id text NOT NULL, notification_id text NOT NULL, subject_ref text NOT NULL DEFAULT '', subject_type text NOT NULL DEFAULT '', title text NOT NULL DEFAULT '', body text NOT NULL DEFAULT '', channel text NOT NULL DEFAULT '', status text NOT NULL, created_at_ms bigint NOT NULL, delivered_at_ms bigint NOT NULL DEFAULT 0, ack_at_ms bigint NOT NULL DEFAULT 0, read_at_ms bigint NOT NULL DEFAULT 0, UNIQUE (tenant_id, user_id, notification_id));
CREATE INDEX notify_notifications_user_created ON notify_notifications (tenant_id, user_id, created_at_ms DESC, id DESC);
CREATE INDEX notify_notifications_user_unread ON notify_notifications (tenant_id, user_id) WHERE status <> 'read';
CREATE TABLE notify_devices ( id uuid PRIMARY KEY, tenant_id text NOT NULL, user_id text NOT NULL, device_type text NOT NULL, token text NOT NULL DEFAULT '', created_at_ms bigint NOT NULL DEFAULT 0, last_active_ms bigint NOT NULL DEFAULT 0, UNIQUE (tenant_id, user_id, device_type));
If you want to manage migrations out of band (a CI step, golang-migrate,
etc.) set NOTIFY_POSTGRES_AUTOMIGRATE=false. The schema
is two tables plus three indexes — easy to mirror verbatim.
Sizing
Reasonable defaults: MaxConns = 25,
MaxConnLifetime = 1h, MaxConnIdleTime = 30m —
cooperative with pgbouncer / Azure idle reapers. notify is read-heavy
on the inbox surface and append-heavy on Notify; the
notify_notifications_user_unread partial index keeps
unread counts fast even with millions of read rows.
Conformance
The Postgres driver is verified at 24/24 against the shared suite. See Postgres driver or the CONFORMANCE.md for the per-subtest table and the design rationale.
entdb (tenant-shard-db)
Use this when EntDB is already a primitive in your stack
(e.g. you run identity against it). The notify driver requires the
v2 schema-aware mode (ADR-031) — v1.32.1 cannot enforce the
composite-uniqueness constraint that idempotent
CreateNotification needs.
Boot a local EntDB
docker run -d --rm --name notify-entdb -p 50051:50051 \ ghcr.io/elloloop/tenant-shard-db:2.0.5 \ -addr=:50051 -data-dir=/tmp/entdb -wal-backend=memory -data-dir is required on v2 (it was implicit in v1.32.1
and earlier).
Wiring it in
docker run --rm \ -p 8080:8080 -p 8081:8081 -p 9090:9090 \ -e NOTIFY_STORE_DRIVER=entdb \ -e NOTIFY_ENTDB_ADDRESS=entdb.internal:50051 \ -e NOTIFY_ENTDB_TENANT_ID=notify-prod \ -e NOTIFY_AUTH_JWT_SECRET=$(openssl rand -hex 32) \ -e NOTIFY_INTERNAL_TOKEN=$(openssl rand -hex 32) \ ghcr.io/elloloop/notify:0.1.0Schema-aware mode
notify's cmd/notifyd registers the schema on the SDK
client at boot:
client, err := sdk.NewClient( cfg.Store.EntDBAddress, sdk.WithSchema(entdb.SchemaMessages()...),) entdb.SchemaMessages() returns the two proto descriptors
(UserNotification, DeviceRegistration) declared
in proto/entdb_notify/notify.proto. The v2 server
materializes the schema from those descriptors on the first
ExecuteAtomic and enforces unique: true on
the composite_key field — which closes the
concurrent-create race the conformance suite probes.
Sharding
One notify container talks to one EntDB tenant shard
(NOTIFY_ENTDB_TENANT_ID). All notify-application tenants
live under that one storage shard, differentiated by the
tenant_id row field. If a large customer needs its own
storage shard, run a second notify container pointed at a different
NOTIFY_ENTDB_TENANT_ID.
Conformance
EntDB driver passes 24/24 on v2.0.5; the two formerly-red canaries (composite uniqueness under concurrent create + device upsert) flipped to green when the schema-aware enforcement landed in v2.0.1. See EntDB driver or store/entdb/CONFORMANCE.md.