Self-hosting on Kubernetes
Install the data plane with Helm. The chart is published to the GHCR OCI registry — no
helm repo add needed — and by default it is self-contained: NATS, Redis, ClickHouse, and
Postgres run inside the release as StatefulSets, so a bare cluster is enough to start.
For a one-command install, use origamy deploy — it runs this Helm
install for you, prompts for endpoint exposure, enrols an mTLS identity for the tunnel, and
keeps every secret in a Kubernetes Secret. A manual install authenticates the tunnel with the
bearer token only.
Prerequisites
Section titled “Prerequisites”kubectlconnected to your cluster- Helm 3.12+ (
helm version) - Outbound TCP 443 from the
origamy-dpnamespace totunnel.origamy.io - Your data plane token from the setup wizard (dashboard → Connections)
Install
Section titled “Install”-
Create the namespace.
Terminal window kubectl create namespace origamy-dp -
Store your token as a Secret.
Never pass the token via
--set— it would land in Helm release history (helm history), readable by anyone with cluster access.Terminal window kubectl create secret generic origamy-byod-token \--namespace origamy-dp \--from-literal=auth-token=dpt_<PASTE_YOUR_TOKEN_HERE> -
Install the chart.
Terminal window helm install odp oci://ghcr.io/qubelylabs/charts/origamy-data-plane \--namespace origamy-dp \--version 0.1.17 \--set controlPlane.url=tunnel.origamy.io:443 \--set controlPlane.httpUrl=https://v1.origamy.io \--set controlPlane.telemetryUrl=https://v1.origamy.io/api/v1/telemetry \--set controlPlane.dataPlaneId=<YOUR_DATA_PLANE_ID> \--set portalAgent.existingSecret=origamy-byod-tokencontrolPlane.httpUrlis the bare control-plane URL — the chart appends/api/v1/configfor config-sync itself (without it, sources and destinations never reach the data plane).telemetryUrlis the full endpoint portal-agent reports health to. The secret’s key defaults toauth-token; override withportalAgent.existingSecretAuthKeyif you named it differently. Add--set datastores.auth.enabled=trueto password-protect the bundled Redis, NATS and ClickHouse — the chart generates the passwords into in-cluster Secrets (the CLI’s--datastore-auth). -
Verify.
Terminal window kubectl get pods -n origamy-dpkubectl logs -n origamy-dp deploy/odp-portal-agent --tail=30Look for
portal-agent connected to control plane. The dashboard’s Connections page updates automatically.
Bring your own ClickHouse (optional)
Section titled “Bring your own ClickHouse (optional)”Instead of the bundled ClickHouse, the data plane can use your own server, such as ClickHouse
Cloud or an existing cluster. This needs chart 0.1.19 or newer: older charts connect without
a password, never create the schema on your server and block egress to it.
origamy deploy asks for all of this on install. By
hand:
-
Store the password in a Secret. Reading it with echo off keeps it out of your shell history and out of
ps:Terminal window read -rs CH_PASSWORDprintf %s "$CH_PASSWORD" | kubectl create secret generic origamy-clickhouse \--namespace origamy-dp --from-file=clickhouse-password=/dev/stdinunset CH_PASSWORD -
Add these flags to the
helm installin step 3, with--version 0.1.19or newer:Terminal window --set clickhouse.enabled=false \--set-string clickhouse.host=<YOUR_CLICKHOUSE_HOST> \--set clickhouse.port=9440 \--set clickhouse.secure=true \--set-string clickhouse.username=<YOUR_CLICKHOUSE_USER> \--set clickhouse.existingSecret=origamy-clickhouseFor a server without TLS, use
clickhouse.port=9000andclickhouse.secure=false.
What the chart does with them:
-
Credentials. Every service reads the password from the Secret and embeds it in its connection URL at start-up, so it never appears in a pod spec or in Helm release history. Because it lands in a URL, the password may contain only letters, digits and
-._~!$&'()*+,;=:@. For a password with other characters,clickhouse.passwordworks (the chart encodes it) but is stored in release history. -
Schema. The
clickhouse-initJob creates theeventsdatabase and its tables on your server during every install and upgrade (CREATE … IF NOT EXISTS), so the user needsCREATE DATABASE,CREATE TABLE,INSERTandSELECT. To manage the schema yourself, setclickhouse.initSchema=falseand applyfiles/clickhouse-init.sqlfrom the chart (helm pull oci://ghcr.io/qubelylabs/charts/origamy-data-plane --version <VERSION> --untar). -
Network. The chart’s NetworkPolicy allows egress to
clickhouse.port. A NetworkPolicy cannot name a host, so the default range is0.0.0.0/0; narrow it to your server’s network withnetworkPolicy.clickhouseCIDR. For a ClickHouse in another namespace of the same cluster, allow it with a namespace selector instead, since some CNIs (Cilium, for one) never match in-cluster pods by IP range:networkPolicy:extraEgress:- to:- namespaceSelector:matchLabels:kubernetes.io/metadata.name: <CLICKHOUSE_NAMESPACE>ports:- port: 9000protocol: TCP -
Datastore auth.
datastores.auth.enabled=truestill password-protects the bundled Redis and NATS. It does not touch your ClickHouse credentials.
| Value | Default | Meaning |
|---|---|---|
clickhouse.enabled |
true |
false uses your server instead of the bundled one |
clickhouse.host |
— | your server’s host name or IPv4 address |
clickhouse.port |
9000 |
native protocol port (usually 9440 with TLS); not the HTTP ports 8123/8443 |
clickhouse.secure |
false |
TLS on the native port |
clickhouse.username |
default |
user the services and the schema Job connect as |
clickhouse.existingSecret |
— | Secret holding the password |
clickhouse.existingSecretPasswordKey |
clickhouse-password |
key of the password in that Secret |
clickhouse.database |
events |
database the services read and write |
clickhouse.initSchema |
true |
run the schema Job against your server |
networkPolicy.clickhouseCIDR |
0.0.0.0/0 |
egress range for the ClickHouse port |
networkPolicy.extraEgress |
[] |
extra egress rules, appended as written |
The other bundled datastores can be swapped the same way (nats, redis and postgres:
<name>.enabled=false plus the connection settings). Store passwords in Secrets for the same
reason as the token.
AI engine
Section titled “AI engine”The orchestrator engine (agent runs, copilot) is an opt-in, gated package that runs inside your cluster. Needs chart 0.1.16 or newer:
origamy upgrade --enable-ai # --disable-ai turns it off; datastores are never touchedThe chart generates the engine’s encryption key and internal API token into a Secret on first enable and keeps them across upgrades. The engine stays inert until AI is enabled for the workspace and an LLM credential is added in the dashboard; on a self-hosted plane it only ever calls the model provider with your workspace’s own credential.
Prediction (churn, propensity, LTV)
Section titled “Prediction (churn, propensity, LTV)”The predictor is an opt-in service (a gated plan feature, disabled by default). It trains churn, purchase-propensity, and lifetime-value models on your workspace’s own event history and writes the results back as profile traits. Needs chart 0.1.17 or newer — the CLI refuses the toggle on an older chart rather than letting Helm silently ignore it. Turn it on with:
origamy upgrade --enable-predictorUse the CLI for this rather than a raw helm upgrade --set predictor.enabled=true: the CLI
re-applies your values from a clean export so the chart’s own predictor defaults fill in.
A raw --reuse-values --set on a release installed before the predictor existed has none of
those defaults and the upgrade fails to render. origamy upgrade --disable-predictor turns
it back off.
Everything happens inside your data plane: models are fit against the plane’s ClickHouse (bundled, or your own), stored in the bundled Postgres, and the scores are written back to Redis and ClickHouse. No profile data and no model ever leaves your infrastructure — apart from your own ClickHouse, the predictor makes no outbound calls.
Once it has run, computed traits appear on each profile — use them anywhere you use traits (segment rules, the People filter, the profile view):
| Trait | Meaning |
|---|---|
churn_score |
0–1 probability the profile goes inactive within the horizon |
propensity_score |
0–1 probability the profile converts |
ltv_score |
predicted lifetime value, in your revenue units |
churn_bucket, propensity_bucket |
low / medium / high band of the matching score |
It trains daily and scores hourly. A model only trains once there are enough labeled profiles
(default 200); propensity and LTV additionally need to know which events count as a conversion
(default: conversion, order completed, order_completed, purchase, checkout completed).
On a smaller workspace, lower the threshold and name your conversion events:
origamy upgrade --enable-predictor \ --set predictor.predict.minSamples=20 \ --set 'predictor.predict.conversionEvents=order completed,subscription started'kubectl logs -n origamy-dp deploy/odp-predictor shows each train/score pass; too few samples there just means the workspace hasn’t accumulated enough history yet.
Expose your event endpoint
Section titled “Expose your event endpoint”Your SDKs send events to the ingestion gateway, a ClusterIP service by default —
reachable only inside the cluster. To receive events you must expose it.
Option A — LoadBalancer:
helm upgrade odp oci://ghcr.io/qubelylabs/charts/origamy-data-plane \ --version 0.1.17 --namespace origamy-dp --reuse-values \ --set ingestGateway.service.type=LoadBalancer \ --set-string 'ingestGateway.service.annotations.service\.beta\.kubernetes\.io/aws-load-balancer-scheme=internet-facing'
# then get the public address:kubectl get svc -n origamy-dp odp-ingestion-gatewaySend events to http://<load-balancer-host>:8081/v1/track.
Option B — Ingress (HTTPS on your domain):
helm upgrade odp oci://ghcr.io/qubelylabs/charts/origamy-data-plane \ --version 0.1.17 --namespace origamy-dp --reuse-values \ --set ingestGateway.ingress.enabled=true \ --set ingestGateway.ingress.host=events.yourcompany.com \ --set ingestGateway.ingress.tls.enabled=true \ --set ingestGateway.ingress.tls.secretName=events-tlsSend events to https://events.yourcompany.com/v1/track.
Test it (works before you expose anything):
kubectl port-forward -n origamy-dp svc/odp-ingestion-gateway 8081:8081curl -u <write-key>: http://localhost:8081/v1/track \ -H 'Content-Type: application/json' -d '{"userId":"test","event":"hello"}'Once your endpoint is public, paste it into your source’s Setup tab in the dashboard so the copy-paste SDK snippets use it.
NetworkPolicy (if your cluster enforces one)
Section titled “NetworkPolicy (if your cluster enforces one)”apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: origamy-portal-agent-egress namespace: origamy-dpspec: podSelector: matchLabels: app.kubernetes.io/component: portal-agent policyTypes: [Egress] egress: - ports: - protocol: TCP port: 443Sizing
Section titled “Sizing”| Tier | Nodes | CPU | RAM | Capacity |
|---|---|---|---|---|
| Starter | 3 | 2 vCPU each | 4 GB each | < 100k events/day |
| Production | 5 | 4 vCPU each | 16 GB each | 100k – 2M events/day |
| High volume | 8+ | 8 vCPU each | 32 GB each | 2M+ events/day |
For high-volume production, run ClickHouse on dedicated nodes — a common shape is 3× (8 vCPU, 32 GB RAM, 500 GB NVMe) — or bring an externally managed cluster (see above).
Token rotation
Section titled “Token rotation”Rotate the token from the Connections page, patch the Secret, and restart portal-agent:
kubectl create secret generic origamy-byod-token \ --namespace origamy-dp \ --from-literal=auth-token=dpt_<NEW_TOKEN> \ --dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deployment/odp-portal-agent -n origamy-dpkubectl rollout status deployment/odp-portal-agent -n origamy-dpUpgrades
Section titled “Upgrades”Use the CLI — it pins every service image to the target version (a raw
helm upgrade --reuse-values can silently leave pods on old images) and never touches
your datastores:
origamy upgrade # to the latest published versionorigamy upgrade --version 0.1.17 # to a specific versionorigamy rollback # back to the previous revisionorigamy upgrade is also how you switch optional services on or off later:
--enable-ai / --disable-ai for the agentic AI engine, and
--enable-predictor / --disable-predictor for prediction (see above). Toggles preserve
all your existing values, and are refused on a chart too old to carry the service.
By hand, pin the image tag explicitly — --reuse-values alone keeps whatever per-service tag
the release already had:
helm upgrade odp oci://ghcr.io/qubelylabs/charts/origamy-data-plane \ --version 0.1.17 --namespace origamy-dp --reuse-values \ --set global.imageTag=0.1.17kubectl rollout restart deployment -n origamy-dpTroubleshooting
Section titled “Troubleshooting”| Symptom | Fix |
|---|---|
| Pod in CrashLoopBackOff | Check the Secret exists with the right key: kubectl describe secret origamy-byod-token -n origamy-dp. The key must be auth-token (not AUTH_TOKEN). |
dial tcp: i/o timeout to tunnel.origamy.io:443 |
A NetworkPolicy is blocking egress — apply the example above, or confirm the egress rule includes TCP 443. |
Token rejected (Unauthenticated in logs) |
The Secret may still hold the old token after a rotation. Patch the Secret and rollout-restart portal-agent. |
| Sources missing on the data plane | Check kubectl logs deploy/odp-config-sync -n origamy-dp and verify controlPlane.url is reachable from inside the cluster. |
| Services crash-loop with an external ClickHouse | They wait until the schema exists on your server. kubectl logs -n origamy-dp -l app.kubernetes.io/component=clickhouse-init --tail=20 shows why the schema Job cannot connect. |
| ClickHouse connection refused or reset (external CH) | Check clickhouse.host and clickhouse.port (the native protocol port, 9000 or 9440 with TLS, not HTTP 8123/8443), that clickhouse.secure matches the server, and that networkPolicy.clickhouseCIDR covers it. |
| ClickHouse authentication failed (external CH) | The origamy-clickhouse Secret must hold the password under clickhouse-password, for the user in clickhouse.username. |
