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.
| Seam | Contract (essence) | Reference impl | Alternatives named in the design |
|---|---|---|---|
| Store | Append · Query(subject, filter, page) · dedup · SetTTL | ClickHouse | Timescale, BigQuery, Snowflake, PG |
| Bus | Publish · Subscribe(topic, group, handler) · ack/nack via handler error · DeadLetter | In-process (direct) | RabbitMQ, Kafka, NATS, Redis Streams, SQS |
| Realtime | Publish(channel, payload) · AuthorizeSubscribe · Presence | Centrifugo | Ably, Pusher, Soketi, SSE, Supabase Realtime |
| Identity | Verify(token) → claims · Resolve → subject_id · Authorize(subject, scope) | SSO-JWT (IdP-shape-agnostic) | Supabase/Firebase Auth, Auth0/OIDC, SAML |
| Channel | Send(recipient, rendered) | In-app | Email, WhatsApp, Teams, Slack, SMS, push |
| Secrets | Get/Put/Rotate, optional Sign/Verify | AWS 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.