Skip to main content
The Firestore integration gives Polycore a native document-database read surface: point reads, collection and collection-group queries, server-side aggregations, collection discovery, and live shape inference. The runner authenticates with Application Default Credentials inside your GCP environment. Polycore cloud does not receive a service-account key.
During early access, we design the GCP identity, runner deployment, query surface, indexes, and agent context with your team.

Architecture

The Firebase Admin SDK bypasses Firestore Security Rules. Read-only safety must come from the runner’s GCP IAM role and the runner’s read-only query implementation, not client-side Firestore rules.

Read-only enforcement

GCP IAM

The runner’s runtime service account receives roles/datastore.viewer on the Firestore project. GCP rejects write operations at the API boundary.

Runner query surface

Built-in Firestore queries call read methods only. Mutations are a separate, approval-gated family that is never implied by enabling reads.
Both layers stay intact when you enable generic mutations with a separate write credential. If you enable mutations as true (ADC), write ability is whatever the runtime SA already has: the usual pattern when customer actions already write via the same identity. Firestore in Native mode uses Datastore IAM roles, which is why the predefined viewer role is named roles/datastore.viewer.

1. Bind the runtime identity

The normal hosted pattern is keyless:
1

Create or select a runtime service account

Use a dedicated identity for the runner in this environment.
2

Grant viewer access

Grant roles/datastore.viewer on the one GCP project the runner should read.
3

Attach it to the runner host

The VM or container receives the service account through GCP workload identity. The Admin SDK resolves it through Application Default Credentials.
For local evaluation only, Google supports GOOGLE_APPLICATION_CREDENTIALS pointing to a key file. Production runner deployments should prefer workload identity.

2. Identify the Firestore project

The runner needs the Firestore project id in its runtime environment:
This coordinate is not a credential. The runtime service account still decides whether the runner can read that project.

3. Enable Firestore queries

The committed capability config is small:
polycore.json

Available capabilities

read
Reads one full document path with an optional field projection and returns document metadata.
read
Reads up to 100 document paths in one batched call and reports missing paths separately.
read
Queries a collection path or collection-group id with filters, boolean groups, ordering, projections, limits, offsets, and cursors.
read
Runs a server-side count over a collection or collection group.
read
Runs up to five server-side count, sum, or average aggregations.
read
Lists top-level collections or subcollections beneath a document path.
read
Samples bounded live documents and reports observed field names, types, presence, and examples.

Enforced limits

  • firestore.query returns 100 documents by default and accepts a maximum limit of 1,000.
  • Query offset is capped at 10,000.
  • firestore.getMany accepts at most 100 document paths.
  • firestore.aggregate accepts at most five aggregate expressions.
  • firestore.describe samples 50 documents by default and at most 100.

Empty results explain themselves

An empty result is the most ambiguous answer a document database can give. A wrong collection path, a misspelled field, a filter value that never matches, an orderBy on a field the documents do not carry, and a genuinely empty collection all come back as []. A caller cannot tell them apart, so it guesses again — and a question that had a one-line answer turns into a long, expensive search. When firestore.query or firestore.describe finds nothing, the runner spends one extra query to establish why and returns a diagnosis alongside the empty result:
It distinguishes three situations:

Nothing there

The collection or collection group holds no documents at all, so no filter could have matched. For a nested path it also reports whether the parent document exists, which separates “wrong id” from “no data yet”.

Wrong field name

The collection has data, but a field named in where or orderBy appears in none of the sampled documents. The field name is the bug, not the data.

Genuinely no match

Every referenced field exists and the values simply matched nothing. That is a real answer to report, not a query to broaden.
The second case catches a Firestore behaviour that surprises almost everyone: ordering by a field a document does not have excludes that document entirely. Sorting an optional field returns nothing while every part of the query looks correct.
The diagnosis costs one sampled read and only ever runs on an empty result. If it fails — a permission boundary, say — the read still returns normally without it: an explanation is a courtesy on top of a correct answer, never a way for a successful read to fail.

Collections and collection groups

A path such as accounts or teams/team_42/members addresses one concrete collection.
For totals, firestore.count and firestore.aggregate keep computation in Firestore. The agent receives the aggregate, not every matching document.

Firestore values across JSON

Firestore types that JSON cannot represent directly use tagged values:
A Firestore Timestamp does not compare equal to a plain ISO string. Tagged values keep filters and returned data faithful to the datastore’s native types.

Schema grounding

Firestore does not enforce a schema, so Polycore supports two complementary ways to ground an agent.

Live description

firestore.describe samples a bounded set of documents and reports observed fields, types, and presence. It reflects live drift but is not a formal schema.

Typesync definition

If your application maintains a Typesync schema, the runner can load a build-generated JSON definition and expose bounded model lookup tools.

Optional Typesync configuration

polycore.json
The generated definition is baked into the runner image and read locally. The full schema is not appended to every agent prompt. Instead, the runner exposes list and get tools for document models and reusable aliases.
read
Searches available document models and their Firestore paths.
read
Returns one document model with its fields and field types.
read
Searches reusable Typesync aliases.
read
Returns one complete alias model.
Keep the canonical Typesync source and generation step in the application or schema repository. Copy only the generated JSON artifact into the runner build context.

Index requirements

Firestore automatically creates many collection-scoped indexes. Filtered or ordered collection-group queries may additionally need a field indexed at collection-group scope. When an index is missing:
  1. Firestore returns FAILED_PRECONDITION.
  2. The error includes a ready-made console link.
  3. The runner marks the failure as terminal and preserves the original message.
  4. The agent reports the blocker instead of repeatedly scanning parent documents.
Commit required indexes in firestore.indexes.json and deploy them through your normal Firebase workflow. A console-created index exists only in the selected project and can drift across environments.

Environment isolation

Deploy one runner per Polycore environment. Each process receives:
  • One Polycore project slug.
  • One enrolled environment.
  • One GCP project id.
  • One runtime service account scoped to that GCP project.
This prevents a single agent argument from switching a runner between production and non-production identities.

Optional: generic mutations

The query family is read-only. One-off document changes are covered by a separate built-in family, off by default: Every one carries the write hazard, so the control plane holds the request until a human approves that exact call with those exact arguments. Nothing here can run unattended. Enable with an explicit grant. Prefer true when the runtime service account already writes (the usual CIP/SAP shape: same Application Default Credentials customer actions use):
To keep the runtime SA viewer-only and inject a separate write identity, name the env var that holds a write-capable service-account key (only the name is committed):
"write": "ambient" uses the ambient ADC identity. That matches runners whose actions already write via ADC. Use { "keyEnv": "…" } when you want Layer 1 IAM (roles/datastore.viewer on the runtime SA) to stay intact for every read; a missing or malformed key then fails at boot rather than advertising a mutation the runner cannot perform.
Two properties keep an approval meaningful:
  • Targets are enumerated, never filtered. There is no delete-by-query: that set would only be resolved after approval, so nobody could review what they approved. To change many documents, read the paths with firestore.query (a Lane 1 read, no approval) and propose a firestore.batchWrite listing them.
  • Preconditions close the approval window. Pass either precondition.lastUpdateTime (the updateTime from a firestore.get) or precondition.exists, never both: Firestore treats them as mutually exclusive. Prefer lastUpdateTime so a write whose document changed while the approval was pending fails instead of overwriting.
  • Destructive writes have to say so. firestore.set requires an explicit mode. Firestore’s own default replaces the document, so a three-field payload can quietly drop everything else; "mode": "replace" puts that in the arguments the approver reads, next to merge and mergeFields.
Deleting a document does not delete its subcollections. The usual call is just { "path": "…" }; nested documents stay put, which is Firestore’s own default and fine for a normal delete. Pass recursive: true with maxDocuments only when you deliberately need to wipe the subtree: teams/acme looks the same in an approval card whether three documents or thirty thousand hang off it, so the runner counts first and refuses, having deleted nothing, if the bound the approver saw is exceeded. A multi-step or reusable write workflow is still better as a named action: a narrow input schema, application-level validation, and PR review of the code that runs.

Production validation

The production runner uses workload identity and has only the intended GCP project grants.
Document, collection, collection-group, and server-side aggregate reads work for the collections in scope.
A deliberate write through the generic query surface is unavailable.
With mutations enabled, a firestore.delete request produces an approval card and writes nothing until it is approved; with mutations disabled, the tool is not advertised at all.
Timestamp and reference filters use tagged values and return expected data.
Missing indexes fail once and preserve the actionable Firestore error.
Schema context contains no secrets and does not return the entire Typesync definition to every request.