Introduction
CNS (Central Notification System) is Amphoze's multi-tenant event & notification platform: a Go core, a TypeScript SDK, and a React/Vite admin portal. It gives a product a single place to record "what happened" (activity, audit) and decide "who should hear about it" (notifications) — instead of every app hand-rolling its own activity feed, audit log, and notification pipe.
CNS is built multi-tenant from day one. One deployment can serve many client
organizations (or many internal apps) with hard isolation between them — every store
query, channel, and policy decision is scoped to a tenant_id that is always derived
from the authenticated caller, never accepted from a request body.
:::info Status CNS is in Phase 1: manual-emit ingestion, the full trust/ordering/tenancy model, one implementation per adapter seam, and the two-authz-plane security model are built and tested. Phase-2 items (auto-instrumentation, additional adapters, self-serve multi-tenant control plane) are tracked in Advanced & Roadmap. :::
The mental model
Everything in CNS follows one flow:
emit() policy.Evaluate() fan-out
Event ──────────▶ Ingestion ──────────────────▶ Decision ────────▶ Realtime (live stream)
(SDK/curl) (trust + (notify? Channels (in-app, …)
ordering) which lane,
│ who to notify)
▼
Chronological store
(activity / audit backbone)
A few things fall out of this shape immediately:
- Persistence is universal. Every event lands in the chronological store, full stop. There is no such thing as a "notification-only" event — the store is the activity/audit backbone for the whole tenant.
- Notification is a per-event decision, not a different kind of event. A policy
engine evaluates each event's
event_type(and itstarget) against tenant-owned rules and decides whether it also triggers a notification, on which latency lane, and to which channels. actorandtargetdo different jobs.actoris who performed the action — it keys the activity/audit timeline ("who did what, in order").targetis who should be notified — a plural set, because one action can notify several people. An approval event's actor is the approver (timeline); its target is the payee (notification). The two can overlap (a viewer acknowledging their own notice is both actor and target) — delivery/read state dedupes that case automatically.- Trust is explicit, not assumed. Every event carries a
provenance:backend-verified(audit-grade — a real server anchored it) orclient-asserted(telemetry-grade — a bare frontend claim). See Trust & render-tokens.
Who it's for
CNS fits a product (or a family of internal apps) that needs:
- one employee/user activity timeline and audit log, instead of scattered per-app logging;
- in-product notifications (and, later, other channels) driven by declarative per-tenant policy rather than if/else scattered through application code;
- to serve more than one tenant from the same infrastructure, with tenant isolation enforced as an access rule rather than a naming convention.
If you only need a single app's internal audit log with no notification concept and no multi-tenancy, CNS is more machinery than you need. If you're building a platform that several tenants/apps will plug into, CNS's adapter model (below) is the reason to reach for it instead of a bespoke pipe per app.
Key concepts at a glance
| Concept | What it is |
|---|---|
| Tenants | The top-level isolation boundary. Everything is tenant-scoped. |
| Apps & credentials | The S2S identity a backend service emits events as. |
| Subjects & identities | The stable subject_id a tenant's fragmented IdP claims resolve to. |
| Events & the envelope | The one event shape everything is built from. |
| Policies | event_type → notify?/lane/channels/recipients rules. |
| Channels | Out-of-band delivery (Phase 1: in-app only). |
| Realtime & streaming | Live delivery via WebSocket/SSE. |
| Adapters & Profiles | The six pluggable infrastructure seams. |
| Authentication | SSO-JWT (users) and service-JWT (apps). |
| Trust & render-tokens | How a client-only-looking event earns audit-grade trust. |
| Authz planes | Publish, subscribe, and admin — three separate decisions. |
| Event Registry | (stub) a governed, versioned per-tenant event catalog. |
Architecture, in words
A CNS deployment is one core plus six adapters, wired together by a Profile.
The core (internal/core/* in the Go module) is never pluggable — it is the
product. It defines: the event envelope; the trust/provenance model; per-session
ordering (seq); the tenant-derivation rule; the identity-map (claims → stable
subject_id); the notification-policy engine; and the two data-plane authorization
planes (publish, subscribe) plus a third admin-authorization plane. This behavior is
identical for every tenant and every deployment.
Around that core sit six adapter seams, each a Go interface with (at least) one
real implementation and a noop fallback so a server always boots even
unconfigured:
| Seam | Job | Reference implementation |
|---|---|---|
| Store | Append/query the event timeline, dedup, TTL | ClickHouse |
| Bus | Publish/subscribe events between ingestion and delivery | In-process (direct) |
| Realtime | Live fan-out to subscribed clients | Centrifugo |
| Identity | Verify a Bearer token, resolve claims | SSO-JWT (IdP-shape-agnostic) |
| Channel | Out-of-band delivery to one recipient | In-app (via realtime) |
| Secrets | Store/rotate signing key material | AWS SSM (ed25519 local-sign) |
A Profile is one deployment's choice of implementation (plus settings) for each
seam. Phase 1 runs one global Profile per running cns-server process; going
multi-tenant means keying the same struct per tenant_id (see
Advanced & Roadmap) — not a rewrite. Full detail:
Adapters & Profiles.
Where to go next
- New to CNS entirely? Go to Quickstart — first event in a few minutes, no cloud credentials required.
- Building against an existing instance? Jump to Emitting events or the API Reference.
- Operating your own instance? See Self-Hosting & Operations.