Firestore
floci-gcp emulates Google Cloud Firestore over gRPC using the real google.firestore.v1 protocol.
Configuration
| Variable | Default | Description |
|---|---|---|
FLOCI_GCP_SERVICES_FIRESTORE_ENABLED |
true |
Enable/disable Firestore |
Emulator Variable
The GCP Firestore SDK uses this variable to route requests to floci-gcp instead of firestore.googleapis.com.
Quick Start
FirestoreOptions options = FirestoreOptions.newBuilder()
.setHost("localhost:4588")
.setProjectId("floci-local")
.setCredentials(NoCredentials.getInstance())
.build();
Firestore db = options.getService();
// Add a document
Map<String, Object> user = new HashMap<>();
user.put("name", "Alice");
user.put("age", 30);
db.collection("users").add(user).get();
// Query documents
ApiFuture<QuerySnapshot> future = db.collection("users")
.whereEqualTo("name", "Alice")
.get();
QuerySnapshot snapshot = future.get();
snapshot.getDocuments().forEach(doc ->
System.out.println(doc.getData()));
import os
os.environ["FIRESTORE_EMULATOR_HOST"] = "localhost:4588"
from google.cloud import firestore
db = firestore.Client(project="floci-local")
# Add a document
db.collection("users").add({"name": "Alice", "age": 30})
# Query documents
docs = db.collection("users").where("name", "==", "Alice").stream()
for doc in docs:
print(doc.to_dict())
# Set a document
db.collection("users").document("alice").set({"name": "Alice", "age": 30})
# Get a document
doc = db.collection("users").document("alice").get()
print(doc.to_dict())
process.env.FIRESTORE_EMULATOR_HOST = "localhost:4588";
import { Firestore } from "@google-cloud/firestore";
const db = new Firestore({ projectId: "floci-local" });
// Add a document
await db.collection("users").add({ name: "Alice", age: 30 });
// Query documents
const snapshot = await db.collection("users")
.where("name", "==", "Alice")
.get();
snapshot.forEach(doc => console.log(doc.data()));
// Set a document
await db.collection("users").doc("alice").set({ name: "Alice", age: 30 });
Transactions
db.runTransaction(transaction -> {
DocumentReference docRef = db.collection("counters").document("visits");
DocumentSnapshot snapshot = transaction.get(docRef).get();
long currentCount = snapshot.exists() ? snapshot.getLong("count") : 0;
transaction.set(docRef, Map.of("count", currentCount + 1));
return null;
}).get();
Transactions use optimistic concurrency, matching real Firestore: every document
read inside a transaction is tracked, and the commit fails with ABORTED if any
of those documents changed after being read. SDK clients retry aborted
transactions automatically, so concurrent increments like the example above never
lose updates.
Preconditions
Write preconditions (currentDocument) are enforced on all write paths:
create()fails withALREADY_EXISTSif the document existsupdate()fails withNOT_FOUNDif the document does not existPrecondition.updatedAt(...)fails withFAILED_PRECONDITIONif the document's update time no longer matches
Commit is atomic: if any write's precondition fails, no writes in the request
are applied. BatchWrite is non-atomic and reports a per-write status, matching
real Firestore.
Batch Writes
WriteBatch batch = db.batch();
DocumentReference ref1 = db.collection("users").document("alice");
DocumentReference ref2 = db.collection("users").document("bob");
batch.set(ref1, Map.of("name", "Alice"));
batch.set(ref2, Map.of("name", "Bob"));
batch.commit().get();
Real-time Listeners
DocumentReference docRef = db.collection("users").document("alice");
ListenerRegistration registration = docRef.addSnapshotListener((snapshot, e) -> {
if (snapshot != null && snapshot.exists()) {
System.out.println("Current data: " + snapshot.getData());
}
});
// Later: stop listening
registration.remove();
Supported Operations
GetDocumentCreateDocumentUpdateDocumentDeleteDocumentListDocumentsBatchGetDocumentsBatchWriteBeginTransactionCommitRollbackRunQueryRunAggregationQueryPartitionQueryWrite(streaming)Listen(real-time change streams)ListCollectionIds
Deviations from real Firestore
- Transaction conflict detection covers documents actually read. Queries inside a transaction track only the documents they return, so phantom reads (a concurrent write creating a document that would have matched the query) do not abort the transaction.
RunAggregationQueryignorestransactionandnewTransaction; aggregations read outside the transaction and are not part of its read set.- Transactions expire after 15 minutes instead of Firestore's shorter server-side deadlines.
- Transaction state is held in memory; transactions do not survive an emulator restart. A commit against an unknown transaction id is applied without conflict validation.