Infrastructure setup
Practical notes for standing up each real (non-noop) adapter implementation. See
Environment variables for the exact variables each one
reads.
ClickHouse (store)
Schema migrations live in schemas/clickhouse/0001_init_events.{up,down}.sql. Apply
with the ClickHouse client:
clickhouse-client --multiquery < schemas/clickhouse/0001_init_events.up.sql
What that migration sets up, verified from the file directly:
- A
cns.eventstable —ENGINE = ReplacingMergeTree(ingest_time),ORDER BY (tenant_id, subject_id, session_id, seq, event_id),PARTITION BY (tenant_id, toYYYYMM(ingest_time)).subject_idin the physical order key is the actor (the activity-timeline key — see Events & the envelope); query-time ordering for a feed read is a separate(ingest_time, session_id, seq)sort the store adapter issues, not this physical key. - Per-tenant retention as a dictionary-backed TTL: a
cns.tenant_retentiontable (upserted bySetTTL) feeds acns.tenant_retention_dictdictionary, which the events table'sTTLexpression calls viadictGet— this is what lets one tenant's retention change without anALTER TABLEper tenant. A tenant with no explicitSetTTLcall falls back to 400 days. - Dedup is belt-and-braces: the store adapter's own short-TTL in-memory seen-set
rejects an obviously-duplicate
Appendbefore it ever reaches ClickHouse; theReplacingMergeTree(ingest_time)engine is the backstop for whatever gets through anyway (a verbatim re-Appendof the sameevent_idcollapses to one row once merged — queries readFINALso this doesn't depend on background merge timing).
:::caution Drop order matters on teardown
The down-migration must drop events before the dictionary/source table — the
events table's TTL expression depends on the dictionary via dictGet, and at least
one real ClickHouse version (24.8.14, tested during development) aborts a
naively-ordered drop with Code: 630 (HAVE_DEPENDENT_OBJECTS). Use the shipped
0001_init_events.down.sql rather than hand-rolling teardown SQL.
:::
Point cns-server at it with CNS_CLICKHOUSE_ENDPOINT (+ _DATABASE/_TABLE/
_USERNAME/_PASSWORD).
Centrifugo (realtime)
cns-server's Centrifugo adapter is a server-to-Centrifugo publish client (built
on gocent, Centrifugo's HTTP server API) — configure CNS_CENTRIFUGO_ADDR and
CNS_CENTRIFUGO_API_KEY and CNS can publish to Centrifugo channels named
cns:<tenant_id>:subject:<subject_id> / cns:<tenant_id>:group:<group_id>.
:::caution What this does not set up
Configuring these two variables makes GET /v1/stream pass its second
authorization check (see Realtime & streaming)
and enables AuthorizeSubscribe for subject/group channels — it does not, by
itself, stand up a working end-user browser connection directly to Centrifugo
(what the TS SDK's CNSClient.subscribe() targets). That path's wire protocol —
Centrifugo connection/subscribe-token issuance bound to a subject/group — is an
explicitly unresolved open item in the design. Running Centrifugo itself (its own
config, TLS, scaling) is out of this repo's scope — see Centrifugo's own
documentation.
:::
AWS SSM (secrets)
Path convention (mirrors envctl's shape deliberately, for a mechanical future
swap):
/cns/<env>/svc/<app>/sign_priv SecureString — per-app ed25519 private key
/cns/<env>/common/pubkeys/<app> String — per-app ed25519 public key
IAM is where "the ingestor can't forge tokens" is actually enforced — this is a deployment/IAM concern, not something the Go interface can enforce on its own:
- Each app's own IAM role should read only its own
svc/<app>/*, pluscommon/pubkeys/*. - The
cns-serverprocess's own role should readcommon/pubkeys/*only — never anysign_privpath. CNS verifies signatures; it never mints render-tokens or service-JWTs itself.
Rotation is version-pinned, not kid-based: a token carries app plus the SSM
parameter's integer version, and the verifier resolves the matching public key by
(app, version) — see Apps & credentials.
Set CNS_SECRETS_IMPL=ssm (the default) once the roles above are in place.
For local development without any AWS account at all, CNS_SECRETS_IMPL=memory
swaps in an in-process ed25519 keystore, SSM-path-faithful in shape, that makes
zero AWS calls — this is what the dev fixture uses, precisely
because the ssm adapter's AWS SDK would otherwise resolve ambient credentials
(~/.aws, IMDS) even with AWS_* variables unset.
Identity (SSO-JWT)
Configure a signing-key source (CNS_SSO_JWT_HMAC_SECRET or CNS_SSO_JWT_JWKS_URL)
plus issuer/audience. The adapter is IdP-shape-agnostic — see
Wiring a non-native IdP for a worked example beyond a
platform-native SSO, and Environment variables
for every claim-name override.