From 1890f07e9965935597e7fc7023c197a5191815e1 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Wed, 7 Jan 2026 15:02:25 -0800 Subject: [PATCH 1/7] First draft of firestore-basics skill --- skills/firestore-basics/SKILL.md | 20 +++++ .../references/provisioning.md | 38 +++++++++ .../firestore-basics/references/sdk_usage.md | 77 +++++++++++++++++++ .../references/security_rules.md | 58 ++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 skills/firestore-basics/SKILL.md create mode 100644 skills/firestore-basics/references/provisioning.md create mode 100644 skills/firestore-basics/references/sdk_usage.md create mode 100644 skills/firestore-basics/references/security_rules.md diff --git a/skills/firestore-basics/SKILL.md b/skills/firestore-basics/SKILL.md new file mode 100644 index 00000000000..73c166cf35f --- /dev/null +++ b/skills/firestore-basics/SKILL.md @@ -0,0 +1,20 @@ +--- +name: firestore-basics +description: Comprehensive guide for Firestore basics including provisioning, security rules, and SDK usage. Use this skill when the user needs help setting up Firestore, writing security rules, or using the Firestore SDK in their application. +--- + +# Firestore Basics + +This skill provides a complete guide for getting started with Cloud Firestore, including provisioning, securing, and integrating it into your application. + +## Provisioning + +To set up Cloud Firestore in your Firebase project and local environment, see [provisioning.md](references/provisioning.md). + +## Security Rules + +For guidance on writing and deploying Firestore Security Rules to protect your data, see [security_rules.md](references/security_rules.md). + +## SDK Usage + +To learn how to initialize and use Cloud Firestore in your application code, see [sdk_usage.md](references/sdk_usage.md). diff --git a/skills/firestore-basics/references/provisioning.md b/skills/firestore-basics/references/provisioning.md new file mode 100644 index 00000000000..9b9503cb7db --- /dev/null +++ b/skills/firestore-basics/references/provisioning.md @@ -0,0 +1,38 @@ +# Provisioning Cloud Firestore + +## CLI Initialization + +To set up Firestore in your project directory, use the Firebase CLI: + +```bash +firebase init firestore +``` + +This command will: +1. Ask you to select a default Firebase project (or create a new one). +2. Create a `firestore.rules` file for your security rules. +3. Create a `firestore.indexes.json` file for your index definitions. +4. Update your `firebase.json` configuration file. + +## Configuration (firebase.json) + +Your `firebase.json` should include the `firestore` key pointing to your rules and indexes: + +```json +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + } +} +``` + +## Local Emulation + +To run Firestore locally for development and testing: + +```bash +firebase emulators:start --only firestore +``` + +This starts the Firestore emulator, typically on port 8080. You can interact with it using the Emulator UI (usually at http://localhost:4000/firestore). diff --git a/skills/firestore-basics/references/sdk_usage.md b/skills/firestore-basics/references/sdk_usage.md new file mode 100644 index 00000000000..30a107c3b35 --- /dev/null +++ b/skills/firestore-basics/references/sdk_usage.md @@ -0,0 +1,77 @@ +# Firestore SDK Usage + +## Web (Modular SDK) + +### Initialization + +```javascript +import { initializeApp } from "firebase/app"; +import { getFirestore } from "firebase/firestore"; + +const firebaseConfig = { + // Your config options +}; + +const app = initializeApp(firebaseConfig); +const db = getFirestore(app); +``` + +### connecting to Emulator + +If you are running the local emulator: + +```javascript +import { connectFirestoreEmulator } from "firebase/firestore"; + +// After initializing db +if (location.hostname === "localhost") { + connectFirestoreEmulator(db, 'localhost', 8080); +} +``` + +### Basic Operations + +#### Add Data + +```javascript +import { collection, addDoc } from "firebase/firestore"; + +try { + const docRef = await addDoc(collection(db, "users"), { + first: "Ada", + last: "Lovelace", + born: 1815 + }); + console.log("Document written with ID: ", docRef.id); +} catch (e) { + console.error("Error adding document: ", e); +} +``` + +#### Read Data + +```javascript +import { collection, getDocs } from "firebase/firestore"; + +const querySnapshot = await getDocs(collection(db, "users")); +querySnapshot.forEach((doc) => { + console.log(`${doc.id} => ${doc.data()}`); +}); +``` + +#### Listen for Realtime Updates + +```javascript +import { collection, onSnapshot } from "firebase/firestore"; + +const unsubscribe = onSnapshot(collection(db, "users"), (snapshot) => { + snapshot.docChanges().forEach((change) => { + if (change.type === "added") { + console.log("New user: ", change.doc.data()); + } + }); +}); + +// To stop listening: +// unsubscribe(); +``` diff --git a/skills/firestore-basics/references/security_rules.md b/skills/firestore-basics/references/security_rules.md new file mode 100644 index 00000000000..598b97b6d6d --- /dev/null +++ b/skills/firestore-basics/references/security_rules.md @@ -0,0 +1,58 @@ +# Firestore Security Rules + +Security rules determine who has read and write access to your database. + +## Basic Structure + +Rules are defined in `firestore.rules`. + +```firestore +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + // Rules go here + } +} +``` + +## Common Patterns + +### Locked Mode (Deny All) +Good for starting development or private data. +```firestore +match /{document=**} { + allow read, write: if false; +} +``` + +### Test Mode (Allow All) +**WARNING: insecure.** Only for quick prototyping. +```firestore +match /{document=**} { + allow read, write: if true; +} +``` + +### Auth Required +Allow access only to authenticated users. +```firestore +match /{document=**} { + allow read, write: if request.auth != null; +} +``` + +### User-Specific Data +Allow users to access only their own data. +```firestore +match /users/{userId} { + allow read, write: if request.auth != null && request.auth.uid == userId; +} +``` + +## Deploying Rules + +To deploy only your Firestore rules: + +```bash +firebase deploy --only firestore:rules +``` From 869d13fc9c81b8f30c1a544a290035051dd75b5e Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Thu, 8 Jan 2026 13:13:24 -0800 Subject: [PATCH 2/7] Second pass, with more detail --- .eslintignore | 3 +- skills/firestore-basics/SKILL.md | 13 +- .../references/android_sdk_usage.md | 156 +++++++++++++++ skills/firestore-basics/references/indexes.md | 82 ++++++++ .../references/ios_sdk_usage.md | 188 ++++++++++++++++++ .../references/provisioning.md | 14 ++ .../firestore-basics/references/sdk_usage.md | 77 ------- .../references/security_rules.md | 183 +++++++++++++++-- .../references/web_sdk_usage.md | 179 +++++++++++++++++ 9 files changed, 803 insertions(+), 92 deletions(-) create mode 100644 skills/firestore-basics/references/android_sdk_usage.md create mode 100644 skills/firestore-basics/references/indexes.md create mode 100644 skills/firestore-basics/references/ios_sdk_usage.md delete mode 100644 skills/firestore-basics/references/sdk_usage.md create mode 100644 skills/firestore-basics/references/web_sdk_usage.md diff --git a/.eslintignore b/.eslintignore index 21b7f77de56..6aae848f0b8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,4 +9,5 @@ scripts/agent-evals/output scripts/agent-evals/node_modules scripts/agent-evals/lib scripts/agent-evals/templates -julesbot \ No newline at end of file +julesbot +skills \ No newline at end of file diff --git a/skills/firestore-basics/SKILL.md b/skills/firestore-basics/SKILL.md index 73c166cf35f..81236102099 100644 --- a/skills/firestore-basics/SKILL.md +++ b/skills/firestore-basics/SKILL.md @@ -1,6 +1,7 @@ --- name: firestore-basics description: Comprehensive guide for Firestore basics including provisioning, security rules, and SDK usage. Use this skill when the user needs help setting up Firestore, writing security rules, or using the Firestore SDK in their application. +compatibility: This skill is best used with the Firebase CLI, but does not require it. Install it by running `npm install -g firebase-tools`. --- # Firestore Basics @@ -15,6 +16,14 @@ To set up Cloud Firestore in your Firebase project and local environment, see [p For guidance on writing and deploying Firestore Security Rules to protect your data, see [security_rules.md](references/security_rules.md). -## SDK Usage +## SDK UsageGuides -To learn how to initialize and use Cloud Firestore in your application code, see [sdk_usage.md](references/sdk_usage.md). +To learn how to use Cloud Firestore in your application code, choose your platform: + +* **Web (Modular SDK)**: [web_sdk_usage.md](references/web_sdk_usage.md) +* **Android (Kotlin)**: [android_sdk_usage.md](references/android_sdk_usage.md) +* **iOS (Swift)**: [ios_sdk_usage.md](references/ios_sdk_usage.md) + +## Indexes + +For checking index types, query support tables, and best practices, see [indexes.md](references/indexes.md). diff --git a/skills/firestore-basics/references/android_sdk_usage.md b/skills/firestore-basics/references/android_sdk_usage.md new file mode 100644 index 00000000000..7d113c1dd8b --- /dev/null +++ b/skills/firestore-basics/references/android_sdk_usage.md @@ -0,0 +1,156 @@ +# Firestore Android SDK Usage Guide + +This guide uses **Kotlin** and **KTX extensions**, which correspond to the modern Android development standards. + +## Initialization + +```kotlin +// In your Activity or Application class +import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.ktx.Firebase + +val db = Firebase.firestore + +// Connect to Emulator +// Use 10.0.2.2 to access localhost from the Android Emulator +if (BuildConfig.DEBUG) { + db.useEmulator("10.0.2.2", 8080) +} +``` + +## Writing Data + +### Set a Document (`set`) +Creates or overwrites a document. + +```kotlin +val city = hashMapOf( + "name" to "Los Angeles", + "state" to "CA", + "country" to "USA" +) + +db.collection("cities").document("LA") + .set(city) + .addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully written!") } + .addOnFailureListener { e -> Log.w(TAG, "Error writing document", e) } + +// Merge +db.collection("cities").document("LA") + .set(mapOf("population" to 3900000), SetOptions.merge()) +``` + +### Add a Document with Auto-ID (`add`) + +```kotlin +val data = hashMapOf( + "name" to "Tokyo", + "country" to "Japan" +) + +db.collection("cities") + .add(data) + .addOnSuccessListener { documentReference -> + Log.d(TAG, "DocumentSnapshot written with ID: ${documentReference.id}") + } +``` + +### Update a Document (`update`) + +```kotlin +val laRef = db.collection("cities").document("LA") + +laRef.update("capital", true) + .addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully updated!") } +``` + +### Transactions +Atomic read-modify-write. + +```kotlin +db.runTransaction { transaction -> + val sfDocRef = db.collection("cities").document("SF") + val snapshot = transaction.get(sfDocRef) + + // Note: You can also use FieldValue.increment() for simple counters + val newPopulation = snapshot.getDouble("population")!! + 1 + transaction.update(sfDocRef, "population", newPopulation) + + // Success + null +}.addOnSuccessListener { Log.d(TAG, "Transaction success!") } + .addOnFailureListener { e -> Log.w(TAG, "Transaction failure.", e) } +``` + +## Reading Data + +### Get a Single Document (`get`) + +```kotlin +val docRef = db.collection("cities").document("SF") + +docRef.get().addOnSuccessListener { document -> + if (document != null && document.exists()) { + Log.d(TAG, "DocumentSnapshot data: ${document.data}") + } else { + Log.d(TAG, "No such document") + } +} +``` + +### Get Multiple Documents (`get`) + +```kotlin +db.collection("cities") + .get() + .addOnSuccessListener { result -> + for (document in result) { + Log.d(TAG, "${document.id} => ${document.data}") + } + } +``` + +## Realtime Updates + +### Listen to Changes (`addSnapshotListener`) + +```kotlin +val docRef = db.collection("cities").document("SF") + +docRef.addSnapshotListener { snapshot, e -> + if (e != null) { + Log.w(TAG, "Listen failed.", e) + return@addSnapshotListener + } + + if (snapshot != null && snapshot.exists()) { + val source = if (snapshot.metadata.hasPendingWrites()) "Local" else "Server" + Log.d(TAG, "$source data: ${snapshot.data}") + } else { + Log.d(TAG, "Current data: null") + } +} +``` + +## Queries + +### Simple and Compound +Note: Compound queries on different fields require an index. + +```kotlin +// Simple +db.collection("cities").whereEqualTo("state", "CA") + +// Compound (AND) +db.collection("cities") + .whereEqualTo("state", "CA") + .whereGreaterThan("population", 1000000) +``` + +### Order and Limit + +```kotlin +db.collection("cities") + .orderBy("name", Query.Direction.KEY_ASCENDING) + .limit(3) +``` diff --git a/skills/firestore-basics/references/indexes.md b/skills/firestore-basics/references/indexes.md new file mode 100644 index 00000000000..3cf6fde69a1 --- /dev/null +++ b/skills/firestore-basics/references/indexes.md @@ -0,0 +1,82 @@ +# Firestore Indexes Reference + +Indexes allow Firestore to ensure that query performance depends on the size of the result set, not the size of the database. + +## Index Types + +### Single-Field Indexes +Firestore **automatically creates** a single-field index for every field in a document (and subfields in maps). +* **Support**: Simple equality queries (`==`) and single-field range/sort queries (`<`, `<=`, `orderBy`). +* **Behavior**: You generally don't need to manage these unless you want to *exempt* a field. + +### Composite Indexes +A composite index stores a sorted mapping of all documents based on an ordered list of fields. +* **Support**: Complex queries that filter or sort by **multiple fields**. +* **Creation**: These are **NOT** automatically created. You must define them manually or via the console/CLI. + +## Automatic vs. Manual Management + +### What is Automatic? +* Indexes for simple queries. +* Merging of single-field indexes for multiple equality filters (e.g., `where("state", "==", "CA").where("country", "==", "USA")`). + +### When Do I Need to Act? +If you attempt a query that requires a composite index, the SDK will throw an error containing a **direct link** to the Firebase Console to create that specific index. + +**Example Error:** +> "The query requires an index. You can create it here: https://console.firebase.google.com/project/..." + +## Query Support Examples + +| Query Type | Index Required | +| :--- | :--- | +| **Simple Equality**
`where("a", "==", 1)` | Automatic (Single-Field) | +| **Simple Range/Sort**
`where("a", ">", 1).orderBy("a")` | Automatic (Single-Field) | +| **Multiple Equality**
`where("a", "==", 1).where("b", "==", 2)` | Automatic (Merged Single-Field) | +| **Equality + Range/Sort**
`where("a", "==", 1).where("b", ">", 2)` | **Composite Index** | +| **Multiple Ranges**
`where("a", ">", 1).where("b", ">", 2)` | **Composite Index** (and technically limited query support) | +| **Array Contains + Equality**
`where("tags", "array-contains", "news").where("active", "==", true)` | **Composite Index** | + +## Best Practices & Exemptions + +You can **exempt** fields from automatic indexing to save storage or strictly enforce write limits. + +### 1. High Write Rates (Sequential Values) +* **Problem**: Indexing fields that increase sequentially (like `timestamp`) limits the write rate to ~500 writes/second per collection. +* **Solution**: If you don't query on this field, **exempt** it from simple indexing. + +### 2. Large String/Map/Array Fields +* **Problem**: Indexing limits (40k entries per doc). Indexing large blobs wastes storage. +* **Solution**: Exempt large text blobs or huge arrays if they aren't used for filtering. + +### 3. TTL Fields +* **Problem**: TTL (Time-To-Live) deletion can cause index churn. +* **Solution**: Exempt the TTL timestamp field from indexing if you don't query it. + +## Management + +### `firebase.json` +Your indexes should be defined in `firestore.indexes.json` (pointed to by `firebase.json`). + +```json +{ + "indexes": [ + { + "collectionGroup": "cities", + "queryScope": "COLLECTION", + "fields": [ + { "fieldPath": "country", "order": "ASCENDING" }, + { "fieldPath": "population", "order": "DESCENDING" } + ] + } + ], + "fieldOverrides": [] +} +``` + +### CLI Commands + +Deploy indexes only: +```bash +firebase deploy --only firestore:indexes +``` diff --git a/skills/firestore-basics/references/ios_sdk_usage.md b/skills/firestore-basics/references/ios_sdk_usage.md new file mode 100644 index 00000000000..c6b55e24af8 --- /dev/null +++ b/skills/firestore-basics/references/ios_sdk_usage.md @@ -0,0 +1,188 @@ +# Firestore iOS SDK Usage Guide + +This guide uses **Swift** and the Firebase iOS SDK. + +## Initialization + +```swift +import FirebaseCore +import FirebaseFirestore + +// In your App Delegate or just before using Firestore +FirebaseApp.configure() + +let db = Firestore.firestore() + +// Connect to Emulator (Localhost) +// iOS Simulator uses 'localhost' +#if DEBUG +let settings = db.settings +settings.host = "127.0.0.1:8080" +settings.cacheSettings = MemoryCacheSettings() +settings.isSSLEnabled = false +db.settings = settings +#endif +``` + +## Writing Data + +### Set a Document (`setData`) +Creates or overwrites a document. + +```swift +let city = [ + "name": "Los Angeles", + "state": "CA", + "country": "USA" +] + +db.collection("cities").document("LA").setData(city) { err in + if let err = err { + print("Error writing document: \(err)") + } else { + print("Document successfully written!") + } +} + +// Merge +db.collection("cities").document("LA").setData([ "population": 3900000 ], merge: true) +``` + +### Add a Document with Auto-ID (`addDocument`) + +```swift +var ref: DocumentReference? = nil +ref = db.collection("cities").addDocument(data: [ + "name": "Tokyo", + "country": "Japan" +]) { err in + if let err = err { + print("Error adding document: \(err)") + } else { + print("Document added with ID: \(ref!.documentID)") + } +} +``` + +### Update a Document (`updateData`) + +```swift +let laRef = db.collection("cities").document("LA") + +laRef.updateData([ + "capital": true +]) { err in + if let err = err { + print("Error updating document: \(err)") + } else { + print("Document successfully updated") + } +} +``` + +### Transactions +Atomic read-modify-write. + +```swift +db.runTransaction({ (transaction, errorPointer) -> Any? in + let sfDocument: DocumentSnapshot + do { + try sfDocument = transaction.getDocument(db.collection("cities").document("SF")) + } catch let fetchError as NSError { + errorPointer?.pointee = fetchError + return nil + } + + guard let oldPopulation = sfDocument.data()?["population"] as? Int else { + let error = NSError( + domain: "AppErrorDomain", + code: -1, + userInfo: [ + NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot \(sfDocument)" + ] + ) + errorPointer?.pointee = error + return nil + } + + // Note: You can also use FieldValue.increment(Int64(1)) + transaction.updateData(["population": oldPopulation + 1], forDocument: sfDocument.reference) + return nil +}) { (object, error) in + if let error = error { + print("Transaction failed: \(error)") + } else { + print("Transaction successfully committed!") + } +} +``` + +## Reading Data + +### Get a Single Document (`getDocument`) + +```swift +let docRef = db.collection("cities").document("SF") + +docRef.getDocument { (document, error) in + if let document = document, document.exists { + let dataDescription = document.data().map(String.init(describing:)) ?? "nil" + print("Document data: \(dataDescription)") + } else { + print("Document does not exist") + } +} +``` + +### Get Multiple Documents (`getDocuments`) + +```swift +db.collection("cities").getDocuments() { (querySnapshot, err) in + if let err = err { + print("Error getting documents: \(err)") + } else { + for document in querySnapshot!.documents { + print("\(document.documentID) => \(document.data())") + } + } +} +``` + +## Realtime Updates + +### Listen to Changes (`addSnapshotListener`) + +```swift +db.collection("cities").document("SF") + .addSnapshotListener { documentSnapshot, error in + guard let document = documentSnapshot else { + print("Error fetching document: \(error!)") + return + } + + let source = document.metadata.hasPendingWrites ? "Local" : "Server" + print("\(source) data: \(document.data() ?? [:])") + } +``` + +## Queries + +### Simple and Compound + +```swift +// Simple +db.collection("cities").whereField("state", isEqualTo: "CA") + +// Compound (AND) +db.collection("cities") + .whereField("state", isEqualTo: "CA") + .whereField("population", isGreaterThan: 1000000) +``` + +### Order and Limit + +```swift +db.collection("cities") + .order(by: "name") + .limit(to: 3) +``` diff --git a/skills/firestore-basics/references/provisioning.md b/skills/firestore-basics/references/provisioning.md index 9b9503cb7db..2c1d861a3e6 100644 --- a/skills/firestore-basics/references/provisioning.md +++ b/skills/firestore-basics/references/provisioning.md @@ -27,6 +27,20 @@ Your `firebase.json` should include the `firestore` key pointing to your rules a } ``` +## Deploy rules and indexes +To deploy all rules and indexes +``` +firebase deploy --only firestore +``` +To deploy just rules +``` +firebase deploy --only firestore:rules +``` +To deploy just indexes +``` +firebase deploy --only firestore:indexes +``` + ## Local Emulation To run Firestore locally for development and testing: diff --git a/skills/firestore-basics/references/sdk_usage.md b/skills/firestore-basics/references/sdk_usage.md deleted file mode 100644 index 30a107c3b35..00000000000 --- a/skills/firestore-basics/references/sdk_usage.md +++ /dev/null @@ -1,77 +0,0 @@ -# Firestore SDK Usage - -## Web (Modular SDK) - -### Initialization - -```javascript -import { initializeApp } from "firebase/app"; -import { getFirestore } from "firebase/firestore"; - -const firebaseConfig = { - // Your config options -}; - -const app = initializeApp(firebaseConfig); -const db = getFirestore(app); -``` - -### connecting to Emulator - -If you are running the local emulator: - -```javascript -import { connectFirestoreEmulator } from "firebase/firestore"; - -// After initializing db -if (location.hostname === "localhost") { - connectFirestoreEmulator(db, 'localhost', 8080); -} -``` - -### Basic Operations - -#### Add Data - -```javascript -import { collection, addDoc } from "firebase/firestore"; - -try { - const docRef = await addDoc(collection(db, "users"), { - first: "Ada", - last: "Lovelace", - born: 1815 - }); - console.log("Document written with ID: ", docRef.id); -} catch (e) { - console.error("Error adding document: ", e); -} -``` - -#### Read Data - -```javascript -import { collection, getDocs } from "firebase/firestore"; - -const querySnapshot = await getDocs(collection(db, "users")); -querySnapshot.forEach((doc) => { - console.log(`${doc.id} => ${doc.data()}`); -}); -``` - -#### Listen for Realtime Updates - -```javascript -import { collection, onSnapshot } from "firebase/firestore"; - -const unsubscribe = onSnapshot(collection(db, "users"), (snapshot) => { - snapshot.docChanges().forEach((change) => { - if (change.type === "added") { - console.log("New user: ", change.doc.data()); - } - }); -}); - -// To stop listening: -// unsubscribe(); -``` diff --git a/skills/firestore-basics/references/security_rules.md b/skills/firestore-basics/references/security_rules.md index 598b97b6d6d..63db32fc3b2 100644 --- a/skills/firestore-basics/references/security_rules.md +++ b/skills/firestore-basics/references/security_rules.md @@ -1,41 +1,54 @@ -# Firestore Security Rules +# Firestore Security Rules Structure Security rules determine who has read and write access to your database. -## Basic Structure +## Service and Database Declaration -Rules are defined in `firestore.rules`. +All Firestore rules begin with the service declaration and a match block for the database (usually default). -```firestore +``` rules_version = '2'; + service cloud.firestore { match /databases/{database}/documents { // Rules go here + // {database} wildcard represents the database name } } ``` +## Basic Read/Write Operations + +Rules describe **conditions** that must be true to allow an operation. + +``` +match /cities/{city} { + allow read: if ; + allow write: if ; +} +``` + ## Common Patterns ### Locked Mode (Deny All) Good for starting development or private data. -```firestore +``` match /{document=**} { allow read, write: if false; } ``` ### Test Mode (Allow All) -**WARNING: insecure.** Only for quick prototyping. -```firestore +**WARNING: insecure.** Only for quick prototyping. Unsafe to deploy for production apps. +``` match /{document=**} { allow read, write: if true; } ``` ### Auth Required -Allow access only to authenticated users. -```firestore +Allow access only to authenticated users. This allows any logged in user access to all data. +``` match /{document=**} { allow read, write: if request.auth != null; } @@ -43,15 +56,161 @@ match /{document=**} { ### User-Specific Data Allow users to access only their own data. -```firestore +``` match /users/{userId} { allow read, write: if request.auth != null && request.auth.uid == userId; } ``` -## Deploying Rules +### Allow only verified emails +Requires users to verify ownership of the email address before using it to read or write data +``` + match /databases/{database}/documents { + // Allow access based on email domain + match /some_collection/{document} { + allow read: if request.auth != null + && request.auth.email_verified + && request.auth.email.endsWith('@example.com') + } + } +``` + +### Validate data in write operations +``` +// Example for creating a user profile +match /users/{userId} { + allow create: if request.auth.uid == userId && + request.resource.data.email is string && + request.resource.data.createdAt == request.time; +} +``` + +### Granular Operations + +You can break down `read` and `write` into more specific operations: + +* **read** + * `get`: Retrieval of a single document. + * `list`: Queries and collection reads. +* **write** + * `create`: Writing to a nonexistent document. + * `update`: Writing to an existing document. + * `delete`: Removing a document. + +```firestore +match /cities/{city} { + allow get: if ; + allow list: if ; + allow create: if ; + allow update: if ; + allow delete: if ; +} +``` + +## Hierarchical Data + +Rules applied to a parent collection **do not** cascade to subcollections. You must explicitly match subcollections. + +### Nested Match Statements + +Inner matches are relative to the outer match path. + +```firestore +match /cities/{city} { + allow read, write: if ; + + // Explicitly match the subcollection 'landmarks' + match /landmarks/{landmark} { + allow read, write: if ; + } +} +``` + +### Recursive Wildcards (`{name=**}`) + +Use recursive wildcards to apply rules to an arbitrarily deep hierarchy. + +* **Version 2** (recommended): `{path=**}` matches zero or more path segments. + +```firestore +// Allow read access to ANY document in the 'cities' collection or its subcollections +match /cities/{document=**} { + allow read: if true; +} +``` + +## Controlling Field Access + +### Read Limitations + +Reads in Firestore are **document-level**. You cannot retrieve a partial document. +* **Allowed**: Read the entire document. +* **Denied**: logical failure, no data returned. + +To secure specific fields (e.g., private user data), you must **split them into a separate document** (e.g., a `private` subcollection). + +### Write Restrictions + +You can strictly control which fields can be written or updated. + +#### On Creation +Use `request.resource.data.keys()` to validate fields. + +```firestore +match /restaurant/{restId} { + allow create: if request.resource.data.keys().hasAll(['name', 'location']) && + request.resource.data.keys().hasOnly(['name', 'location', 'city', 'address']); +} +``` + +#### On Update +Use `diff()` to see what changed between the existing document (`resource.data`) and the incoming data (`request.resource.data`). + +```firestore +match /restaurant/{restId} { + allow update: if request.resource.data.diff(resource.data).affectedKeys() + .hasOnly(['name', 'location', 'city']); // Prevent others from changing +} +``` + +### Enforcing Field Types +Use the `is` operator to validate data types. + +```firestore +allow create: if request.resource.data.score is int && + request.resource.data.active is bool && + request.resource.data.tags is list; +``` + +## Understanding Rule Evaluation + +### Overlapping Matches -> OR Logic + +If a document matches more than one rule statement, access is allowed if **ANY** of the matching rules allow it. + +```firestore +// Document: /cities/SF + +match /cities/{city} { + allow read: if false; // Deny +} + +match /cities/{document=**} { + allow read: if true; // Allow +} + +// Result: ALLOWED (because one rule returned true) +``` + +## Common Limits + +* **Call Depth**: Maximum call depth for custom functions is 20. +* **Document Access**: + * 10 access calls for single-doc requests/queries. + * 20 access calls for multi-doc reads/transactions/batches. +* **Size**: Ruleset source max 256 KB. Compiled max 250 KB. -To deploy only your Firestore rules: +## Deploying ```bash firebase deploy --only firestore:rules diff --git a/skills/firestore-basics/references/web_sdk_usage.md b/skills/firestore-basics/references/web_sdk_usage.md new file mode 100644 index 00000000000..29f6ee6db24 --- /dev/null +++ b/skills/firestore-basics/references/web_sdk_usage.md @@ -0,0 +1,179 @@ +# Firestore Web SDK Usage Guide + +This guide focuses on the **Modular Web SDK** (v9+), which is tree-shakeable and efficient. + +## Initialization + +import { initializeApp } from "firebase/app"; +import { getFirestore } from "firebase/firestore"; + +const firebaseConfig = { + // Your config options. Get the values by running 'firebase apps:sdkconfig ' +}; + +const app = initializeApp(firebaseConfig); +const db = getFirestore(app); + +``` + +## Writing Data + +### Set a Document (`setDoc`) +Creates a document if it doesn't exist, or overwrites it if it does. + +```javascript +import { doc, setDoc } from "firebase/firestore"; + +// Create/Overwrite document with ID "LA" +await setDoc(doc(db, "cities", "LA"), { + name: "Los Angeles", + state: "CA", + country: "USA" +}); + +// To merge with existing data instead of overwriting: +await setDoc(doc(db, "cities", "LA"), { population: 3900000 }, { merge: true }); +``` + +### Add a Document with Auto-ID (`addDoc`) +Use when you don't care about the document ID. + +```javascript +import { collection, addDoc } from "firebase/firestore"; + +const docRef = await addDoc(collection(db, "cities"), { + name: "Tokyo", + country: "Japan" +}); +console.log("Document written with ID: ", docRef.id); +``` + +### Update a Document (`updateDoc`) +Update some fields of an existing document without overwriting the entire document. Fails if the document doesn't exist. + +```javascript +import { doc, updateDoc } from "firebase/firestore"; + +const laRef = doc(db, "cities", "LA"); + +await updateDoc(laRef, { + capital: true +}); +``` + +### Transactions +Perform an atomic read-modify-write operation. + +```javascript +import { runTransaction, doc } from "firebase/firestore"; + +const sfDocRef = doc(db, "cities", "SF"); + +try { + await runTransaction(db, async (transaction) => { + const sfDoc = await transaction.get(sfDocRef); + if (!sfDoc.exists()) { + throw "Document does not exist!"; + } + + const newPopulation = sfDoc.data().population + 1; + transaction.update(sfDocRef, { population: newPopulation }); + }); + console.log("Transaction successfully committed!"); +} catch (e) { + console.log("Transaction failed: ", e); +} +``` + +## Reading Data + +### Get a Single Document (`getDoc`) + +```javascript +import { doc, getDoc } from "firebase/firestore"; + +const docRef = doc(db, "cities", "SF"); +const docSnap = await getDoc(docRef); + +if (docSnap.exists()) { + console.log("Document data:", docSnap.data()); +} else { + console.log("No such document!"); +} +``` + +### Get Multiple Documents (`getDocs`) +Fetches all documents in a query or collection once. + +```javascript +import { collection, getDocs } from "firebase/firestore"; + +const querySnapshot = await getDocs(collection(db, "cities")); +querySnapshot.forEach((doc) => { + // doc.data() is never undefined for query doc snapshots + console.log(doc.id, " => ", doc.data()); +}); +``` + +## Realtime Updates + +### Listen to a Document/Query (`onSnapshot`) + +```javascript +import { doc, onSnapshot } from "firebase/firestore"; + +const unsub = onSnapshot(doc(db, "cities", "SF"), (doc) => { + console.log("Current data: ", doc.data()); +}); + +// Stop listening +// unsub(); +``` + +### Handle Changes (Added/Modified/Removed) + +```javascript +import { collection, query, where, onSnapshot } from "firebase/firestore"; + +const q = query(collection(db, "cities"), where("state", "==", "CA")); +const unsubscribe = onSnapshot(q, (snapshot) => { + snapshot.docChanges().forEach((change) => { + if (change.type === "added") { + console.log("New city: ", change.doc.data()); + } + if (change.type === "modified") { + console.log("Modified city: ", change.doc.data()); + } + if (change.type === "removed") { + console.log("Removed city: ", change.doc.data()); + } + }); +}); +``` + +## Queries + +### Simple and Compound Queries +Use `query()` to combine filters. + +```javascript +import { collection, query, where, getDocs } from "firebase/firestore"; + +const citiesRef = collection(db, "cities"); + +// Simple equality +const q1 = query(citiesRef, where("state", "==", "CA")); + +// Compound (AND) +// Note: Requires an index if filtering on different fields +const q2 = query(citiesRef, where("state", "==", "CA"), where("population", ">", 1000000)); +``` + +### Order and Limit +Sort and limit results. + +```javascript +import { orderBy, limit } from "firebase/firestore"; + +const q = query(citiesRef, orderBy("name"), limit(3)); +``` From 1535b51fe25cce8f6090530b7e1d5a9f9c753ae7 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Fri, 9 Jan 2026 09:03:53 -0800 Subject: [PATCH 3/7] More skill improvements --- skills/firestore-basics/SKILL.md | 2 +- .../references/provisioning.md | 77 +++++++++++++++---- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/skills/firestore-basics/SKILL.md b/skills/firestore-basics/SKILL.md index 81236102099..24c4d0aaad5 100644 --- a/skills/firestore-basics/SKILL.md +++ b/skills/firestore-basics/SKILL.md @@ -16,7 +16,7 @@ To set up Cloud Firestore in your Firebase project and local environment, see [p For guidance on writing and deploying Firestore Security Rules to protect your data, see [security_rules.md](references/security_rules.md). -## SDK UsageGuides +## SDK Usage To learn how to use Cloud Firestore in your application code, choose your platform: diff --git a/skills/firestore-basics/references/provisioning.md b/skills/firestore-basics/references/provisioning.md index 2c1d861a3e6..3484f895c54 100644 --- a/skills/firestore-basics/references/provisioning.md +++ b/skills/firestore-basics/references/provisioning.md @@ -1,22 +1,16 @@ # Provisioning Cloud Firestore -## CLI Initialization +## Manual Initialization -To set up Firestore in your project directory, use the Firebase CLI: +For non-interactive environments (like AI agents), it is recommended to manually create the necessary configuration files instead of using `firebase init`. -```bash -firebase init firestore -``` - -This command will: -1. Ask you to select a default Firebase project (or create a new one). -2. Create a `firestore.rules` file for your security rules. -3. Create a `firestore.indexes.json` file for your index definitions. -4. Update your `firebase.json` configuration file. +1. **Create `firebase.json`**: This file configures the Firebase CLI. +2. **Create `firestore.rules`**: This file contains your security rules. +3. **Create `firestore.indexes.json`**: This file contains your index definitions. -## Configuration (firebase.json) +### 1. Create `firebase.json` -Your `firebase.json` should include the `firestore` key pointing to your rules and indexes: +Create a file named `firebase.json` in your project root with the following content. If this file already exists, instead append to the existing JSON: ```json { @@ -27,6 +21,63 @@ Your `firebase.json` should include the `firestore` key pointing to your rules a } ``` +This will use the default database. To use a different database, specify the database ID and location. You can check the list of available databases using `firebase firestore:databases:list`. If the database does not exist, it will be created when you deploy: + +```json +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json", + "database": "my-database-id", + "location": "us-central1" + } +} +``` + + To use Enterprise edition, specify the `enterprise` field. + +```json +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json", + "enterprise": true, + "database": "my-database-id", + "location": "us-central1" + } +} +``` + +### 2. Create `firestore.rules` + +Create a file named `firestore.rules`. A good starting point (locking down the database) is: + +``` +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if false; + } + } +} +``` +*See [security_rules.md](security_rules.md) for how to write actual rules.* + +### 3. Create `firestore.indexes.json` + +Create a file named `firestore.indexes.json` with an empty configuration to start: + +```json +{ + "indexes": [], + "fieldOverrides": [] +} +``` + +*See [indexes.md](indexes.md) for how to configure indexes.* + + ## Deploy rules and indexes To deploy all rules and indexes ``` From c5937b46c28536cdf9b8564e65a1444f3a1a0a41 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Fri, 9 Jan 2026 09:37:58 -0800 Subject: [PATCH 4/7] Omit skills from prettier --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index 4b61cb58ecf..2f612bc6250 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,6 +8,7 @@ /scripts/agent-evals/output/** /src/frameworks/docs/** /prompts +/skills # Intentionally invalid YAML file: /src/test/fixtures/extension-yamls/invalid/extension.yaml From 766970ad63abada5114cf257b76b06175a0a16d9 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Fri, 9 Jan 2026 11:26:31 -0800 Subject: [PATCH 5/7] PR fixes --- .../references/android_sdk_usage.md | 5 +++-- .../firestore-basics/references/ios_sdk_usage.md | 6 +++--- skills/firestore-basics/references/provisioning.md | 14 ++++++-------- .../firestore-basics/references/security_rules.md | 14 ++++++-------- .../firestore-basics/references/web_sdk_usage.md | 2 ++ 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/skills/firestore-basics/references/android_sdk_usage.md b/skills/firestore-basics/references/android_sdk_usage.md index 7d113c1dd8b..82b91de5179 100644 --- a/skills/firestore-basics/references/android_sdk_usage.md +++ b/skills/firestore-basics/references/android_sdk_usage.md @@ -7,6 +7,7 @@ This guide uses **Kotlin** and **KTX extensions**, which correspond to the moder ```kotlin // In your Activity or Application class import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.firestore.SetOptions import com.google.firebase.ktx.Firebase val db = Firebase.firestore @@ -73,7 +74,7 @@ db.runTransaction { transaction -> val snapshot = transaction.get(sfDocRef) // Note: You can also use FieldValue.increment() for simple counters - val newPopulation = snapshot.getDouble("population")!! + 1 + val newPopulation = (snapshot.getDouble("population") ?: 0.0) + 1 transaction.update(sfDocRef, "population", newPopulation) // Success @@ -151,6 +152,6 @@ db.collection("cities") ```kotlin db.collection("cities") - .orderBy("name", Query.Direction.KEY_ASCENDING) + .orderBy("name", Query.Direction.ASCENDING) .limit(3) ``` diff --git a/skills/firestore-basics/references/ios_sdk_usage.md b/skills/firestore-basics/references/ios_sdk_usage.md index c6b55e24af8..e76c1532f66 100644 --- a/skills/firestore-basics/references/ios_sdk_usage.md +++ b/skills/firestore-basics/references/ios_sdk_usage.md @@ -48,7 +48,7 @@ db.collection("cities").document("LA").setData(city) { err in db.collection("cities").document("LA").setData([ "population": 3900000 ], merge: true) ``` -### Add a Document with Auto-ID (`addDocument`) +###// Add a Document with Auto-ID (`addDocument`) ```swift var ref: DocumentReference? = nil @@ -59,7 +59,7 @@ ref = db.collection("cities").addDocument(data: [ if let err = err { print("Error adding document: \(err)") } else { - print("Document added with ID: \(ref!.documentID)") + print("Document added with ID: \(ref?.documentID ?? "unknown")") } } ``` @@ -141,7 +141,7 @@ db.collection("cities").getDocuments() { (querySnapshot, err) in if let err = err { print("Error getting documents: \(err)") } else { - for document in querySnapshot!.documents { + for document in querySnapshot?.documents ?? [] { print("\(document.documentID) => \(document.data())") } } diff --git a/skills/firestore-basics/references/provisioning.md b/skills/firestore-basics/references/provisioning.md index 3484f895c54..3a6882f9a61 100644 --- a/skills/firestore-basics/references/provisioning.md +++ b/skills/firestore-basics/references/provisioning.md @@ -79,16 +79,14 @@ Create a file named `firestore.indexes.json` with an empty configuration to star ## Deploy rules and indexes -To deploy all rules and indexes -``` +```bash +# To deploy all rules and indexes firebase deploy --only firestore -``` -To deploy just rules -``` + +# To deploy just rules firebase deploy --only firestore:rules -``` -To deploy just indexes -``` + +# To deploy just indexes firebase deploy --only firestore:indexes ``` diff --git a/skills/firestore-basics/references/security_rules.md b/skills/firestore-basics/references/security_rules.md index 63db32fc3b2..5ed40ad703a 100644 --- a/skills/firestore-basics/references/security_rules.md +++ b/skills/firestore-basics/references/security_rules.md @@ -65,14 +65,12 @@ match /users/{userId} { ### Allow only verified emails Requires users to verify ownership of the email address before using it to read or write data ``` - match /databases/{database}/documents { - // Allow access based on email domain - match /some_collection/{document} { - allow read: if request.auth != null - && request.auth.email_verified - && request.auth.email.endsWith('@example.com') - } - } +// Allow access based on email domain +match /some_collection/{document} { + allow read: if request.auth != null + && request.auth.email_verified + && request.auth.email.endsWith('@example.com'); +} ``` ### Validate data in write operations diff --git a/skills/firestore-basics/references/web_sdk_usage.md b/skills/firestore-basics/references/web_sdk_usage.md index 29f6ee6db24..64510ab8998 100644 --- a/skills/firestore-basics/references/web_sdk_usage.md +++ b/skills/firestore-basics/references/web_sdk_usage.md @@ -4,8 +4,10 @@ This guide focuses on the **Modular Web SDK** (v9+), which is tree-shakeable and ## Initialization +```javascript import { initializeApp } from "firebase/app"; import { getFirestore } from "firebase/firestore"; +``` const firebaseConfig = { // Your config options. Get the values by running 'firebase apps:sdkconfig ' From 028ae5f2e914b71577413995211c43434ca2e414 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Wed, 14 Jan 2026 10:34:03 -0800 Subject: [PATCH 6/7] incorporating the rules gen strategy developed by the access team --- .../references/security_rules.md | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/skills/firestore-basics/references/security_rules.md b/skills/firestore-basics/references/security_rules.md index 5ed40ad703a..3c9f02e878d 100644 --- a/skills/firestore-basics/references/security_rules.md +++ b/skills/firestore-basics/references/security_rules.md @@ -213,3 +213,87 @@ match /cities/{document=**} { ```bash firebase deploy --only firestore:rules ``` + +## Security Rules Development Workflow + +For complex applications, follow this structured 6-phase workflow to ensure your rules are secure and comprehensive. + +### Phase 1: Codebase Analysis + +Before writing rules, scan your codebase to identify: +1. **Collections & Paths**: List all collections and document structures. +2. **Data Models**: Define required fields, data types, and constraints (e.g., string length, regex patterns). +3. **Access Patterns**: Document who can read/write what and under what conditions (e.g., exact ownership, role-based). +4. **Authentication**: Identify if you use Firebase Auth, anonymous auth, or custom tokens. + +### Phase 2: Security Rules Generation + +Write your rules following these core principles: +* **Default Deny**: Start with `allow read, write: if false;` and whitelist specific operations. +* **Least Privilege**: Grant only the minimum permissions required. +* **Validate Data**: Check types (e.g., `is string`), required fields, and values on `create` and `update`. +* **UID Protection**: Ensure users cannot create documents with another user's UID or change ownership. + +#### Recommended Structure + +It is helpful to define a `User` type or similar helper functions at the top of your rules file. + +```javascript +// Helper Functions +function isAuthenticated() { + return request.auth != null; +} + +function isOwner(userId) { + return isAuthenticated() && request.auth.uid == userId; +} + +// Validate data types and required fields +function isValidUser() { + let user = request.resource.data; + return user.keys().hasAll(['name', 'email', 'createdAt']) && + user.name is string && user.name.size() > 0 && + user.email is string && user.email.matches('.+@.+\\..+') && + user.createdAt is timestamp; +} + +// Prevent UID tampering +function isUidUnchanged() { + return request.resource.data.uid == resource.data.uid; +} +``` + +### Phase 3: Devil's Advocate Attack + +Attempt to mentally "break" your rules by checking for common vulnerabilities: +1. Can I read data I shouldn't? +2. Can I create a document with someone else's UID? +3. Can I update a document and steal ownership (change the `uid` field)? +4. Can I send a massive string to a field with no length limit? +5. Can I delete a document I don't own? +6. Can I bypass validation by sending `null` or missing fields? + +If *any* of these succeed, fix the rule and repeat. + +### Phase 4: Syntactic Validation + +Use `firebase deploy --only firestore:rules --dry-run` to validate syntax. + +### Phase 5: Test Suite Generation + +Create a comprehensive test suite using `@firebase/rules-unit-testing`. Ideally, create a dedicated `rules_test/` directory. + +**Test Coverage Checklist:** +* [ ] **Authorized Operations**: Users *can* do what they are supposed to. +* [ ] **Unauthorized Operations**: Users *cannot* do what is forbidden. +* [ ] **UID Tampering**: Users cannot create/update data with another's UID. +* [ ] **Data Validation**: Invalid types, missing fields, or malformed data (bad emails, URLs) must fail. +* [ ] **Immutable Fields**: Fields like `createdAt` or `authorId` cannot be changed on update. + +### Phase 6: Test Validation Loop + +1. Start the emulator: `firebase emulators:start --only firestore` +2. Run tests: `npm test` (inside your test directory) +3. If tests fail due to **rules**: Fix the rules. +4. If tests fail due to **test bugs**: Fix the tests. +5. Repeat until 100% pass rate. From 428fe1961df09537e410f4f5b7a8652ce6b63705 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Wed, 14 Jan 2026 16:30:02 -0800 Subject: [PATCH 7/7] PR fixes --- skills/firestore-basics/references/indexes.md | 2 +- skills/firestore-basics/references/provisioning.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/firestore-basics/references/indexes.md b/skills/firestore-basics/references/indexes.md index 3cf6fde69a1..6b0c4a8357e 100644 --- a/skills/firestore-basics/references/indexes.md +++ b/skills/firestore-basics/references/indexes.md @@ -55,7 +55,7 @@ You can **exempt** fields from automatic indexing to save storage or strictly en ## Management -### `firebase.json` +### Config files Your indexes should be defined in `firestore.indexes.json` (pointed to by `firebase.json`). ```json diff --git a/skills/firestore-basics/references/provisioning.md b/skills/firestore-basics/references/provisioning.md index 3a6882f9a61..1c7af017820 100644 --- a/skills/firestore-basics/references/provisioning.md +++ b/skills/firestore-basics/references/provisioning.md @@ -2,7 +2,7 @@ ## Manual Initialization -For non-interactive environments (like AI agents), it is recommended to manually create the necessary configuration files instead of using `firebase init`. +Initialize the following firebase configuration files manually. Do not use `firebase init`, as it expects interactive inputs. 1. **Create `firebase.json`**: This file configures the Firebase CLI. 2. **Create `firestore.rules`**: This file contains your security rules.