ClickHouse
Datafly Signal writes first-party events into a ClickHouse MergeTree table for real-time analytics with sub-second query response times on billions of rows.
Why route ClickHouse through Datafly Signal
- The data lands in infrastructure you own. Signal writes to your ClickHouse directly from your own deployment. Datafly operates no intermediary and never holds a copy of your event data.
- Personal data is hashed on the way in. Email, phone (shaped to E.164 first), first, last and full name and postcode are normalised (trimmed and lower-cased) and hashed with SHA-256 inside your own deployment, so the table holds
email_sha256,phone_sha256and the other digests rather than the raw values. Storing a raw value is a deliberate edit to the installed blueprint. - Only what the blueprint maps is stored. The properties Signal writes are exactly the fields the blueprint maps. Identify traits and client properties the blueprint does not map are never written, so a stray email in an event property cannot land in ClickHouse in plain text.
- Presets for Retail, Insurance, Travel, Media & Streaming, B2B / SaaS, Automotive and General. Each vertical preset names one field per property in the matching event spec, with canonical names (
transaction_id,value,items), legacy aliases (order_id,revenue,products) read automatically, numbers typed as numbers and currency codes upper-cased. - Client IP and user agent are recorded as event context. Both come from the original visitor request rather than from your server, so geography and device analysis done in the warehouse reflects the actual visitor.
- One consent decision governs every destination. ClickHouse is gated by the same consent state as the rest of your stack, so a withdrawal applies everywhere at once.
Prerequisites
Before configuring ClickHouse in Signal, you need a ClickHouse server (self-hosted or ClickHouse Cloud) with a database and a MergeTree table.
Set Up a ClickHouse Server
You have two options:
Option A: ClickHouse Cloud (Managed)
- Sign up at clickhouse.cloud.
- Create a new service and select a region.
- Note the Host (e.g.
abc123.us-east-1.aws.clickhouse.cloud), the secure native port (9440; Signal does not use the HTTPS port8443), and the default Username and Password.
Option B: Self-Hosted
- Install ClickHouse using the official guide.
- Ensure Signal can reach the server: the native TCP interface (port
9000, or9440with TLS) or the HTTP interface (8123, or8443with TLS; ClickHouse Cloud’s HTTPS port). - Note the hostname and port.
Create a Database
Connect to ClickHouse and create a database:
CREATE DATABASE IF NOT EXISTS datafly_events;Create a MergeTree Table
Create a table using the MergeTree engine family for optimal analytics performance:
CREATE TABLE datafly_events.events (
message_id String,
event_type LowCardinality(String),
event_name LowCardinality(String),
anonymous_id String,
user_id String,
ip_address String,
user_agent String,
page_url String,
page_path String,
page_referrer String,
org_id LowCardinality(String),
pipeline_id LowCardinality(String),
event_timestamp DateTime64(3, 'UTC'),
received_at DateTime64(3, 'UTC'),
properties_json String,
context_json String
)
ENGINE = ReplacingMergeTree
PARTITION BY toYYYYMM(event_timestamp)
ORDER BY (event_timestamp, message_id);Signal inserts exactly these column names. An absent value (for example user_id on an anonymous event) is stored as an empty string. ReplacingMergeTree with message_id in the sort key removes a redelivered event at merge time; query with FINAL for exact counts. LowCardinality(String) suits columns with few distinct values.
Create a User for Signal
For self-hosted ClickHouse, create a dedicated user:
CREATE USER datafly_signal IDENTIFIED BY 'your_secure_password';
GRANT INSERT ON datafly_events.events TO datafly_signal;For ClickHouse Cloud, you can use the default user or create an additional user in the Cloud console.
Configuration
| Field | Type | Required | Description |
|---|---|---|---|
host | string | Yes | The ClickHouse server hostname. |
port | string | No | Native protocol 9000 (9440 with TLS) or HTTP interface 8123 (8443 with TLS). Blank follows secure: 9000, or 9440 with TLS. 8123 and 8443 select the HTTP interface. |
database | string | Yes | The target database name. |
table_name | string | Yes | The target table name to insert rows into. Also accepts table. |
username | string | Yes | The ClickHouse username for authentication. |
password | secret | Yes | The ClickHouse password for authentication. |
secure | boolean | No | Encrypt the connection with TLS and verify the server certificate for its host name, against ca_certificate when set, otherwise the system roots. Off unless set; ClickHouse Cloud requires it. Also accepts "true", "1", "yes", "on" and the legacy use_tls. |
ca_certificate | textarea | No | PEM bundle of a private certificate authority, for a self-managed server with TLS. ClickHouse Cloud needs none. |
async_insert | boolean | No | Opt-in server-side batching (see Schema). Off by default. |
Signal Setup
Quick Setup
- Navigate to Integrations in the sidebar.
- Open the Integration Library tab.
- Find ClickHouse or filter by Database.
- Click Install, select a variant if available, and fill in the required fields.
- Click Install Integration to create the integration with a ready-to-use default blueprint.
API Setup
curl -X POST http://localhost:8084/v1/admin/integration-catalog/clickhouse/install \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "ClickHouse",
"variant": "default",
"config": {
"host": "clickhouse.internal.example.com",
"port": "9000",
"database": "datafly_events",
"table_name": "events",
"username": "datafly_signal",
"password": "your_secure_password",
"secure": false
},
"delivery_mode": "server_side"
}'Schema
Signal inserts one row per event into exactly these sixteen columns, so the table must have them all (extra columns of your own are fine if they are nullable or have defaults):
| Column | Contents |
|---|---|
message_id | Unique per delivered event; the idempotency key (a redelivery is not inserted twice). |
event_type | track, page, identify, and so on. |
event_name | Signal event name (snake_case), for example purchase. |
anonymous_id | First-party visitor identifier. |
user_id | Logged-in user identifier; NULL when absent. |
ip_address, user_agent | Client IP and user agent from the visitor’s request. |
page_url, page_path, page_referrer | Page context. |
org_id, pipeline_id | Signal identifiers. |
event_timestamp | Client event time (UTC). |
received_at | Time Signal received the event (UTC). |
properties_json | The fields the blueprint mapped, as JSON (below). |
context_json | The event context as JSON: page, device, campaign, consent, IP and user agent. |
Signal groups the events it is delivering at the same moment into one INSERT (a lone event is sent at once, with no added delay), and a group that fails on a data error is re-sent row by row, so only the offending event is dead-lettered. At the default delivery concurrency that is a few rows per INSERT; ClickHouse recommends batches of at least 1,000 rows and about one insert per second per table. If parts accumulate (“too many parts”), turn on async_insert in the integration: the server then consolidates inserts across Signal’s pods, at the cost of each delivery waiting for the server’s flush timer, which lowers throughput per pod. How far to take this is tracked in #1538.
What the properties hold
The blueprint decides what properties_json contains, and only mapped fields are written (identify traits and unmapped client properties never are):
- The event’s own fields from the preset sit at the top level, for example
transaction_id,value,currency,items,insurance_typeorpolicy_term_months. - The preset’s global block sits under
@root: copies of the envelope (event_name,event_type,user_id,anonymous_id,event_id,message_id,event_timestamp,page_url,page_referrer,ip_address,user_agent,locale,timezone,source_id),utm_source,utm_medium,utm_campaign, the consent flagsconsent_analyticsandconsent_marketing, and the hashed identityemail_sha256,phone_sha256,first_name_sha256,last_name_sha256,name_sha256. identifyaddsname_sha256andpostal_code_sha256at the top level.- An event the preset does not list stores its envelope and the
@rootblock only.
For example, the purchases with their order ID, value and hashed email:
SELECT event_timestamp,
JSONExtractString(properties_json, 'transaction_id') AS transaction_id,
JSONExtractFloat(properties_json, 'value') AS value,
JSONExtractString(properties_json, 'currency') AS currency,
JSONExtractString(properties_json, '@root', 'email_sha256') AS email_sha256
FROM datafly_events.events FINAL
WHERE event_name = 'purchase'
ORDER BY event_timestamp DESC
LIMIT 10;Signal does not write mapped fields to their own columns; create views over properties_json for the fields you query often. What else a row carries outside the blueprint (the whole event context) is under review in #1414.
Stored event context
Alongside the fields your blueprint maps (where email, phone, names and address are stored as normalised SHA-256 by default), each row carries the event’s collected context exactly as Signal received it. The blueprint does not filter or hash the context: this is deliberate (decision 2026-09-23), so the context is complete for attribution, fraud and debugging queries, and it is listed here so you can decide what to keep.
| Stored | What it contains |
|---|---|
| Visitor request | IP address, user agent, Accept-Language, locale and timezone |
| Page | URL including its query string, path, title and referrer |
| Campaign | UTM source, medium, campaign, term and content |
| Consent | The visitor’s consent state |
| Location | GeoIP country, region and city, and the network (ASN) |
| Device | Screen size; for app events the OS and version, manufacturer, model, advertising ID and IDFV when the app collected them |
| Vendor IDs | Cookie and click identifiers in vendor_ids (for example the _fbp browser ID, gclid, ttclid, the GA client ID) |
| Session and library | Session ID, number and engagement time; the collector’s name and version |
| Partner attributes | Profile attributes from partner enrichment (partner_attrs) |
They are written to the ip_address, user_agent, page_url, page_path and page_referrer columns and, in full, to the context_json column, beside anonymous_id and user_id. Click and collected IDs outside the context are stored only where the blueprint maps them.
The IP address, device and advertising IDs, and cookie and click identifiers are personal data under UK and EU GDPR. To keep any of them out of storage, strip, mask or hash the field for every integration with the organisation data layer, apply URL redaction to page URLs, or remove it in the installed blueprint; and set the storage’s retention to match your privacy notice. See Personal data by integration.
Consent
ClickHouse is a first-party destination under your control, gated by the same consent state as every other destination. Each stored event also records the visitor’s consent at collection time: the preset writes consent_analytics and consent_marketing under @root in the properties (properties_json), and the full consent object is in the context. Use them in views or queries if your governance needs a per-row consent filter.
Testing
- Enable the integration in Signal and trigger a test event on your website.
- Connect to ClickHouse and query the target table:
SELECT message_id, event_type, event_name, event_timestamp
FROM datafly_events.events
ORDER BY event_timestamp DESC
LIMIT 10;- Verify that event rows are appearing with correct data.
- In Signal, check the Live Events view to confirm delivery status shows as successful.
Troubleshooting
| Problem | Solution |
|---|---|
| Events not appearing in ClickHouse | Verify the host, port, database, and table name are correct. |
| Connection refused / timeout | Check that the ClickHouse server is running and the port is reachable (native 9000/9440, HTTP 8123/8443), and that secure matches the port: a TLS port needs secure on, a plain one needs it off. Verify firewall rules or the ClickHouse Cloud IP access list. |
Authentication failed | The username or password is incorrect. Verify the credentials. |
Table not found | The table does not exist in the specified database. Verify the database and table names. |
INSERT permission denied | The user lacks INSERT permission. Run GRANT INSERT ON db.table TO user;. |
| TLS errors | The server certificate is verified for its host name. A self-managed server with a private CA needs its bundle in ca_certificate; connect with the host name the certificate names. |
| Slow inserts or “too many parts” | Turn on async_insert in the integration so the server consolidates inserts (see Schema), or raise the delivery concurrency so more rows share an INSERT. |
| Schema mismatch | The column types must match the event data. Verify column types with DESCRIBE TABLE datafly_events.events;. |
Visit ClickHouse documentation for full SQL reference, table engine options, and performance tuning guides.