IntegrationsDatabasesGoogle Spanner

Google Cloud Spanner

Datafly Signal writes first-party events into a Spanner table — globally distributed, strongly consistent, horizontally scalable relational storage.

Why route Google Cloud Spanner through Datafly Signal

  • The data lands in infrastructure you own. Signal writes to your Google Cloud Spanner 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_sha256 and 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 Cloud Spanner 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. Google Cloud Spanner is gated by the same consent state as the rest of your stack, so a withdrawal applies everywhere at once.

Prerequisites

Before configuring Google Cloud Spanner in Signal, you need a GCP project with a Spanner instance, a database with a target table, and a service account.

Create a GCP Account and Project

  1. Sign up at cloud.google.com if you don’t already have an account.
  2. Create a new project or select an existing one in the GCP Console.
  3. Note the Project ID.

Enable the Cloud Spanner API

  1. Go to APIs & Services > Library.
  2. Search for Cloud Spanner API.
  3. Click Enable.

Create a Spanner Instance

  1. Go to the Spanner console.
  2. Click Create instance.
  3. Enter an Instance name (e.g. datafly-events) and Instance ID.
  4. Choose a Configuration:
    • Regional — data in a single region (lower latency, lower cost).
    • Multi-region — data replicated across regions (higher availability).
  5. Set the Compute capacity (processing units or nodes). 1 node = 1000 processing units.
  6. Click Create.

For development and testing, you can use the free trial instance (1 node, limited to specific regions). For production, size the instance based on your expected write throughput.

Create a Database

  1. In the Spanner console, click on your instance.
  2. Click Create database.
  3. Enter a Database name (e.g. events_db).
  4. Click Create.

Create a Table

In the Spanner console, open the database and run the following DDL:

CREATE TABLE events (
  message_id      STRING(64) NOT NULL,
  event_type      STRING(32) NOT NULL,
  event_name      STRING(MAX),
  anonymous_id    STRING(MAX),
  user_id         STRING(MAX),
  ip_address      STRING(45),
  user_agent      STRING(MAX),
  page_url        STRING(MAX),
  page_path       STRING(MAX),
  page_referrer   STRING(MAX),
  org_id          STRING(64),
  pipeline_id     STRING(64),
  event_timestamp TIMESTAMP,
  received_at     TIMESTAMP,
  properties_json STRING(MAX),
  context_json    STRING(MAX),
) PRIMARY KEY (message_id);

Signal writes exactly these column names with an InsertOrUpdate mutation keyed on message_id, so a redelivered event overwrites its own row. message_id is random, which spreads writes across splits as Google recommends; never put a timestamp first in the primary key. Keep properties_json and context_json as STRING(MAX): Signal binds them as strings, which a JSON column rejects.

Create a Service Account

  1. Go to IAM & Admin > Service Accounts > Create Service Account.
  2. Enter a name (e.g. datafly-signal-spanner).
  3. Grant the Cloud Spanner Database User role (roles/spanner.databaseUser).
  4. Click Done.

Generate a Service Account Key

  1. Click on the service account.
  2. Go to Keys > Add Key > Create new key > JSON.
  3. The key file will download. Store it securely.
⚠️

Store the JSON key file securely. Do not commit it to version control.

Configuration

FieldTypeRequiredDescription
project_idstringYesThe Google Cloud project ID that contains the Spanner instance.
instance_idstringYesThe Spanner instance ID.
database_idstringYesThe Spanner database ID.
table_namestringYesThe target table name (case-sensitive). Also accepts table.
service_account_jsonsecretYesThe full JSON key file content for a service account with roles/spanner.databaseUser.

You can alternatively supply a single fully-qualified database field in the form projects/<pid>/instances/<iid>/databases/<did> instead of the three split values.

Signal Setup

Quick Setup

  1. Navigate to Integrations in the sidebar.
  2. Open the Integration Library tab.
  3. Find Google Cloud Spanner or filter by Database.
  4. Click Install, select a variant if available, and fill in the required fields.
  5. 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/google_spanner/install \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Google Cloud Spanner",
    "variant": "default",
    "config": {
      "project_id": "datafly-analytics",
      "instance_id": "datafly-events",
      "database_id": "events_db",
      "table_name": "events",
      "service_account_json": "{\"type\": \"service_account\", ...}"
    },
    "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):

ColumnContents
message_idUnique per delivered event; the idempotency key (a redelivery is not inserted twice).
event_typetrack, page, identify, and so on.
event_nameSignal event name (snake_case), for example purchase.
anonymous_idFirst-party visitor identifier.
user_idLogged-in user identifier; NULL when absent.
ip_address, user_agentClient IP and user agent from the visitor’s request.
page_url, page_path, page_referrerPage context.
org_id, pipeline_idSignal identifiers.
event_timestampClient event time (UTC).
received_atTime Signal received the event (UTC).
properties_jsonThe fields the blueprint mapped, as JSON (below).
context_jsonThe event context as JSON: page, device, campaign, consent, IP and user agent.

Spanner mutations are atomic per event; the 10 MiB column limit and 80,000 mutations per commit are far above one event’s row.

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_type or policy_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 flags consent_analytics and consent_marketing, and the hashed identity email_sha256, phone_sha256, first_name_sha256, last_name_sha256, name_sha256.
  • identify adds name_sha256 and postal_code_sha256 at the top level.
  • An event the preset does not list stores its envelope and the @root block only.

For example, the purchases with their order ID, value and hashed email:

SELECT event_timestamp,
       JSON_VALUE(properties_json, '$.transaction_id')        AS transaction_id,
       CAST(JSON_VALUE(properties_json, '$.value') AS FLOAT64) AS value,
       JSON_VALUE(properties_json, '$.currency')              AS currency,
       JSON_VALUE(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.

StoredWhat it contains
Visitor requestIP address, user agent, Accept-Language, locale and timezone
PageURL including its query string, path, title and referrer
CampaignUTM source, medium, campaign, term and content
ConsentThe visitor’s consent state
LocationGeoIP country, region and city, and the network (ASN)
DeviceScreen size; for app events the OS and version, manufacturer, model, advertising ID and IDFV when the app collected them
Vendor IDsCookie and click identifiers in vendor_ids (for example the _fbp browser ID, gclid, ttclid, the GA client ID)
Session and librarySession ID, number and engagement time; the collector’s name and version
Partner attributesProfile 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.

Cloud Spanner 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

  1. Enable the integration in Signal and trigger a test event on your website.
  2. Open the Spanner console and navigate to your database.
  3. Go to Query and run:
SELECT message_id, event_type, event_name, event_timestamp
FROM events
ORDER BY event_timestamp DESC
LIMIT 10;
  1. Verify that event rows are appearing with correct data.
  2. In Signal, check the Live Events view to confirm delivery status shows as successful.

Troubleshooting

ProblemSolution
Events not appearing in the tableVerify the project ID, instance ID, database ID, and table name are correct.
Permission denied (403)The service account lacks the Cloud Spanner Database User role. Add it in IAM & Admin > IAM.
NOT_FOUND: Database not foundThe database does not exist. Verify the database ID in the Spanner console.
NOT_FOUND: Table not foundThe table does not exist in the database. Verify the table name (case-sensitive in Spanner).
Invalid service account JSONEnsure you pasted the complete JSON key file content.
RESOURCE_EXHAUSTEDThe instance is at capacity. Increase the number of processing units or nodes.
Write hotspotsAvoid sequential primary keys. Use UUIDs or add a hash prefix to distribute writes evenly across splits.

Visit Google Cloud Spanner documentation for full SQL reference, schema design best practices, and performance tuning guides.

See also