MySQL
Datafly Signal writes first-party events into a MySQL table for high-performance relational storage with JSON column support and broad ecosystem compatibility.
Why route MySQL through Datafly Signal
- The data lands in infrastructure you own. Signal writes to your MySQL 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 MySQL 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. MySQL is gated by the same consent state as the rest of your stack, so a withdrawal applies everywhere at once.
Prerequisites
Before configuring MySQL in Signal, you need a MySQL server (self-hosted or managed), a database and table, and a user with INSERT privileges.
Set Up a MySQL Server
You have several options:
Option A: Managed MySQL
- Amazon RDS for MySQL — Create an RDS instance
- Azure Database for MySQL — Create an Azure MySQL server
- Google Cloud SQL for MySQL — Create a Cloud SQL instance
- PlanetScale, Aiven, or other managed providers
Option B: Self-Hosted MySQL
- Install MySQL using the official guide.
- Start the MySQL server.
- Ensure the server is accessible from your Signal infrastructure on port
3306.
Configure Network Access
Ensure your MySQL server accepts connections from Signal’s IP addresses:
- RDS: Configure the security group inbound rules.
- Azure: Add firewall rules in the Networking tab.
- Cloud SQL: Add authorised networks.
- Self-hosted: Configure the
bind-addressinmy.cnfand firewall rules.
Create a Database and Table
Connect to the MySQL server and create the target database and table:
CREATE DATABASE datafly_events;
USE datafly_events;
CREATE TABLE events (
message_id VARCHAR(64) NOT NULL PRIMARY KEY,
event_type VARCHAR(32) NOT NULL,
event_name VARCHAR(255),
anonymous_id VARCHAR(128),
user_id VARCHAR(255),
ip_address VARCHAR(45),
user_agent TEXT,
page_url TEXT,
page_path TEXT,
page_referrer TEXT,
org_id VARCHAR(64),
pipeline_id VARCHAR(64),
event_timestamp DATETIME(3),
received_at DATETIME(3),
properties_json JSON,
context_json JSON,
INDEX idx_events_ts (event_timestamp),
INDEX idx_events_name (event_name, event_timestamp)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;Signal inserts exactly these column names. utf8mb4 gives full Unicode support, and the JSON columns allow JSON_EXTRACT and the -> / ->> operators. message_id must be the primary key: Signal inserts with INSERT ... ON DUPLICATE KEY UPDATE message_id = message_id, which skips a redelivered event.
Create a User for Signal
CREATE USER 'datafly_signal'@'%' IDENTIFIED BY 'your_secure_password';
GRANT INSERT ON datafly_events.events TO 'datafly_signal'@'%';
FLUSH PRIVILEGES;Replace '%' with Signal’s specific IP address or CIDR range for tighter security. The % wildcard allows connection from any host.
Configuration
| Field | Type | Required | Description |
|---|---|---|---|
host | string | Yes | The MySQL server hostname. |
port | string | No | The MySQL server port. Defaults to 3306. |
database | string | Yes | The target database name. |
table_name | string | Yes | The target table name to insert rows into. |
username | string | Yes | The database username for authentication. |
password | secret | Yes | The database password for authentication. |
tls_mode | string | No | true (the default: TLS, certificate and host name verified, against ca_certificate when set, otherwise the system roots), verify-ca (chain verified against ca_certificate, host name not: for a certificate that names the instance rather than the host you dial, such as a Cloud SQL per-instance CA), skip-verify and preferred (TLS without verifying anything; preferred also falls back to plain text) or false. Also accepts the legacy use_tls. |
ca_certificate | textarea | No | PEM bundle of the certificate authority that signed the server’s certificate (Amazon RDS, Cloud SQL, Azure Database for MySQL, or a private CA). Used with true or verify-ca; refused with false, skip-verify or preferred. |
Signal Setup
Quick Setup
- Navigate to Integrations in the sidebar.
- Open the Integration Library tab.
- Find MySQL 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/mysql/install \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "MySQL",
"variant": "default",
"config": {
"host": "db.example.com",
"port": "3306",
"database": "datafly_events",
"table_name": "events",
"username": "datafly_signal",
"password": "your_secure_password",
"tls_mode": "true"
},
"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 inserts with INSERT ... ON DUPLICATE KEY UPDATE message_id = message_id, so a redelivered event (same message_id) is skipped rather than stored twice; message_id must be the primary key or carry a unique index. Only duplicates are ignored: a value the column cannot hold (too long, out of range, invalid JSON, NULL into NOT NULL) fails the insert in MySQL’s default strict mode and the event is dead-lettered with the MySQL error, rather than stored truncated. Size the columns generously (the table above uses TEXT for URLs and the user agent). A server running without strict mode still truncates with a warning.
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,
properties_json->>'$.transaction_id' AS transaction_id,
CAST(properties_json->>'$.value' AS DECIMAL(12,2)) AS value,
properties_json->>'$.currency' AS currency,
properties_json->>'$."@root".email_sha256' AS email_sha256
FROM events
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
MySQL 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 the MySQL server 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 the table | Verify the host, port, database, and table name are correct. |
| Connection refused / timeout | Check that the MySQL server accepts connections from Signal’s IP. Verify firewall rules, security groups, or authorised networks. |
Access denied for user | The username or password is incorrect, or the user cannot connect from Signal’s IP. Verify credentials and user host restriction. |
INSERT command denied | The user lacks INSERT privilege. Run GRANT INSERT ON db.table TO 'user'@'%';. |
Table doesn't exist | The table does not exist in the specified database. Verify the database and table names. |
| TLS connection errors | With tls_mode true the server certificate is verified against the system roots unless ca_certificate is set. For a managed service (RDS, Cloud SQL, Azure) paste its CA bundle into ca_certificate; if the certificate does not name the host you connect to, use verify-ca. |
Data too long / out-of-range errors (dead-lettered) | A value did not fit its column. Widen the column (TEXT for URLs and user agents) and replay the dead letters. |
| Character encoding issues | Ensure the table uses utf8mb4 character set and the connection character set matches. |
Visit MySQL documentation for full SQL reference, performance tuning, and replication setup.