Skip to main content

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 its target) against tenant-owned rules and decides whether it also triggers a notification, on which latency lane, and to which channels.
  • actor and target do different jobs. actor is who performed the action — it keys the activity/audit timeline ("who did what, in order"). target is 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) or client-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

ConceptWhat it is
TenantsThe top-level isolation boundary. Everything is tenant-scoped.
Apps & credentialsThe S2S identity a backend service emits events as.
Subjects & identitiesThe stable subject_id a tenant's fragmented IdP claims resolve to.
Events & the envelopeThe one event shape everything is built from.
Policiesevent_type → notify?/lane/channels/recipients rules.
ChannelsOut-of-band delivery (Phase 1: in-app only).
Realtime & streamingLive delivery via WebSocket/SSE.
Adapters & ProfilesThe six pluggable infrastructure seams.
AuthenticationSSO-JWT (users) and service-JWT (apps).
Trust & render-tokensHow a client-only-looking event earns audit-grade trust.
Authz planesPublish, 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:

SeamJobReference implementation
StoreAppend/query the event timeline, dedup, TTLClickHouse
BusPublish/subscribe events between ingestion and deliveryIn-process (direct)
RealtimeLive fan-out to subscribed clientsCentrifugo
IdentityVerify a Bearer token, resolve claimsSSO-JWT (IdP-shape-agnostic)
ChannelOut-of-band delivery to one recipientIn-app (via realtime)
SecretsStore/rotate signing key materialAWS 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