Skip to main content

Adapters & Profiles

CNS's core (event envelope, trust model, ordering, identity-mapping policy, notification policy, tenant isolation) is never pluggable — it's identical for every deployment. Everything that touches a specific piece of infrastructure sits behind one of six adapter seams, each a Go interface with a noop stub so the server always boots even when a seam is left unconfigured.

SeamContract (essence)Reference implAlternatives named in the design
StoreAppend · Query(subject, filter, page) · dedup · SetTTLClickHouseTimescale, BigQuery, Snowflake, PG
BusPublish · Subscribe(topic, group, handler) · ack/nack via handler error · DeadLetterIn-process (direct)RabbitMQ, Kafka, NATS, Redis Streams, SQS
RealtimePublish(channel, payload) · AuthorizeSubscribe · PresenceCentrifugoAbly, Pusher, Soketi, SSE, Supabase Realtime
IdentityVerify(token) → claims · Resolve → subject_id · Authorize(subject, scope)SSO-JWT (IdP-shape-agnostic)Supabase/Firebase Auth, Auth0/OIDC, SAML
ChannelSend(recipient, rendered)In-appEmail, WhatsApp, Teams, Slack, SMS, push
SecretsGet/Put/Rotate, optional Sign/VerifyAWS SSM (ed25519 local-sign)envctl, Vault/OpenBao Transit

Adapters depend on core types (event.Event etc.); core never imports an adapter package. SDKs and apps never see adapters at all — they only ever see the stable HTTP/SDK contract. Which implementation backs a seam is purely a deployment concern.

The Profile

One deployment is one config.Profile: a named implementation plus arbitrary settings, per seam.

type AdapterConfig struct {
Impl string
Settings map[string]string
}

type Profile struct {
Store AdapterConfig
Bus AdapterConfig
Realtime AdapterConfig
Identity AdapterConfig
Channels []AdapterConfig // a tenant may wire more than one channel at once
Secrets AdapterConfig
}

Phase 1 loads exactly one global Profile per running cns-server process from environment variables (internal/config.Load — no file-based config yet). Going multi-tenant later means keying the same struct per tenant_id, resolved from the authenticated token — not a rewrite of anything above this layer. See Advanced & Roadmap.

Two seams don't have an "unconfigured" state in Phase 1: Bus is always direct (trust and ordering don't depend on the bus at manual-emit volume — a real broker is explicitly deferred to Phase-2 auto-emit volume), and Secrets defaults to ssm (overridable to an in-process memory keystore for dev/test, or noop). Every other seam falls back to noop — which means it compiles and boots, but every call returns "not implemented" — until its specific environment variables are set.

For the exhaustive environment-variable reference and how to actually stand up ClickHouse/Centrifugo/SSM/identity for a real deployment, see Self-Hosting & Operations.