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:latest

Use 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.0

What 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.

migration 1 — init
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.0

Schema-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.

Related