Server-side GTM Firestore lookup variables
last verified · against GTM server-side Firestore API docs as of 2026-07
Reference for the server-side GTM Firestore Lookup variable and Firestore Sandboxed JS API: read, query, write, transactions, IAM roles, caching, and limits.
What this is
The Firestore Lookup variable and the Firestore Sandboxed JS API let a server-side Google Tag Manager container read and write documents in a Cloud Firestore (Native mode) database during request handling, enriching tags with server-held data. Both shipped for server-side containers on 2022-03-24, alongside the Promise Sandboxed JS API — release text: “Launched Firestore Sandboxed JS API, Firestore Lookup variable, and Promise Sandboxed JS API for server-side containers.”
Capabilities (launched 2022-03-24)
| Capability | Surface | Use |
|---|---|---|
| Firestore Lookup variable | Container UI variable | Read one field/value from Firestore into a tag |
| Firestore Sandboxed JS API | Custom templates | Firestore.read / query / write / runTransaction |
| Promise Sandboxed JS API | Custom templates | Await the promises the Firestore API returns |
Support constraints
| Constraint | Value |
|---|---|
| Firestore mode | Native mode only — not Datastore mode |
| Database | (default) only — named databases unsupported |
projectId |
Optional; defaults to env GOOGLE_CLOUD_PROJECT |
| Result cache | Enabled by default, for the duration of the request |
Query limit |
Default 5 |
| Transaction retries | Up to two on conflict |
Firestore Sandboxed JS API — functions
| Function | Resolves to | Rejects / notes |
|---|---|---|
Firestore.read(path, options) |
{id, data} |
{reason:'not_found'} when the document is missing |
Firestore.query(collection, queryConditions, options) |
Array of matching documents | Conditions ANDed; limit defaults to 5 |
Firestore.write(path, input, options) |
Written document ID | No ID when inside a transaction (writes are batched) |
Firestore.runTransaction(callback) |
Array of document IDs, one per write | Retried up to two times on conflict |
Options
| Option | Applies to | Default | Effect |
|---|---|---|---|
projectId |
All functions | env GOOGLE_CLOUD_PROJECT |
Target a different GCP project |
disableCache |
read, query |
false (cache on) |
Bypass the per-request result cache |
limit |
query |
5 |
Maximum documents returned |
merge |
write |
false |
Merge supplied keys vs overwrite the whole document |
transaction |
read, write |
— | Transaction identifier from runTransaction |
queryConditions
Shape: queryConditions: [[key, operator, expectedValue], ...]. Conditions are
ANDed.
| Element | Position | Meaning |
|---|---|---|
key |
0 | Field path to test |
operator |
1 | Comparison operator |
expectedValue |
2 | Value compared against |
The operator set is not enumerated on the API page — it defers to Firestore REST
semantics. Assume Firestore REST operators (==, <, <=, >, >=,
array-contains) and verify in your container. The element shape of each
returned document is not stated verbatim on that page (“an array of Firestore
documents that match”); confirm whether it matches read’s {id, data}
empirically.
Errors
Reject shape: {reason: <code>}.
| reason | Trigger |
|---|---|
not_found |
Firestore.read on a missing document |
| Firestore REST API error codes | Any function; e.g. permission_denied |
Firestore Lookup variable — UI fields
Practitioner-documented (Simo Ahava, Measurelab); official developer docs mention the variable only in passing. Treat labels as version-dependent and verify in the container.
| Field | Behavior |
|---|---|
| Lookup Type: Document Path | Direct single-document read by collection/document path — fastest |
| Lookup Type: Collection Path & Query | Match within a collection; if several match, only the FIRST document is used |
| Key Path | Field/value to return; dot notation for nested/array values |
| Default Value | Returned when nothing is found |
| Project ID | Override to read Firestore in another GCP project |
IAM roles
| Role | ID | Grants |
|---|---|---|
| Cloud Datastore User | roles/datastore.user |
Read + write — the role GTM’s setup guide specifies for Firestore access |
| Cloud Datastore Viewer | roles/datastore.viewer |
Read-only — likely enough for a pure lookup, but not documented as supported |
| Cloud Datastore Admin | roles/datastore.admin |
Administrative access to Cloud Datastore |
Deployment env vars (container outside Google Cloud)
| Variable | Purpose |
|---|---|
GOOGLE_APPLICATION_CREDENTIALS |
Path to the mounted service-account JSON key |
GOOGLE_CLOUD_PROJECT |
Project the server selects implicitly; default projectId for lookups |
Common patterns
| Pattern | Approach |
|---|---|
| Enrichment before dispatch | BigQuery computes CLV / tier / price / consent, scheduled-exports to Firestore keyed by user_id / client_id / transaction_id; the variable reads it in real time to augment GA4 / Ads / Floodlight tags |
| Value-based bidding | Store item monetary value in Firestore; inject it on purchase via a Firestore Lookup inside an Augment event transformation |
| Transaction de-duplication | Write-then-check a Firestore document to block duplicate purchase events |
| Cross-device stitching | Query a GCP database in real time to stitch visitor identity |
| Prefer Document Path | When the document ID is derivable from request data (client_id, transaction_id) — a single direct read, the fastest option |
| Co-locate data and container | Keep Firestore in the same GCP project when possible — the lookup runs synchronously in request handling and adds latency per tagged event |
Gotchas
- Every lookup fails although the document exists — cause: the database is in Datastore mode or is a named (non-default) database — fix: provision Firestore in Native mode and use the
(default)database; no supported alternative exists. - Cross-project lookup returns nothing or
permission_denied— cause: the container’s runtime service account has no IAM binding on the target Firestore project — fix: grant that service accountroles/datastore.useron the project holding the data. - A custom template aborts when a looked-up document is absent — cause:
Firestore.readrejects with{reason:'not_found'}, surfacing as an unhandled rejection — fix:.catch()and branch onreason === 'not_found'; for the variable, set a Default Value. - A value written earlier in the request reads back stale — cause:
read/querycache results for the duration of the request — fix: passdisableCache:true, or perform the read and write insideFirestore.runTransaction. - Collection Path & Query returns the wrong record — cause: results are bounded (
limitdefault5) and only the first match is used, so a loose query is non-deterministic — fix: make conditions match exactly one document, or key a Document Path lookup by a unique ID. - Lookups behave oddly when a field holds an empty array — cause: a community-reported edge case whose root cause is not confirmed in official docs — fix: store a sentinel or omit empty-array fields and validate; verify against your container version.
Quick recipes
Direct read by document path, with missing-document handling:
const Firestore = require('Firestore');
// projectId omitted -> defaults to the GOOGLE_CLOUD_PROJECT env var.return Firestore.read('users/' + clientId).then(function (doc) { // Resolves to { id, data }. return doc.data.tier;}, function (err) { // Rejects with { reason: 'not_found' } when the document is absent. if (err.reason === 'not_found') return 'unknown'; return undefined;});Query a collection (conditions ANDed, capped by limit):
const Firestore = require('Firestore');
// Each condition is [key, operator, expectedValue].const conditions = [ ['country', '==', 'SE'], ['tier', '==', 'gold']];
return Firestore.query('customers', conditions, { limit: 5 }).then(function (docs) { // Resolves to an array of matching documents (default limit 5). if (docs.length === 0) return 'none'; return docs[0];});Read the freshest value written earlier in the same request:
const Firestore = require('Firestore');
// disableCache bypasses the per-request result cache.return Firestore.read('counters/daily', { disableCache: true }).then(function (doc) { return doc.data.count;});Write with merge vs overwrite:
const Firestore = require('Firestore');
// merge:true updates only supplied keys; default (false) overwrites the whole document.return Firestore.write('users/' + clientId, { tier: 'gold' }, { merge: true }).then(function (id) { // Resolves to the written document ID. return id;});Atomic read-modify-write inside a transaction:
const Firestore = require('Firestore');
// Retried up to two times on conflict; pass the transaction id to each read/write.return Firestore.runTransaction(function (transaction) { return Firestore.read('counters/daily', { transaction: transaction }).then(function (doc) { const next = doc.data.count + 1; // Batched write: returns no ID inside the transaction. return Firestore.write('counters/daily', { count: next }, { transaction: transaction }); });}).then(function (ids) { // Resolves to an array of document IDs, one per write. return ids;});Grant a cross-project service account read + write on Firestore:
# Bind the sGTM container's runtime service account on the project that holds# the Firestore data (needed when Project ID points at another GCP project).gcloud projects add-iam-policy-binding TARGET_PROJECT \ --member=serviceAccount:SA_EMAIL \ --role=roles/datastore.userRelated
Sources
Official pages these values were read from (read 2026-07-20):
- Server-side tagging APIs — Firestore Sandboxed JS API reference (read 2026-07-20)
- Manual setup guide — service account, Cloud Datastore User, env vars (read 2026-07-20)
- Google Floodlight tags — mentions the
{{Firestore Lookup}}variable (read 2026-07-20) - Tag Manager release notes — 2022-03-24 launch (read 2026-07-20)
- Firestore roles and permissions | Google Cloud IAM (read 2026-07-20)
Practitioner references for the variable’s UI fields (not official Google docs):
- Enrich server-side data with Cloud Firestore — Simo Ahava (read 2026-07-20)
- Access BigQuery data from server-side GTM via Firestore — Measurelab (read 2026-07-20)
Two limitations are worth stating explicitly. No dedicated official page
documents the Lookup variable’s UI fields, so exact labels are version-dependent
and should be verified in the container. And whether the read-only
roles/datastore.viewer suffices for a pure lookup is unconfirmed — GTM’s setup
guide specifies roles/datastore.user (read + write).
Changelog
- — Initial version, verified against official documentation.