Firestore Data Model and Operations

Quote

In document databases, the questions you need to answer should shape the document model and indexes.

Source: Financial Data Engineering.epub

Archived demo boundary

The original Firestore project used throughout this note, bq-wh-nb, has been removed. Treat every captured gcloud firestore output in this page as archived operator reference, not as a current environment snapshot.

This refresh intentionally skips live reruns. The note now keeps the former main database examples as historical context while preserving the command patterns and updating knowledge where current documentation or Cloud SDK help exposed meaningful changes.

Mental Model / Architecture Model

Firestore is most valuable when it acts as an operational data service close to application or pipeline control flow, while analytical or high-volume event history is pushed to more appropriate systems.


flowchart LR
    A["Cloud Run / Composer / Dataflow / services"] --> B["Firestore database<br/>low-latency operational state"]
    B --> C["Automatic single-field indexes<br/>composite indexes<br/>TTL policies"]
    B --> D["Backups / PITR / export to GCS"]
    B --> E["IAM for admin and server access<br/>Security Rules for direct mobile/web clients"]
    B --> F["BigQuery or Cloud Storage<br/>historical and analytical offload"]

The practical reading is simple:

  • Firestore is strong when the question is “what is the current state of this job, config, lease, run, or control-plane object?”
  • Firestore is weak when the question is “scan or aggregate a large history set cheaply” or “hold a replayable event stream with queue semantics.”

Core Concepts

This section builds the service model before the operational commands.

Firestore | service positioning | where Firestore fits in Google Cloud

Firestore is a low-latency document database for operational data, application state, and lightweight transactional workloads. It is not a warehouse, not a queue, and not an object store.

ServiceBest atWeak atTypical data-engineering use
FirestoreLow-latency document reads and writes, flexible schema, operational state, metadata, control-plane recordsLarge analytical scans, joins, queue semantics, very high sustained hot-key trafficPipeline checkpoints, job state, config registry, feature flags, run metadata, dedup tokens
BigQueryAnalytical scans, SQL, aggregations, historical reporting, cost-efficient large readsPer-row operational lookups, low-latency transactional stateRun history analytics, cost reporting, SLA trend analysis, audit exploration
Cloud SQL / AlloyDBRelational integrity, joins, SQL transactions, normalized schemasGlobal-scale low-ops document workloads, large fan-out real-time listenersTransactional control data with strict relational rules
Cloud StorageLarge blobs, files, immutable artifacts, cheap retentionMillisecond operational lookups or frequent tiny updatesExport archives, raw payloads, backup artifacts, replay manifests
Pub/SubAsynchronous delivery, buffering, replayable message retention, fan-outServing current operational state or document lookup workloadsEvent transport and backpressure boundary

For data engineers, Firestore usually sits beside the pipeline, not underneath the warehouse. It stores the current answer to operational questions while BigQuery stores history and Pub/Sub carries events between stages.

Firestore | operating modes | Native mode versus Datastore mode

Mode is decided at database creation time. It is a foundational platform decision, not a runtime toggle.

DimensionNative modeDatastore mode
Primary API surfaceFirestore APIs and Firestore client librariesDatastore APIs and Datastore client libraries
Data modelDocuments, collections, subcollectionsEntities, kinds, namespaces
Real-time capabilitiesAvailableNot available
Index modelFirestore indexesDatastore indexes
Recommended for new workloadsYesNo, except migration or legacy compatibility cases
Best fitOperational document databaseExisting Datastore-based systems

Mode is not a casual setup choice

A database’s mode is chosen when the database is created. The practical consequence is that API surface, client libraries, index behavior, and feature availability all follow from that initial choice.

Do not treat mode selection as a detail you can clean up later.

Default to Native mode for new work

Choose Native mode unless you are intentionally preserving Datastore compatibility. It is the correct baseline for Firestore-backed data-engineering control planes.

The archived demo database for this note was projects/bq-wh-nb/databases/main, and it was FIRESTORE_NATIVE at the time of capture.

Firestore | location strategy | latency, durability, and co-location

Firestore location is an architectural decision with direct consequences for latency, availability, disaster recovery, and cost. Google documents that regional databases replicate across at least three zones in one region, while multi-region databases replicate across five zones in three regions.

Location choiceBest whenTradeoff
RegionalPipeline compute is concentrated in one region and write latency mattersLower availability target than multi-region
Multi-regionFirestore is business-critical and must remain resilient across regional failureHigher cost and usually higher write latency

The archived demo database was regional in europe-west1. That remains a sensible choice when Cloud Run jobs, Composer environments, or other operational compute are also in Europe and lower write latency matters for control-plane state.

Firestore | data model | platform implications of document shape

Firestore alternates collection and document path segments. That shape is not only a modeling concern; it controls query patterns, delete workflows, and index requirements.

StructureWhat it isOperational implication
Root collectionTop-level collection under the databaseSimplest target for collection-scoped queries and admin commands
DocumentJSON-like record with typed fieldsMaximum size is 1 MiB; writes update the document and all relevant indexes
SubcollectionCollection nested under a documentParent deletion does not automatically delete child subcollections
Collection groupAll collections sharing the same nameUseful for fleet-wide admin actions and cross-parent queries

For operational metadata, a flat root collection is often the better default:

  • pipeline_runs/{run_id} is simpler for fleet-wide failed-run inspection, TTL policies, backup retention, and bulk deletes.
  • pipelines/{pipeline_id}/runs/{run_id} is reasonable only when parent-local grouping is more important than fleet-wide operations.

Firestore | limits and scaling | what constrains design

The official limits below matter because they drive schema shape, index design, and recovery strategy.

Limit or behaviorValueWhy it matters operationally
Maximum document size1 MiBRun history, checkpoints, or config blobs must stay compact; large payloads belong in Cloud Storage
Maximum field depth in maps and arrays20Deeply nested control metadata becomes fragile and hard to query
Maximum API request size10 MiBLarge transactional or batch-style mutations hit request limits quickly
Transaction time limit270 seconds with 60-second idle expirationLong-running orchestration logic should not hold Firestore transactions open
Maximum composite indexes1000 with billing enabledIndex sprawl is real; version-control query shapes
Maximum single-field configurations1000 with billing enabledTTL and index exemptions both consume this budget
Maximum index entries per document40,000Large arrays and many indexed fields can make one document expensive to write
Maximum indexed field value size1500 bytes before truncationLong strings are dangerous query keys and poor index candidates
Export and import request rate20 per minute per projectBulk admin workflows need pacing and scheduling

Hotspots are the next operational boundary. Firestore documentation recommends the 500/50/5 rule for warming up new collections or narrow key ranges: start around 500 operations per second and then increase by 50% every five minutes. Sequential document IDs, sequential indexed timestamps, and aggressive writes to one document all work against that guidance.

Firestore | indexes, TTL, and cost | why write patterns matter

Firestore automatically indexes fields unless you exempt them. That default is convenient for development, but it makes write cost and write latency a function of document shape.

MechanismGood forRisk if unmanaged
Automatic single-field indexesFast iteration and simple filtersIndex fanout on fields you never query
Composite indexesKnown production query shapesOperational drift when query shape changes but index deployment does not
Single-field exemptionsHigh-write timestamp fields, large strings, large arrays, TTL fieldsMissing exemptions increase write latency and storage cost
TTL policyCleanup of operational historyTeams expect immediate deletion, but TTL is asynchronous

Firestore best practices explicitly call out TTL fields as a common exemption target: the TTL field must be a timestamp, indexing is enabled by default, and that indexing can hurt performance at higher write rates if the field is never queried.

Firestore | security boundary | IAM versus Security Rules

You must separate server-side control from client-side control.

Control planePrimary mechanismUse it forDo not use it for
Admin and server accessIAMgcloud, Terraform, backups, restore, exports, Cloud Run services, Dataflow jobs, Composer workersDirect mobile or web client authorization
Direct mobile and web clientsSecurity RulesEnforcing document-level access patterns for untrusted client SDKsReplacing IAM for server jobs or admin tooling

The operator rule is simple:

  • If the caller is a server workload or administrator, use IAM and service accounts.
  • If the caller is a browser or mobile client talking directly to Firestore, use Security Rules.

Mixing the two leads to false assumptions, especially in incident response. A broken IAM binding blocks admin commands. A broken Security Rule blocks direct client access. They are not the same failure domain.

Operational Commands And Workflows

All commands below should now be read as operator patterns. The original walkthrough used the removed project bq-wh-nb and the named database main, so embedded outputs are archived reference material rather than current validation. State-changing commands still include pre-check guidance and verification steps, but they were not rerun in this refresh.

PowerShell / Linux | gcloud | confirm the active project and database context

These commands answer the first operational question: “Which identity, project, and database will this command actually touch?”

Before any Firestore admin command, especially in a shared workstation or Cloud Shell session. It is typically triggered by you open a new shell, switch configurations, or are about to make a state change. gcloud CLI, read-only. Requires an authenticated account but does not mutate any resource. Confirm which identity will authorize Firestore admin actions.

Print the active gcloud account that will execute Firestore admin commands.

gcloud auth list --filter=status:ACTIVE --format="value(account)"
alexper.recovery@gmail.com

Immediately after confirming the account and before using any gcloud firestore command. It is typically triggered by you suspect a configuration switch, or a command could hit the wrong project. gcloud CLI, read-only. Confirm which Google Cloud project is the default target for the current shell session.

Print the active Google Cloud project from the current gcloud configuration.

gcloud config get-value project
bq-wh-nb

List Firestore databases in the active project

Before any database-scoped admin action. It is typically triggered by you need to know whether the project uses '(default)' or a named database, or you want to verify location and delete protection state. gcloud CLI, read-only. Requires metadata access to the project. Show which Firestore databases exist in the active project so later commands target the correct database ID.

FieldSource columnTypeMeaning
namenamestringFully qualified resource name of the Firestore database
typetypeenumDatabase mode, such as FIRESTORE_NATIVE
locationIdlocationIdstringRegion or multi-region where the database lives
deleteProtectionStatedeleteProtectionStateenumWhether database deletion is blocked
pointInTimeRecoveryEnablementpointInTimeRecoveryEnablementenumWhether PITR is enabled

List Firestore databases in the active project and surface the settings that change operational risk.

gcloud firestore databases list \
  --format="table(name,type,locationId,deleteProtectionState,pointInTimeRecoveryEnablement)"
NAMETYPELOCATION_IDDELETE_PROTECTION_STATEPOINT_IN_TIME_RECOVERY_ENABLEMENT
projects/bq-wh-nb/databases/mainFIRESTORE_NATIVEeurope-west1DELETE_PROTECTION_DISABLEDPOINT_IN_TIME_RECOVERY_DISABLED

The important archived result is that this project did not use '(default)'. Any command aimed at '(default)' would fail with NOT_FOUND until retargeted to the named database.

Describe the current Firestore database configuration

After listing databases and before changing delete protection, PITR, or location-dependent integrations. It is typically triggered by you need to verify the current configuration of the exact database you are about to touch. gcloud CLI, read-only. Retrieve the current platform settings that govern recovery window, real-time behavior, and destructive-risk posture.

FieldSource columnTypeMeaning
namenamestringFully qualified database resource name
locationIdlocationIdstringDatabase location
typetypeenumNative mode or Datastore mode
databaseEditiondatabaseEditionenumStandard or Enterprise
deleteProtectionStatedeleteProtectionStateenumWhether database deletion is blocked
pointInTimeRecoveryEnablementpointInTimeRecoveryEnablementenumWhether PITR is enabled
realtimeUpdatesModerealtimeUpdatesModeenumWhether real-time updates are enabled
versionRetentionPeriodversionRetentionPerioddurationRecovery-read retention window supported by the database

Describe the archived main Firestore database configuration captured for this note.

gcloud firestore databases describe \
  --database='main' \
  --format="table(name,locationId,type,databaseEdition,deleteProtectionState,pointInTimeRecoveryEnablement,realtimeUpdatesMode,versionRetentionPeriod)"
NAMELOCATION_IDTYPEDATABASE_EDITIONDELETE_PROTECTION_STATEPOINT_IN_TIME_RECOVERY_ENABLEMENTREALTIME_UPDATES_MODEVERSION_RETENTION_PERIOD
projects/bq-wh-nb/databases/maineurope-west1FIRESTORE_NATIVESTANDARDDELETE_PROTECTION_DISABLEDPOINT_IN_TIME_RECOVERY_DISABLEDREALTIME_UPDATES_MODE_ENABLED3600s

The archived reading is still operationally useful:

  • main was a Standard edition Native-mode database.
  • Delete protection was disabled.
  • PITR was disabled.
  • Real-time updates were enabled.
  • With PITR disabled, the version retention period was 3600s, not seven days.
FlagSyntaxDescription
--filtergcloud auth list --filter=status:ACTIVELimits gcloud auth list output to the active identity
--format--format="table(...)"Controls output shape so the command is readable and script-safe
--database--database='main'Targets a named Firestore database instead of the default database

PowerShell / Linux | gcloud | create and update database settings

These commands are state-changing. Run the read-only inspection commands above first, and do not create or reconfigure a database until you have confirmed the active account, active project, intended database ID, and intended location.

Create a new Firestore database

During initial environment provisioning or when intentionally adding a separate database for testing, regional isolation, or customer separation. It is typically triggered by A new environment needs its own Firestore database, or the project currently lacks the intended database. gcloud CLI, state-changing. Requires roles/datastore.owner. Database location and mode are foundational choices. Create a new Firestore database with explicit mode, location, and deletion-risk posture instead of relying on console defaults.

Creation choices are architectural

Before running the create command, verify:

  • The active project is correct.
  • The database ID is new and intentionally named.
  • The location matches the compute region strategy.
  • You want Native mode rather than Datastore mode.
  • You understand whether PITR and delete protection should be enabled from day one.

Use an explicit create command

Create databases with all critical settings visible in the command. Silent defaults make reviews and incident reconstruction harder later.

Enterprise edition access mode

Current Cloud SDK releases expose additional creation-time switches for Enterprise edition databases, including --enable-firestore-data-access, --enable-mongodb-compatible-data-access, and --enable-realtime-updates.

Those flags matter when you are creating an Enterprise database with an explicit API access model. They are not required for the Standard Native-mode pattern shown below, but they are important if you standardize on Enterprise mode in newer environments.

Create a new Native-mode Firestore database in the active project with delete protection enabled at creation time.

gcloud firestore databases create \
  --project='bq-wh-nb' \
  --database='metadata-eu' \
  --location='europe-west1' \
  --edition=standard \
  --type=firestore-native \
  --delete-protection

If you run this command, verify the result with gcloud firestore databases describe --database='metadata-eu'. If the database is meant for production control-plane data, decide whether PITR should also be enabled immediately.

Enable delete protection or PITR on an existing database

After provisioning if these protections were omitted, or during a hardening pass before production use. It is typically triggered by the describe output shows DELETE_PROTECTION_DISABLED or POINT_IN_TIME_RECOVERY_DISABLED. gcloud CLI, state-changing. Requires database update permission. Raise the safety baseline of an existing database without recreating it.

Do not confuse protection scopes

Delete protection blocks database deletion. PITR improves recovery from document-level accidental change. Neither setting replaces scheduled backups for longer retention.

Turn on the protections deliberately

Treat delete protection, PITR, and scheduled backups as separate controls. Use all three when the database holds production metadata you cannot cheaply reconstruct.

Enable delete protection on the current main database.

gcloud firestore databases update \
  --database='main' \
  --delete-protection

Enable PITR on the current main database.

gcloud firestore databases update \
  --database='main' \
  --enable-pitr

After either command, re-run gcloud firestore databases describe --database='main' and confirm the target field changed to ENABLED.

FlagSyntaxDescription
--project--project='bq-wh-nb'Forces the command to the intended Google Cloud project
--database--database='metadata-eu'Names the database being created or updated
--location--location='europe-west1'Sets the region or multi-region at creation time
--edition--edition=standardChooses Standard or Enterprise edition
--type--type=firestore-nativeChooses Native mode or Datastore mode
--delete-protection--delete-protectionEnables protection against database deletion
--enable-pitr--enable-pitrEnables seven-day point-in-time recovery

PowerShell / Linux | gcloud | inspect indexes and TTL policies

Index and TTL configuration are infrastructure, not application trivia. They change write latency, storage cost, cleanup behavior, and failure modes.

List composite indexes in the current database

Before deploying a new query shape, after a failed precondition error, or during environment drift inspection. It is typically triggered by A planned query needs a composite index, or an environment behaves differently than another. gcloud CLI, read-only. Requires metadata access to the database. Show which composite indexes already exist and which collection groups they govern.

FieldSource columnTypeMeaning
namenamestringFull resource name of the index, including collection group and index ID
queryScopequeryScopeenumWhether the index applies to a collection or collection group
statestateenumBuild status such as READY

List composite indexes from the archived main database snapshot used in this note.

gcloud firestore indexes composite list \
  --database='main' \
  --format=json
Index resourceCollection groupQuery scopeIndexed fieldsState
projects/bq-wh-nb/databases/main/collectionGroups/prices/indexes/CICAgJim14AKpricesCOLLECTION_GROUPdate ASC, close DESC, __name__ DESCREADY
projects/bq-wh-nb/databases/main/collectionGroups/stocks/indexes/CICAgOjXh4EKstocksCOLLECTIONcountry ASC, current_price ASC, __name__ ASCREADY
projects/bq-wh-nb/databases/main/collectionGroups/pipeline_runs/indexes/CICAgJiUsZIKpipeline_runsCOLLECTIONstatus ASC, started_at DESC, __name__ DESCREADY

The practical lesson from the archived environment is that one important operational query shape had already been versioned: failed or status-filtered pipeline runs ordered by start time.

Inspect TTL fields in the current database

When verifying cleanup behavior or checking whether old operational records should already be disappearing. It is typically triggered by historical control-plane data is growing, or the team believes TTL is configured but documents remain visible. gcloud CLI, read-only. Show which collection-group fields are configured as TTL expiry fields.

FieldSource columnTypeMeaning
namenamestringFully qualified field resource name
ttlConfig.statettlConfig.stateenumWhether TTL is active for the field
indexConfig.usesAncestorConfigindexConfig.usesAncestorConfigbooleanWhether the field still inherits default single-field indexing behavior

List TTL-enabled fields from the archived main database snapshot used in this note.

gcloud firestore fields ttls list \
  --database='main' \
  --format="table(name,ttlConfig.state,indexConfig.usesAncestorConfig)"
NAMESTATEUSES_ANCESTOR_CONFIG
projects/bq-wh-nb/databases/main/collectionGroups/pipeline_runs/fields/expires_atACTIVETrue

This archived output shows that pipeline_runs.expires_at was configured as the TTL field. The field still inherited ancestor index behavior, which meant it remained indexed unless a single-field exemption was added.

Create a composite index safely

After a query design review has identified a stable production query shape that needs explicit index support. It is typically triggered by A Firestore query fails with FAILED_PRECONDITION, or a new operational query is being promoted into production. gcloud CLI, state-changing. Index builds are asynchronous and may take time. Create the exact composite index that a known query shape needs.

Create only stable indexes

Every composite index increases storage, write amplification, and admin surface area. Do not create indexes for exploratory one-off queries in a production database.

Version-control query shapes

Treat a composite index as part of the deployment contract for a production query. Add it intentionally and keep the query and the index definition together in reviewable infrastructure code.

Create the status ASC + started_at DESC index for pipeline_runs in the main database.

gcloud firestore indexes composite create \
  --database='main' \
  --collection-group='pipeline_runs' \
  --field-config=field-path=status,order=ascending \
  --field-config=field-path=started_at,order=descending

Verify completion with gcloud firestore indexes composite list --database='main' --format=json and confirm the new index reaches READY.

Enable TTL and exempt the expiry field from unnecessary indexing

When a collection group contains short-lived operational data such as run history, replay manifests, dedup tokens, or checkpoints. It is typically triggered by storage is growing without bound, or a timestamp field exists purely for expiry and not for query filtering. gcloud CLI, state-changing. These are separate operations: one configures TTL, the other changes indexing behavior. Automate cleanup while reducing avoidable write fanout on a sequential expiry timestamp field.

TTL is not immediate deletion

Firestore documents with expired TTL fields continue to appear until the asynchronous TTL service deletes them.

Use TTL for lifecycle management, not for second-by-second enforcement or security guarantees.

Pair TTL with a field-exemption review

If the expiry timestamp is not used in queries, exempt it from indexing so it stops contributing to write fanout.

Enable expires_at as the TTL field for the pipeline_runs collection group.

gcloud firestore fields ttls update expires_at \
  --database='main' \
  --collection-group='pipeline_runs' \
  --enable-ttl

Disable default indexing on the same high-write expiry field if your application never filters on it.

gcloud firestore indexes fields update expires_at \
  --database='main' \
  --collection-group='pipeline_runs' \
  --disable-indexes

After changing either setting, re-run the TTL inspection command and gcloud firestore indexes fields list --database='main' to confirm the field now reflects the intended TTL and indexing posture.

FlagSyntaxDescription
--database--database='main'Targets the named Firestore database
--collection-group--collection-group='pipeline_runs'Chooses the collection group or repeated nested collection name
--field-config--field-config=field-path=status,order=ascendingDefines one field inside a composite index
--query-scope--query-scope=collection-groupChanges index scope when needed
--enable-ttl--enable-ttlMarks the field as the TTL expiry field
--disable-indexes--disable-indexesExempts a field from default single-field indexing

PowerShell / Linux | gcloud | back up, export, import, and restore data

Recovery workflows are where teams most often discover they never verified the real database name, real location, or real recovery control posture.

List backup schedules for the current database

During a hardening review, before a maintenance window, or after onboarding a new environment. It is typically triggered by you need to confirm whether automated backups exist at all. gcloud CLI, read-only. Show whether the database currently has scheduled backups configured.

List backup schedules configured for the archived main database snapshot used in this note.

gcloud firestore backups schedules list \
  --database='main' \
  --format=json
[]

At the time of capture, there were no scheduled backups for main.

List existing backups in the current Firestore region

Before planning restore, clone, or disaster-recovery exercises. It is typically triggered by you need to know whether recoverable backup artifacts already exist in the database location. gcloud CLI, read-only. Show which backup artifacts exist in the database’s region.

List Firestore backups in europe-west1, the archived database location used in this note.

gcloud firestore backups list \
  --location='europe-west1' \
  --format=json
[]

At the time of capture, the project had no managed Firestore backups in europe-west1.

Create a weekly backup schedule

Before placing the database into production or before accepting that historical operational metadata must be recoverable beyond the PITR window. It is typically triggered by A hardening review shows no backup schedule is present. gcloud CLI, state-changing. Requires backup schedule permissions on the database. Add a managed backup policy with explicit cadence and retention.

Backups and PITR solve different problems

PITR supports recent surgical recovery. Scheduled backups support longer retention and restore to a new database.

One is not a substitute for the other.

Set retention deliberately

Choose a retention period that matches operational replay, audit, or rollback needs. Firestore scheduled backups support retention up to 14 weeks.

Create a weekly Sunday backup schedule with 28-day retention for the main database.

gcloud firestore backups schedules create \
  --database='main' \
  --retention=28d \
  --recurrence=weekly \
  --day-of-week=SUN

Verify with gcloud firestore backups schedules list --database='main'.

Export collection groups to Cloud Storage

Before major data migrations, before destructive cleanup, or when offloading history for analytics or offline processing. It is typically triggered by you need a portable snapshot in Cloud Storage or want to load a Firestore export into BigQuery. gcloud CLI, state-changing. Requires billing, a writable Cloud Storage bucket, and Firestore plus Storage permissions. Produce a managed export without reading documents through an SDK.

Export location must be real

Do not invent a bucket name and run the command blind. First verify the bucket exists, is writable from the project, and is located near the Firestore database.

Export narrowly when possible

Use --collection-ids for operational datasets such as pipeline_runs, config, or checkpoints so export scope, cost, and later import blast radius stay controlled.

Export only the pipeline_runs collection group from the archived main database pattern to a verified Cloud Storage prefix.

gcloud firestore export 'gs://YOUR_EXISTING_BUCKET/firestore/main/pipeline-runs-2026-04-13' \
  --database='main' \
  --collection-ids='pipeline_runs'

If you need the export for BigQuery loading, prefer narrow collection-group exports. Firestore documentation explicitly states that Firestore exports can be loaded into BigQuery.

Import from a managed export prefix

During controlled restore or migration workflows where the source is an existing Firestore managed export. It is typically triggered by you have a known-good export prefix and need to load it into an existing target database. gcloud CLI, state-changing. Import overwrites documents with matching IDs that already exist in the target database. Load data from a managed export back into Firestore without writing custom loader code.

Import can overwrite live documents

Firestore import preserves document IDs. If a document with the same ID already exists, the import overwrites it.

Never run an import into a production database until you have verified the source export, target database, and collision consequences.

Prefer restore to a new database when possible

If the goal is investigation or validation, restore into a new database first. That isolates the blast radius and lets you compare states safely.

Import only the pipeline_runs collection group from a known managed export prefix into main.

gcloud firestore import 'gs://YOUR_EXISTING_BUCKET/firestore/main/pipeline-runs-2026-04-13' \
  --database='main' \
  --collection-ids='pipeline_runs'

After import, verify critical document counts and document IDs before allowing applications to rely on the result.

Restore a new database from a backup

During disaster recovery, restore testing, or forensic comparison against a backup artifact. It is typically triggered by A known backup exists and the team needs a safe recovery target. gcloud CLI, state-changing. Restore creates or targets a destination database in the same location as the source backup. Recover data into a separate database instead of disturbing the current primary database.

Restore workflow changes which database applications should target

Restoring data is only half of the incident. The other half is deciding whether applications should be repointed, dual-read, or kept on the original database while you validate the restore.

Restore into a separate destination first

Make the restore database explicit, validate it, and only then decide whether traffic should move.

Restore a backup into a new database named main-restore-20260413.

gcloud firestore databases restore \
  --source-backup='projects/bq-wh-nb/locations/europe-west1/backups/BACKUP_ID' \
  --destination-database='main-restore-20260413'

After restore, use gcloud firestore databases describe --database='main-restore-20260413' and application-level validation before any cutover decision.

FlagSyntaxDescription
--database--database='main'Targets the database being backed up, exported, or imported
--retention--retention=28dKeeps backups for the specified period
--recurrence--recurrence=weeklyChooses daily or weekly schedule cadence
--day-of-week--day-of-week=SUNSets the weekly backup day in UTC
--collection-ids--collection-ids='pipeline_runs'Restricts export or import to selected collection groups
--location--location='europe-west1'Targets the backup location
--source-backup--source-backup='projects/.../backups/BACKUP_ID'Identifies the backup artifact to restore from
--destination-database--destination-database='main-restore-20260413'Names the destination database for restore

Terraform | google provider | manage Firestore as code

Terraform is where Firestore stops being an ad-hoc console service and becomes a reviewable platform component.

Declare the Firestore database

Define database identity, location, delete protection, and PITR in version control so environment creation is reproducible.

Declare a production-style Native-mode Firestore database with PITR and delete protection enabled.

resource "google_firestore_database" "firestore_main" {
  project     = var.project_id
  name        = "main"
  location_id = "europe-west1"
  type        = "FIRESTORE_NATIVE"
 
  concurrency_mode                  = "PESSIMISTIC"
  app_engine_integration_mode       = "DISABLED"
  point_in_time_recovery_enablement = "POINT_IN_TIME_RECOVERY_ENABLED"
  delete_protection_state           = "DELETE_PROTECTION_ENABLED"
  deletion_policy                   = "ABANDON"
}

deletion_policy = "ABANDON" is a strong default for production because it keeps terraform destroy from deleting the live database. If you want Terraform to be able to delete the database intentionally, use DELETE only with strong review controls.

Declare composite indexes and field settings

Indexes and field exemptions are part of production query design and write-path performance.

Declare the pipeline_runs composite index and disable indexing on the expires_at field while keeping TTL on the same field.

resource "google_firestore_index" "pipeline_runs_status_started_at" {
  project    = var.project_id
  database   = google_firestore_database.firestore_main.name
  collection = "pipeline_runs"
 
  fields {
    field_path = "status"
    order      = "ASCENDING"
  }
 
  fields {
    field_path = "started_at"
    order      = "DESCENDING"
  }
}
 
resource "google_firestore_field" "pipeline_runs_expires_at" {
  project    = var.project_id
  database   = google_firestore_database.firestore_main.name
  collection = "pipeline_runs"
  field      = "expires_at"
 
  ttl_config {}
 
  index_config {
    indexes = []
  }
}

This pattern matters because TTL and indexing are not separate design domains. The same timestamp field can drive retention and still be a poor index candidate for a high-write collection.

Declare backup schedules

Backups should not depend on someone remembering to click a console button.

Create a weekly Firestore backup schedule with 28-day retention.

resource "google_firestore_backup_schedule" "weekly_main_backup" {
  project   = var.project_id
  database  = google_firestore_database.firestore_main.name
  retention = "2419200s"
 
  weekly_recurrence {
    day = "SUNDAY"
  }
}

The Google provider also supports daily recurrence. Keep backup cadence aligned with data-change frequency and incident recovery expectations.

Warnings, Limitations, And Anti-Patterns

These are the mistakes that turn a useful operational store into a source of cost or reliability pain.

Firestore | anti-patterns | where teams usually get hurt

Anti-patternWhy it is harmfulBetter pattern
Using Firestore as the analytical system of recordReads are billed per document and Firestore is not optimized for large scans or joinsOffload history to BigQuery or export to Cloud Storage
Treating Firestore like a queueFirestore does not provide queue semantics, consumer groups, or durable replay like Pub/SubUse Pub/Sub for transport and Firestore for current state
Using sequential document IDs or high-write sequential indexed timestampsCauses hotspotting and write contentionUse scattered IDs and exempt non-query timestamp fields from indexing
Storing large raw payloads or logs inside documentsHits the 1 MiB document limit and inflates write costStore blobs in Cloud Storage and keep references in Firestore
Assuming parent deletion removes subcollectionsChild collections survive parent deletionDesign explicit cleanup paths or use collection-group cleanup workflows
Leaving delete protection, PITR, and backups disabled in productionMakes a single operator mistake disproportionately expensiveTurn on the controls intentionally and verify them periodically

Firestore | lifecycle caveats | what does not happen automatically

Deletion is rarely one-step

Several common assumptions are wrong:

  • Deleting a parent document does not delete its subcollections.
  • TTL deletion is asynchronous, not immediate.
  • Import can overwrite existing documents with matching IDs.
  • Bulk delete does not delete documents added or modified after the operation starts.

Design explicit lifecycle workflows

Put lifecycle policy in the model:

  • Add expires_at to short-lived data.
  • Use collection-group-aware cleanup.
  • Keep exports or backups before destructive operations.
  • Validate restore targets before cutover.

Recommendations And Best Practices

The defaults below are what keep Firestore useful for data-engineering control planes instead of letting it drift into accidental complexity.

Firestore | production defaults | decisions worth making early

RecommendationWhy it matters
Prefer Native mode for new workloadsIt keeps you on the Firestore platform and feature set rather than legacy Datastore behavior
Choose the location deliberately with compute co-location in mindCross-region hops add latency and failure surface
Turn on delete protection for production databasesIt blocks catastrophic database deletion mistakes
Turn on PITR when the metadata cannot be cheaply reconstructedIt shortens recovery from accidental change
Add backup schedules for longer retentionPITR is not a long-term retention substitute
Keep documents compact and purpose-builtSmaller documents reduce write cost, latency, and schema confusion
Exempt non-query sequential timestamp fields from indexingIt reduces write fanout and avoids the 500 writes per second sequential-index limit
Treat indexes as deployable infrastructureEnvironment drift here shows up as runtime query failures
Add schema_version, updated_at, and owner fields to operational documentsThey make migrations and incident response less ambiguous

Data Engineering Scenarios

The patterns below are Firestore use cases that fit the service well from a platform point of view.

Firestore | control-plane records | checkpoints, cursors, and config

These are low-cardinality, frequently read, operational records that represent current truth rather than historical analytics.

Use caseGood Firestore shapeWhy Firestore fits
Job cursor / watermarkpipeline_checkpoints/{pipeline_name}One current document per pipeline is cheap to read and easy to update atomically
Runtime config registryconfig/{pipeline_name}Operators can update one document and services can reread it cheaply
Feature flag or kill switchfeature_flags/{flag_name}Low-latency reads and simple operational toggles
Lease or lock metadataleases/{resource_id}Small document with ownership and expiry fields

Example checkpoint document for a pipeline watermark or replay cursor.

{
  "schema_version": 1,
  "pipeline": "daily-stocks-ingest",
  "cursor_type": "event_time",
  "cursor_value": "2026-04-13T11:00:00Z",
  "updated_at": "2026-04-13T11:03:12Z",
  "updated_by": "cloud-run-job/daily-stocks-ingest",
  "owner": "data-platform"
}

Firestore | operational history | run metadata and incident breadcrumbs

Run metadata is a good Firestore workload when you need current and recent operational visibility, not warehouse-style analytics over large history.

FieldWhy it should exist
statusSupports current-state dashboards and operational filters
started_at / finished_atSupports ordering and incident timeline reconstruction
expires_atSupports TTL-driven cleanup of old run metadata
attemptMakes retries explicit
last_errorKeeps the most useful failure context close to the run record
schema_versionProtects you from silent shape drift over time

Example run metadata document for a Firestore-backed pipeline control plane.

{
  "schema_version": 2,
  "pipeline": "daily-stocks-ingest",
  "status": "failed",
  "attempt": 3,
  "started_at": "2026-04-13T10:00:00Z",
  "finished_at": "2026-04-13T10:04:51Z",
  "last_error": {
    "class": "DeadlineExceeded",
    "stage": "score-gold",
    "message": "BigQuery job exceeded configured timeout"
  },
  "expires_at": "2026-05-13T10:04:51Z",
  "owner": "data-platform"
}

Firestore | archive boundary | when to offload instead of retaining forever

Firestore should keep recent operational truth and bounded operational history. It should not become the forever-home for run history, detailed logs, or replayable event bodies.

Keep in FirestoreOffload elsewhere
Current configurationHistorical config change audit at scale
Latest checkpointFull replay archive
Recent run metadata with TTLLong retention SLA trend analysis in BigQuery
Lightweight incident breadcrumbLarge error payloads or stack traces in Cloud Storage or logging systems

Troubleshooting And Common Failures

Troubleshooting starts with confirming the exact database name and current control posture. Many Firestore incidents are actually targeting mistakes or missing control-plane resources.

Firestore | failure modes | symptoms, confirmation, and action

SymptomLikely causeHow to confirmAction
NOT_FOUND when listing indexes on '(default)'The project uses a named databasegcloud firestore databases listRetarget commands to --database='main' or the correct database name
Query fails with FAILED_PRECONDITIONMissing composite indexReview error details and gcloud firestore indexes composite listCreate the index and wait for READY
Old run documents still appear after expiryTTL is configured but asynchronousgcloud firestore fields ttls list and inspect expires_atWait for TTL processing or use controlled bulk delete for urgent cleanup
Restore plan fails because there are no backupsBackup schedule was never configuredgcloud firestore backups schedules list and gcloud firestore backups listAdd backup schedules and stop assuming PITR or exports already exist
PITR recovery is unavailablePITR is disabledgcloud firestore databases describe --database='main'Enable PITR for the future; use export or backup strategy for current recovery
High write latency or contentionHotspotting or index fanoutCheck document ID pattern, indexed sequential fields, and collection write shapeScatter IDs, exempt fields, and ramp traffic with 500/50/5 guidance
Bulk delete finishes but some documents remainDocuments were added or modified after the operation beganCheck operation timing and remaining documentsRe-run cleanup or use a better retention pattern such as TTL

Decision Matrix Or When-To-Use Guidance

Use this matrix when deciding whether Firestore should own a platform concern.

QuestionChoose Firestore when…Choose something else when…
Do you need the current value quickly?You need sub-second operational reads of small documentsYou need large scans, joins, or analytical aggregation
Is the data naturally document-shaped?Each control-plane record fits cleanly into one bounded documentThe model needs relational integrity across many tables
Is retention intentionally bounded?You can TTL or archive old historyYou need cheap long-term retention and broad queryability
Is query shape known ahead of time?A small set of operational queries can be indexed intentionallyAd-hoc exploration across large history is the norm
Is this transport or state?It is state, metadata, config, or lightweight event recordIt is transport, replay, fan-out, or queueing

Quick Reference

The table below preserves the archived demo environment captured on 2026-04-13. It is historical reference only because the original project has been removed.

ItemCurrent valueOperational implication
Active projectbq-wh-nbAll default gcloud commands hit this project unless overridden
Active accountalexper.recovery@gmail.comThis identity is authorizing Firestore admin commands
Firestore databasemainThe project does not use '(default)'
Database locationeurope-west1Region choice should match compute placement and Eventarc trigger region
Database modeFIRESTORE_NATIVENative-mode operational guidance applies
EditionSTANDARDStandard edition features and limits apply
Delete protectionDISABLEDDatabase deletion is not currently guarded
PITRDISABLEDRecovery window is not the seven-day PITR window
TTL fieldpipeline_runs.expires_atRecent run history is configured for TTL cleanup
Backup schedulesnoneThe database currently has no scheduled managed backups
Existing backupsnoneRestore from managed backup is not currently available

The shortest safe admin checklist is:

  1. Confirm gcloud auth list --filter=status:ACTIVE.
  2. Confirm gcloud config get-value project.
  3. Confirm gcloud firestore databases list.
  4. Confirm gcloud firestore databases describe --database='main'.
  5. Only then run a state-changing command.