From b3641c68e930d3e14759d668eada8c31a638dd21 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Fri, 13 Feb 2026 15:17:47 -0800 Subject: [PATCH 01/10] Add firebase-data-connect-basics skill (#7) * Add firebase-data-connect-basics skill * Add firebase dataconnect basic skills --------- Co-authored-by: Muhammad Talha --- skills/firebase-data-connect-basics/SKILL.md | 184 +++++++++ .../firebase-data-connect-basics/examples.md | 377 ++++++++++++++++++ .../reference/advanced.md | 303 ++++++++++++++ .../reference/config.md | 267 +++++++++++++ .../reference/operations.md | 357 +++++++++++++++++ .../reference/schema.md | 278 +++++++++++++ .../reference/sdks.md | 275 +++++++++++++ .../reference/security.md | 289 ++++++++++++++ .../firebase-data-connect-basics/templates.md | 269 +++++++++++++ 9 files changed, 2599 insertions(+) create mode 100644 skills/firebase-data-connect-basics/SKILL.md create mode 100644 skills/firebase-data-connect-basics/examples.md create mode 100644 skills/firebase-data-connect-basics/reference/advanced.md create mode 100644 skills/firebase-data-connect-basics/reference/config.md create mode 100644 skills/firebase-data-connect-basics/reference/operations.md create mode 100644 skills/firebase-data-connect-basics/reference/schema.md create mode 100644 skills/firebase-data-connect-basics/reference/sdks.md create mode 100644 skills/firebase-data-connect-basics/reference/security.md create mode 100644 skills/firebase-data-connect-basics/templates.md diff --git a/skills/firebase-data-connect-basics/SKILL.md b/skills/firebase-data-connect-basics/SKILL.md new file mode 100644 index 0000000..7d22d3b --- /dev/null +++ b/skills/firebase-data-connect-basics/SKILL.md @@ -0,0 +1,184 @@ +--- +name: firebase-data-connect +description: Build and deploy Firebase Data Connect backends with PostgreSQL. Use for schema design, GraphQL queries/mutations, authorization, and SDK generation for web, Android, iOS, and Flutter apps. +--- + +# Firebase Data Connect + +Firebase Data Connect is a relational database service using Cloud SQL for PostgreSQL with GraphQL schema, auto-generated queries/mutations, and type-safe SDKs. + +## Quick Start + +```graphql +# schema.gql - Define your data model +type Movie @table { + id: UUID! @default(expr: "uuidV4()") + title: String! + releaseYear: Int + genre: String +} + +# queries.gql - Define operations +query ListMovies @auth(level: PUBLIC) { + movies { id title genre } +} + +mutation CreateMovie($title: String!, $genre: String) @auth(level: USER) { + movie_insert(data: { title: $title, genre: $genre }) +} +``` + +## Project Structure + +``` +dataconnect/ +├── dataconnect.yaml # Service configuration +├── schema/ +│ └── schema.gql # Data model (types with @table) +└── connector/ + ├── connector.yaml # Connector config + SDK generation + ├── queries.gql # Queries + └── mutations.gql # Mutations +``` + +## Core Concepts + +| Concept | Description | +|---------|-------------| +| **Schema** | GraphQL types with `@table` → PostgreSQL tables | +| **Connector** | Collection of queries/mutations as API endpoints | +| **Generated Fields** | Auto-generated `movie`, `movies`, `movie_insert`, `movie_update`, `movie_delete` | +| **Key Scalars** | `Movie_Key` type for record identification | +| **@auth** | Authorization directive: `PUBLIC`, `USER`, `USER_EMAIL_VERIFIED`, `NO_ACCESS` | + +## Detailed References + +**Design your data model** → See [schema.md](reference/schema.md) +- Types, @table, @col, @default directives +- Relationships with @ref (one-to-one, one-to-many, many-to-many) +- Data types: UUID, String, Int, Int64, Float, Boolean, Date, Timestamp, Vector + +**Build queries and mutations** → See [operations.md](reference/operations.md) +- Generated fields and key scalars +- Filtering with `where`, `orderBy`, `limit` +- Relational queries with `_on_` and `_via_` syntax +- Multi-step mutations with `@transaction` + +**Secure your operations** → See [security.md](reference/security.md) +- @auth directive and access levels +- CEL expressions for custom authorization +- @check and @redact for data lookup authorization +- Common authorization patterns and anti-patterns + +**Integrate with client apps** → See [sdks.md](reference/sdks.md) +- Web, Android, iOS, Flutter SDK usage +- SDK generation with Firebase CLI +- Calling queries/mutations from client code + +**Configure and deploy** → See [config.md](reference/config.md) +- dataconnect.yaml and connector.yaml structure +- Firebase CLI commands +- Local emulator setup +- Deployment workflow + +**Advanced features** → See [advanced.md](reference/advanced.md) +- Vector similarity search with Vertex AI embeddings +- Full-text search with @searchable directive +- Cloud Functions integration (mutation triggers) +- Data seeding and bulk operations + +## Common Patterns + +### User-Owned Resources + +```graphql +type Post @table { + id: UUID! @default(expr: "uuidV4()") + authorUid: String! @default(expr: "auth.uid") + content: String! +} + +mutation CreatePost($content: String!) @auth(level: USER) { + post_insert(data: { authorUid_expr: "auth.uid", content: $content }) +} + +query MyPosts @auth(level: USER) { + posts(where: { authorUid: { eq_expr: "auth.uid" }}) { id content } +} +``` + +### Many-to-Many Relationship + +```graphql +type Movie @table { + id: UUID! @default(expr: "uuidV4()") + title: String! +} + +type Actor @table { + id: UUID! @default(expr: "uuidV4()") + name: String! +} + +type MovieActor @table(key: ["movie", "actor"]) { + movie: Movie! + actor: Actor! + role: String! +} +``` + +### Filtered Queries + +```graphql +query MoviesByGenre($genre: String!, $minRating: Int) @auth(level: PUBLIC) { + movies( + where: { genre: { eq: $genre }, rating: { ge: $minRating }}, + orderBy: [{ rating: DESC }], + limit: 10 + ) { id title rating } +} +``` + +## Examples & Templates + +**Complete working examples** → See [examples.md](examples.md) +**Ready-to-use templates** → See [templates.md](templates.md) + +## MCP Tools Available + +- `firebase_init` - Initialize Data Connect with `dataconnect` feature +- `firebase_get_sdk_config` - Get Firebase configuration for client apps +- `firebase_get_project` - Get current project information +- `firebase_update_environment` - Set project directory and active project + +## CLI Commands + +```bash +# Initialize Data Connect +firebase init dataconnect + +# Start emulator for local development +firebase emulators:start --only dataconnect + +# Generate SDKs +firebase dataconnect:sdk:generate + +# Deploy to Firebase +firebase deploy --only dataconnect +``` + +## Key Directives Quick Reference + +| Directive | Purpose | Example | +|-----------|---------|---------| +| `@table` | Define PostgreSQL table | `type Movie @table { ... }` | +| `@col` | Customize column name/type | `@col(name: "movie_id", dataType: "serial")` | +| `@default` | Set default value | `@default(expr: "uuidV4()")` | +| `@ref` | Foreign key reference | `author: User!` (implicit) or `@ref(fields: "authorId")` | +| `@unique` | Unique constraint | `email: String! @unique` | +| `@index` | Database index | `title: String! @index` | +| `@searchable` | Enable full-text search | `title: String! @searchable` | +| `@auth` | Authorization level | `@auth(level: USER)` or `@auth(expr: "auth.uid != nil")` | +| `@check` | Validate field in mutation | `@check(expr: "this != null", message: "Not found")` | +| `@redact` | Hide field from response | Used with @check for auth lookups | +| `@transaction` | Atomic multi-step mutation | `mutation Multi @transaction { ... }` | diff --git a/skills/firebase-data-connect-basics/examples.md b/skills/firebase-data-connect-basics/examples.md new file mode 100644 index 0000000..87a6e3d --- /dev/null +++ b/skills/firebase-data-connect-basics/examples.md @@ -0,0 +1,377 @@ +# Examples + +Complete, working examples for common Data Connect use cases. + +--- + +## Movie Review App + +A complete schema for a movie database with reviews, actors, and user authentication. + +### Schema + +```graphql +# schema.gql + +# Users +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + email: String! @unique + displayName: String + createdAt: Timestamp! @default(expr: "request.time") +} + +# Movies +type Movie @table { + id: UUID! @default(expr: "uuidV4()") + title: String! + releaseYear: Int + genre: String @index + rating: Float + description: String + posterUrl: String + createdAt: Timestamp! @default(expr: "request.time") +} + +# Movie metadata (one-to-one) +type MovieMetadata @table { + movie: Movie! @unique + director: String + runtime: Int + budget: Int64 +} + +# Actors +type Actor @table { + id: UUID! @default(expr: "uuidV4()") + name: String! + birthDate: Date +} + +# Movie-Actor relationship (many-to-many) +type MovieActor @table(key: ["movie", "actor"]) { + movie: Movie! + actor: Actor! + role: String! # "lead" or "supporting" + character: String +} + +# Reviews (user-owned) +type Review @table @unique(fields: ["movie", "user"]) { + id: UUID! @default(expr: "uuidV4()") + movie: Movie! + user: User! + rating: Int! + text: String + createdAt: Timestamp! @default(expr: "request.time") +} +``` + +### Queries + +```graphql +# queries.gql + +# Public: List movies with filtering +query ListMovies($genre: String, $minRating: Float, $limit: Int) + @auth(level: PUBLIC) { + movies( + where: { + genre: { eq: $genre }, + rating: { ge: $minRating } + }, + orderBy: [{ rating: DESC }], + limit: $limit + ) { + id title genre rating releaseYear posterUrl + } +} + +# Public: Get movie with full details +query GetMovie($id: UUID!) @auth(level: PUBLIC) { + movie(id: $id) { + id title genre rating releaseYear description + metadata: movieMetadata_on_movie { director runtime } + actors: actors_via_MovieActor { name } + reviews: reviews_on_movie(orderBy: [{ createdAt: DESC }], limit: 10) { + rating text createdAt + user { displayName } + } + } +} + +# User: Get my reviews +query MyReviews @auth(level: USER) { + reviews(where: { user: { uid: { eq_expr: "auth.uid" }}}) { + id rating text createdAt + movie { id title posterUrl } + } +} +``` + +### Mutations + +```graphql +# mutations.gql + +# User: Create/update profile on first login +mutation UpsertUser($email: String!, $displayName: String) @auth(level: USER) { + user_upsert(data: { + uid_expr: "auth.uid", + email: $email, + displayName: $displayName + }) +} + +# User: Add review (one per movie per user) +mutation AddReview($movieId: UUID!, $rating: Int!, $text: String) + @auth(level: USER) { + review_upsert(data: { + movie: { id: $movieId }, + user: { uid_expr: "auth.uid" }, + rating: $rating, + text: $text + }) +} + +# User: Delete my review +mutation DeleteReview($id: UUID!) @auth(level: USER) { + review_delete( + first: { where: { + id: { eq: $id }, + user: { uid: { eq_expr: "auth.uid" }} + }} + ) +} +``` + +--- + +## E-Commerce Store + +Products, orders, and cart management with user authentication. + +### Schema + +```graphql +# schema.gql + +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + email: String! @unique + name: String + shippingAddress: String +} + +type Product @table { + id: UUID! @default(expr: "uuidV4()") + name: String! @index + description: String + price: Float! + stock: Int! @default(value: 0) + category: String @index + imageUrl: String +} + +type CartItem @table(key: ["user", "product"]) { + user: User! + product: Product! + quantity: Int! +} + +enum OrderStatus { + PENDING + PAID + SHIPPED + DELIVERED + CANCELLED +} + +type Order @table { + id: UUID! @default(expr: "uuidV4()") + user: User! + status: OrderStatus! @default(value: PENDING) + total: Float! + shippingAddress: String! + createdAt: Timestamp! @default(expr: "request.time") +} + +type OrderItem @table { + id: UUID! @default(expr: "uuidV4()") + order: Order! + product: Product! + quantity: Int! + priceAtPurchase: Float! +} +``` + +### Operations + +```graphql +# Public: Browse products +query ListProducts($category: String, $search: String) @auth(level: PUBLIC) { + products(where: { + category: { eq: $category }, + name: { contains: $search }, + stock: { gt: 0 } + }) { + id name price stock imageUrl + } +} + +# User: View cart +query MyCart @auth(level: USER) { + cartItems(where: { user: { uid: { eq_expr: "auth.uid" }}}) { + quantity + product { id name price imageUrl stock } + } +} + +# User: Add to cart +mutation AddToCart($productId: UUID!, $quantity: Int!) @auth(level: USER) { + cartItem_upsert(data: { + user: { uid_expr: "auth.uid" }, + product: { id: $productId }, + quantity: $quantity + }) +} + +# User: Checkout (transactional) +mutation Checkout($shippingAddress: String!) + @auth(level: USER) + @transaction { + # Query cart items + query @redact { + cartItems(where: { user: { uid: { eq_expr: "auth.uid" }}}) + @check(expr: "this.size() > 0", message: "Cart is empty") { + quantity + product { id price } + } + } + # Create order (in real app, calculate total from cart) + order_insert(data: { + user: { uid_expr: "auth.uid" }, + shippingAddress: $shippingAddress, + total: 0 # Calculate in app logic + }) +} +``` + +--- + +## Blog with Permissions + +Multi-author blog with role-based permissions. + +### Schema + +```graphql +# schema.gql + +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + email: String! @unique + name: String! + bio: String +} + +enum UserRole { + VIEWER + AUTHOR + EDITOR + ADMIN +} + +type BlogPermission @table(key: ["user"]) { + user: User! + role: UserRole! @default(value: VIEWER) +} + +enum PostStatus { + DRAFT + PUBLISHED + ARCHIVED +} + +type Post @table { + id: UUID! @default(expr: "uuidV4()") + author: User! + title: String! @searchable + content: String! @searchable + status: PostStatus! @default(value: DRAFT) + publishedAt: Timestamp + createdAt: Timestamp! @default(expr: "request.time") + updatedAt: Timestamp! @default(expr: "request.time") +} + +type Comment @table { + id: UUID! @default(expr: "uuidV4()") + post: Post! + author: User! + content: String! + createdAt: Timestamp! @default(expr: "request.time") +} +``` + +### Operations with Role Checks + +```graphql +# Public: Read published posts +query PublishedPosts @auth(level: PUBLIC) { + posts( + where: { status: { eq: PUBLISHED }}, + orderBy: [{ publishedAt: DESC }] + ) { + id title content publishedAt + author { name } + } +} + +# Author+: Create post +mutation CreatePost($title: String!, $content: String!) + @auth(level: USER) + @transaction { + # Check user is at least AUTHOR + query @redact { + blogPermission(key: { user: { uid_expr: "auth.uid" }}) + @check(expr: "this != null", message: "No permission record") { + role @check(expr: "this in ['AUTHOR', 'EDITOR', 'ADMIN']", message: "Must be author+") + } + } + post_insert(data: { + author: { uid_expr: "auth.uid" }, + title: $title, + content: $content + }) +} + +# Editor+: Publish any post +mutation PublishPost($id: UUID!) + @auth(level: USER) + @transaction { + query @redact { + blogPermission(key: { user: { uid_expr: "auth.uid" }}) { + role @check(expr: "this in ['EDITOR', 'ADMIN']", message: "Must be editor+") + } + } + post_update(id: $id, data: { + status: PUBLISHED, + publishedAt_expr: "request.time" + }) +} + +# Admin: Grant role +mutation GrantRole($userUid: String!, $role: UserRole!) + @auth(level: USER) + @transaction { + query @redact { + blogPermission(key: { user: { uid_expr: "auth.uid" }}) { + role @check(expr: "this == 'ADMIN'", message: "Must be admin") + } + } + blogPermission_upsert(data: { + user: { uid: $userUid }, + role: $role + }) +} +``` diff --git a/skills/firebase-data-connect-basics/reference/advanced.md b/skills/firebase-data-connect-basics/reference/advanced.md new file mode 100644 index 0000000..9b5cb3b --- /dev/null +++ b/skills/firebase-data-connect-basics/reference/advanced.md @@ -0,0 +1,303 @@ +# Advanced Features Reference + +## Contents +- [Vector Similarity Search](#vector-similarity-search) +- [Full-Text Search](#full-text-search) +- [Cloud Functions Integration](#cloud-functions-integration) +- [Data Seeding & Bulk Operations](#data-seeding--bulk-operations) + +--- + +## Vector Similarity Search + +Semantic search using Vertex AI embeddings and PostgreSQL's `pgvector`. + +### Schema Setup + +```graphql +type Movie @table { + id: UUID! @default(expr: "uuidV4()") + title: String! + description: String + # Vector field for embeddings - size must match model output (768 for gecko) + descriptionEmbedding: Vector! @col(size: 768) +} +``` + +### Generate Embeddings in Mutations + +Use `_embed` server value to auto-generate embeddings via Vertex AI: + +```graphql +mutation CreateMovieWithEmbedding($title: String!, $description: String!) + @auth(level: USER) { + movie_insert(data: { + title: $title, + description: $description, + descriptionEmbedding_embed: { + model: "textembedding-gecko@003", + text: $description + } + }) +} +``` + +### Similarity Search Query + +Data Connect generates `_similarity` fields for Vector columns: + +```graphql +query SearchMovies($query: String!) @auth(level: PUBLIC) { + movies_descriptionEmbedding_similarity( + compare_embed: { model: "textembedding-gecko@003", text: $query }, + method: L2, # L2, COSINE, or INNER_PRODUCT + within: 2.0, # Max distance threshold + limit: 5 + ) { + id + title + description + _metadata { distance } # See how close each result is + } +} +``` + +### Similarity Parameters + +| Parameter | Description | +|-----------|-------------| +| `compare` | Raw Vector to compare against | +| `compare_embed` | Generate embedding from text via Vertex AI | +| `method` | Distance function: `L2`, `COSINE`, `INNER_PRODUCT` | +| `within` | Max distance (results further are excluded) | +| `where` | Additional filters | +| `limit` | Max results to return | + +### Custom Embeddings + +Pass pre-computed vectors directly: + +```graphql +mutation StoreCustomEmbedding($id: UUID!, $embedding: Vector!) @auth(level: USER) { + movie_update(id: $id, data: { descriptionEmbedding: $embedding }) +} + +query SearchWithCustomVector($vector: Vector!) @auth(level: PUBLIC) { + movies_descriptionEmbedding_similarity( + compare: $vector, + method: COSINE, + limit: 10 + ) { id title } +} +``` + +--- + +## Full-Text Search + +Fast keyword/phrase search using PostgreSQL's full-text capabilities. + +### Enable with @searchable + +```graphql +type Movie @table { + title: String! @searchable + description: String @searchable(language: "english") + genre: String @searchable +} +``` + +### Search Query + +Data Connect generates `_search` fields: + +```graphql +query SearchMovies($query: String!) @auth(level: PUBLIC) { + movies_search( + query: $query, + queryFormat: QUERY, # QUERY, PLAIN, PHRASE, or ADVANCED + limit: 20 + ) { + id title description + _metadata { relevance } # Relevance score + } +} +``` + +### Query Formats + +| Format | Description | +|--------|-------------| +| `QUERY` | Web-style (default): quotes, AND, OR supported | +| `PLAIN` | Match all words, any order | +| `PHRASE` | Match exact phrase | +| `ADVANCED` | Full tsquery syntax | + +### Tuning Results + +```graphql +query SearchWithThreshold($query: String!) @auth(level: PUBLIC) { + movies_search( + query: $query, + relevanceThreshold: 0.05, # Min relevance score + where: { genre: { eq: "Action" }}, + orderBy: [{ releaseYear: DESC }] + ) { id title } +} +``` + +### Supported Languages + +`english` (default), `french`, `german`, `spanish`, `italian`, `portuguese`, `dutch`, `danish`, `finnish`, `norwegian`, `swedish`, `russian`, `arabic`, `hindi`, `simple` + +--- + +## Cloud Functions Integration + +Trigger Cloud Functions when mutations execute. + +### Basic Trigger (Node.js) + +```typescript +import { onMutationExecuted } from "firebase-functions/dataconnect"; +import { logger } from "firebase-functions"; + +export const onUserCreate = onMutationExecuted( + { + service: "myService", + connector: "default", + operation: "CreateUser", + region: "us-central1" // Must match Data Connect location + }, + (event) => { + const variables = event.data.payload.variables; + const returnedData = event.data.payload.data; + + logger.info("User created:", returnedData); + // Send welcome email, sync to analytics, etc. + } +); +``` + +### Basic Trigger (Python) + +```python +from firebase_functions import dataconnect_fn, logger + +@dataconnect_fn.on_mutation_executed( + service="myService", + connector="default", + operation="CreateUser" +) +def on_user_create(event: dataconnect_fn.Event): + variables = event.data.payload.variables + returned_data = event.data.payload.data + logger.info("User created:", returned_data) +``` + +### Event Data + +```typescript +// event.authType: "app_user" | "unauthenticated" | "admin" +// event.authId: Firebase Auth UID (for app_user) +// event.data.payload.variables: mutation input variables +// event.data.payload.data: mutation response data +// event.data.payload.errors: any errors that occurred +``` + +### Filtering with Wildcards + +```typescript +// Trigger on all User* mutations +export const onUserMutation = onMutationExecuted( + { operation: "User*" }, + (event) => { /* ... */ } +); + +// Capture operation name +export const onAnyMutation = onMutationExecuted( + { service: "myService", operation: "{operationName}" }, + (event) => { + console.log("Operation:", event.params.operationName); + } +); +``` + +### Use Cases + +- **Data sync**: Replicate to Firestore, BigQuery, external APIs +- **Notifications**: Send emails, push notifications on events +- **Async workflows**: Image processing, data aggregation +- **Audit logging**: Track all data changes + +> ⚠️ **Avoid infinite loops**: Don't trigger mutations that would fire the same trigger. Use filters to exclude self-triggered events. + +--- + +## Data Seeding & Bulk Operations + +### Local Prototyping with _insertMany + +```graphql +mutation SeedMovies @transaction { + movie_insertMany(data: [ + { id: "uuid-1", title: "Movie 1", genre: "Action" }, + { id: "uuid-2", title: "Movie 2", genre: "Drama" }, + { id: "uuid-3", title: "Movie 3", genre: "Comedy" } + ]) +} +``` + +### Reset Data with _upsertMany + +```graphql +mutation ResetData { + movie_upsertMany(data: [ + { id: "uuid-1", title: "Movie 1", genre: "Action" }, + { id: "uuid-2", title: "Movie 2", genre: "Drama" } + ]) +} +``` + +### Clear All Data + +```graphql +mutation ClearMovies { + movie_deleteMany(all: true) +} +``` + +### Production: Admin SDK Bulk Operations + +```typescript +import { initializeApp } from 'firebase-admin/app'; +import { getDataConnect } from 'firebase-admin/data-connect'; + +const app = initializeApp(); +const dc = getDataConnect({ location: "us-central1", serviceId: "my-service" }); + +const movies = [ + { id: "uuid-1", title: "Movie 1", genre: "Action" }, + { id: "uuid-2", title: "Movie 2", genre: "Drama" } +]; + +// Bulk insert +await dc.insertMany("movie", movies); + +// Bulk upsert +await dc.upsertMany("movie", movies); + +// Single operations +await dc.insert("movie", movies[0]); +await dc.upsert("movie", movies[0]); +``` + +### Emulator Data Persistence + +```bash +# Export emulator data +firebase emulators:export ./seed-data + +# Start with saved data +firebase emulators:start --only dataconnect --import=./seed-data +``` diff --git a/skills/firebase-data-connect-basics/reference/config.md b/skills/firebase-data-connect-basics/reference/config.md new file mode 100644 index 0000000..ac88d33 --- /dev/null +++ b/skills/firebase-data-connect-basics/reference/config.md @@ -0,0 +1,267 @@ +# Configuration Reference + +## Contents +- [Project Structure](#project-structure) +- [dataconnect.yaml](#dataconnectyaml) +- [connector.yaml](#connectoryaml) +- [Firebase CLI Commands](#firebase-cli-commands) +- [Emulator](#emulator) +- [Deployment](#deployment) + +--- + +## Project Structure + +``` +project-root/ +├── firebase.json # Firebase project config +└── dataconnect/ + ├── dataconnect.yaml # Service configuration + ├── schema/ + │ └── schema.gql # Data model (types, relationships) + └── connector/ + ├── connector.yaml # Connector config + SDK generation + ├── queries.gql # Query operations + └── mutations.gql # Mutation operations (optional separate file) +``` + +--- + +## dataconnect.yaml + +Main Data Connect service configuration: + +```yaml +specVersion: "v1" +serviceId: "my-service" +location: "us-central1" +schemaValidation: "STRICT" # or "COMPATIBLE" +schema: + source: "./schema" + datasource: + postgresql: + database: "fdcdb" + cloudSql: + instanceId: "my-instance" +connectorDirs: ["./connector"] +``` + +| Field | Description | +|-------|-------------| +| `specVersion` | Always `"v1"` | +| `serviceId` | Unique identifier for the service | +| `location` | GCP region (us-central1, us-east4, europe-west1, etc.) | +| `schemaValidation` | Deployment mode: `"STRICT"` (must match exactly) or `"COMPATIBLE"` (backward compatible) | +| `schema.source` | Path to schema directory | +| `schema.datasource` | PostgreSQL connection config | +| `connectorDirs` | List of connector directories | + +### Cloud SQL Configuration + +```yaml +schema: + datasource: + postgresql: + database: "my-database" # Database name + cloudSql: + instanceId: "my-instance" # Cloud SQL instance ID +``` + +--- + +## connector.yaml + +Connector configuration and SDK generation: + +```yaml +connectorId: "default" +generate: + javascriptSdk: + outputDir: "../web/src/lib/dataconnect" + package: "@myapp/dataconnect" + kotlinSdk: + outputDir: "../android/app/src/main/kotlin/com/myapp/dataconnect" + package: "com.myapp.dataconnect" + swiftSdk: + outputDir: "../ios/MyApp/DataConnect" +``` + +### SDK Generation Options + +| SDK | Fields | +|-----|--------| +| `javascriptSdk` | `outputDir`, `package` | +| `kotlinSdk` | `outputDir`, `package` | +| `swiftSdk` | `outputDir` | +| `nodeAdminSdk` | `outputDir`, `package` (for Admin SDK) | + +--- + +## Firebase CLI Commands + +### Initialize Data Connect + +```bash +# Interactive setup +firebase init dataconnect + +# Set project +firebase use +``` + +### Local Development + +```bash +# Start emulator +firebase emulators:start --only dataconnect + +# Start with database seed data +firebase emulators:start --only dataconnect --import=./seed-data + +# Generate SDKs +firebase dataconnect:sdk:generate + +# Watch for schema changes (auto-regenerate) +firebase dataconnect:sdk:generate --watch +``` + +### Schema Management + +```bash +# Compare local schema to production +firebase dataconnect:sql:diff + + +# Apply migration +firebase dataconnect:sql:migrate +``` + +### Deployment + +```bash +# Deploy Data Connect service +firebase deploy --only dataconnect + +# Deploy specific connector +firebase deploy --only dataconnect:connector-id + +# Deploy with schema migration +firebase deploy --only dataconnect --force +``` + +--- + +## Emulator + +### Start Emulator + +```bash +firebase emulators:start --only dataconnect +``` + +Default ports: +- Data Connect: `9399` +- PostgreSQL: `9939` (local PostgreSQL instance) + +### Emulator Configuration (firebase.json) + +```json +{ + "emulators": { + "dataconnect": { + "port": 9399 + } + } +} +``` + +### Connect from SDK + +```typescript +// Web +import { connectDataConnectEmulator } from 'firebase/data-connect'; +connectDataConnectEmulator(dc, 'localhost', 9399); + +// Android +connector.dataConnect.useEmulator("10.0.2.2", 9399) + +// iOS +connector.useEmulator(host: "localhost", port: 9399) + + +``` + +### Seed Data + +Create seed data files and import: + +```bash +# Export current emulator data +firebase emulators:export ./seed-data + +# Start with seed data +firebase emulators:start --only dataconnect --import=./seed-data +``` + +--- + +## Deployment + +### Deploy Workflow + +1. **Test locally** with emulator +2. **Generate SQL diff**: `firebase dataconnect:sql:diff` +3. **Review migration**: Check breaking changes +4. **Deploy**: `firebase deploy --only dataconnect` + +### Schema Migrations + +Data Connect auto-generates PostgreSQL migrations: + +```bash +# Preview migration +firebase dataconnect:sql:diff + +# Apply migration (interactive) +firebase dataconnect:sql:migrate + +# Force migration (non-interactive) +firebase dataconnect:sql:migrate --force +``` + +### Breaking Changes + +Some schema changes require special handling: +- Removing required fields +- Changing field types +- Removing tables + +Use `--force` flag to acknowledge breaking changes during deploy. + +### CI/CD Integration + +```yaml +# GitHub Actions example +- name: Deploy Data Connect + run: | + firebase deploy --only dataconnect --token ${{ secrets.FIREBASE_TOKEN }} --force +``` + +--- + +## VS Code Extension + +Install "Firebase Data Connect" extension for: +- Schema intellisense and validation +- GraphQL operation testing +- Emulator integration +- SDK generation on save + +### Extension Settings + +```json +{ + "firebase.dataConnect.autoGenerateSdk": true, + "firebase.dataConnect.emulator.port": 9399 +} +``` diff --git a/skills/firebase-data-connect-basics/reference/operations.md b/skills/firebase-data-connect-basics/reference/operations.md new file mode 100644 index 0000000..6fe7e73 --- /dev/null +++ b/skills/firebase-data-connect-basics/reference/operations.md @@ -0,0 +1,357 @@ +# Operations Reference + +## Contents +- [Generated Fields](#generated-fields) +- [Queries](#queries) +- [Mutations](#mutations) +- [Key Scalars](#key-scalars) +- [Multi-Step Operations](#multi-step-operations) + +--- + +## Generated Fields + +Data Connect auto-generates fields for each `@table` type: + +| Generated Field | Purpose | Example | +|-----------------|---------|---------| +| `movie(id: UUID, key: Key, first: Row)` | Get single record | `movie(id: $id)` or `movie(first: {where: ...})` | +| `movies(where: ..., orderBy: ..., limit: ..., offset: ..., distinct: ..., having: ...)` | List/filter records | `movies(where: {...})` | +| `movie_insert(data: ...)` | Create record | Returns key | +| `movie_insertMany(data: [...])` | Bulk create | Returns keys | +| `movie_update(id: ..., data: ...)` | Update by ID | Returns key or null | +| `movie_updateMany(where: ..., data: ...)` | Bulk update | Returns count | +| `movie_upsert(data: ...)` | Insert or update | Returns key | +| `movie_delete(id: ...)` | Delete by ID | Returns key or null | +| `movie_deleteMany(where: ...)` | Bulk delete | Returns count | + +### Relation Fields +For a `Post` with `author: User!`: +- `post.author` - Navigate to related User +- `user.posts_on_author` - Reverse: all Posts by User + +For many-to-many via `MovieActor`: +- `movie.actors_via_MovieActor` - Get all actors +- `actor.movies_via_MovieActor` - Get all movies + +--- + +## Queries + +### Basic Query + +```graphql +query GetMovie($id: UUID!) @auth(level: PUBLIC) { + movie(id: $id) { + id title genre releaseYear + } +} +``` + +### List with Filtering + +```graphql +query ListMovies($genre: String, $minRating: Int) @auth(level: PUBLIC) { + movies( + where: { + genre: { eq: $genre }, + rating: { ge: $minRating } + }, + orderBy: [{ releaseYear: DESC }, { title: ASC }], + limit: 20, + offset: 0 + ) { + id title genre rating + } +} +``` + +### Filter Operators + +| Operator | Description | Example | +|----------|-------------|---------| +| `eq` | Equals | `{ title: { eq: "Matrix" }}` | +| `ne` | Not equals | `{ status: { ne: "deleted" }}` | +| `gt`, `ge` | Greater than (or equal) | `{ rating: { ge: 4 }}` | +| `lt`, `le` | Less than (or equal) | `{ releaseYear: { lt: 2000 }}` | +| `in` | In list | `{ genre: { in: ["Action", "Drama"] }}` | +| `nin` | Not in list | `{ status: { nin: ["deleted", "hidden"] }}` | +| `isNull` | Is null check | `{ description: { isNull: true }}` | +| `contains` | String contains | `{ title: { contains: "war" }}` | +| `startsWith` | String starts with | `{ title: { startsWith: "The" }}` | +| `endsWith` | String ends with | `{ email: { endsWith: "@gmail.com" }}` | +| `includes` | Array includes | `{ tags: { includes: "sci-fi" }}` | + +### Expression Operators (Compare with Server Values) + +Use `_expr` suffix to compare with server-side values: + +```graphql +query MyPosts @auth(level: USER) { + posts(where: { authorUid: { eq_expr: "auth.uid" }}) { + id title + } +} + +query RecentPosts @auth(level: PUBLIC) { + posts(where: { publishedAt: { lt_expr: "request.time" }}) { + id title + } +} +``` + +### Logical Operators + +```graphql +query ComplexFilter($genre: String, $minRating: Int) @auth(level: PUBLIC) { + movies(where: { + _or: [ + { genre: { eq: $genre }}, + { rating: { ge: $minRating }} + ], + _and: [ + { releaseYear: { ge: 2000 }}, + { status: { ne: "hidden" }} + ], + _not: { genre: { eq: "Horror" }} + }) { id title } +} +``` + +### Relational Queries + +```graphql +# Navigate relationships +query MovieWithDetails($id: UUID!) @auth(level: PUBLIC) { + movie(id: $id) { + title + # One-to-one + metadata: movieMetadata_on_movie { director } + # One-to-many + reviews: reviews_on_movie { rating user { name }} + # Many-to-many + actors: actors_via_MovieActor { name } + } +} + +# Filter by related data +query MoviesByDirector($director: String!) @auth(level: PUBLIC) { + movies(where: { + movieMetadata_on_movie: { director: { eq: $director }} + }) { id title } +} +``` + +### Aliases + +```graphql +query CompareRatings($genre: String!) @auth(level: PUBLIC) { + highRated: movies(where: { genre: { eq: $genre }, rating: { ge: 8 }}) { + title rating + } + lowRated: movies(where: { genre: { eq: $genre }, rating: { lt: 5 }}) { + title rating + } +} +``` + +--- + +## Mutations + +### Create + +```graphql +mutation CreateMovie($title: String!, $genre: String) @auth(level: USER) { + movie_insert(data: { + title: $title, + genre: $genre + }) +} +``` + +### Create with Server Values + +```graphql +mutation CreatePost($title: String!, $content: String!) @auth(level: USER) { + post_insert(data: { + authorUid_expr: "auth.uid", # Current user + id_expr: "uuidV4()", # Auto-generate UUID + createdAt_expr: "request.time", # Server timestamp + title: $title, + content: $content + }) +} +``` + +### Update + +```graphql +mutation UpdateMovie($id: UUID!, $title: String, $genre: String) @auth(level: USER) { + movie_update( + id: $id, + data: { + title: $title, + genre: $genre, + updatedAt_expr: "request.time" + } + ) +} +``` + +### Update Operators + +```graphql +mutation IncrementViews($id: UUID!) @auth(level: PUBLIC) { + movie_update(id: $id, data: { + viewCount_update: { inc: 1 } + }) +} + +mutation AddTag($id: UUID!, $tag: String!) @auth(level: USER) { + movie_update(id: $id, data: { + tags_update: { add: [$tag] } # add, remove, append, prepend + }) +} +``` + +| Operator | Types | Description | +|----------|-------|-------------| +| `inc` | Int, Float, Date, Timestamp | Increment value | +| `dec` | Int, Float, Date, Timestamp | Decrement value | +| `add` | Lists | Add items if not present | +| `remove` | Lists | Remove all matching items | +| `append` | Lists | Append to end | +| `prepend` | Lists | Prepend to start | + +### Upsert + +```graphql +mutation UpsertUser($email: String!, $name: String!) @auth(level: USER) { + user_upsert(data: { + uid_expr: "auth.uid", + email: $email, + name: $name + }) +} +``` + +### Delete + +```graphql +mutation DeleteMovie($id: UUID!) @auth(level: USER) { + movie_delete(id: $id) +} + +mutation DeleteOldDrafts @auth(level: USER) { + post_deleteMany(where: { + status: { eq: "draft" }, + createdAt: { lt_time: { now: true, sub: { days: 30 }}} + }) +} +``` + +### Filtered Updates/Deletes (User-Owned) + +```graphql +mutation UpdateMyPost($id: UUID!, $content: String!) @auth(level: USER) { + post_update( + first: { where: { + id: { eq: $id }, + authorUid: { eq_expr: "auth.uid" } # Only own posts + }}, + data: { content: $content } + ) +} +``` + +--- + +## Key Scalars + +Key scalars (`Movie_Key`, `User_Key`) are auto-generated types representing primary keys: + +```graphql +# Using key scalar +query GetMovie($key: Movie_Key!) @auth(level: PUBLIC) { + movie(key: $key) { title } +} + +# Variable format +# { "key": { "id": "uuid-here" } } + +# Composite key +# { "key": { "movieId": "...", "userId": "..." } } +``` + +Key scalars are returned by mutations: + +```graphql +mutation CreateAndFetch($title: String!) @auth(level: USER) { + key: movie_insert(data: { title: $title }) + # Returns: { "key": { "id": "generated-uuid" } } +} +``` + +--- + +## Multi-Step Operations + +### @transaction + +Ensures atomicity - all steps succeed or all rollback: + +```graphql +mutation CreateUserWithProfile($name: String!, $bio: String!) + @auth(level: USER) + @transaction { + # Step 1: Create user + user_insert(data: { + uid_expr: "auth.uid", + name: $name + }) + # Step 2: Create profile (uses response from step 1) + userProfile_insert(data: { + userId_expr: "response.user_insert.uid", + bio: $bio + }) +} +``` + +### Using response Binding + +Access results from previous steps: + +```graphql +mutation CreateTodoWithItem($listName: String!, $itemText: String!) + @auth(level: USER) + @transaction { + todoList_insert(data: { + id_expr: "uuidV4()", + name: $listName + }) + todoItem_insert(data: { + listId_expr: "response.todoList_insert.id", # From previous step + text: $itemText + }) +} +``` + +### Embedded Queries + +Run queries within mutations for validation: + +```graphql +mutation AddToPublicList($listId: UUID!, $item: String!) + @auth(level: USER) + @transaction { + # Step 1: Verify list exists and is public + query @redact { + todoList(id: $listId) @check(expr: "this != null", message: "List not found") { + isPublic @check(expr: "this == true", message: "List is not public") + } + } + # Step 2: Add item + todoItem_insert(data: { listId: $listId, text: $item }) +} +``` diff --git a/skills/firebase-data-connect-basics/reference/schema.md b/skills/firebase-data-connect-basics/reference/schema.md new file mode 100644 index 0000000..21d34ec --- /dev/null +++ b/skills/firebase-data-connect-basics/reference/schema.md @@ -0,0 +1,278 @@ +# Schema Reference + +## Contents +- [Defining Types](#defining-types) +- [Core Directives](#core-directives) +- [Relationships](#relationships) +- [Data Types](#data-types) +- [Enumerations](#enumerations) + +--- + +## Defining Types + +Types with `@table` map to PostgreSQL tables. Data Connect auto-generates an implicit `id: UUID!` primary key. + +```graphql +type Movie @table { + # id: UUID! is auto-added + title: String! + releaseYear: Int + genre: String +} +``` + +### Customizing Tables + +```graphql +type Movie @table(name: "movies", key: "id", singular: "movie", plural: "movies") { + id: UUID! @col(name: "movie_id") @default(expr: "uuidV4()") + title: String! + releaseYear: Int @col(name: "release_year") + genre: String @col(dataType: "varchar(20)") +} +``` + +### User Table with Auth + +```graphql +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + email: String! @unique + displayName: String @col(dataType: "varchar(100)") + createdAt: Timestamp! @default(expr: "request.time") +} +``` + +--- + +## Core Directives + +### @table +Defines a database table. + +| Argument | Description | +|----------|-------------| +| `name` | PostgreSQL table name (snake_case default) | +| `key` | Primary key field(s), default `["id"]` | +| `singular` | Singular name for generated fields | +| `plural` | Plural name for generated fields | + +### @col +Customizes column mapping. + +| Argument | Description | +|----------|-------------| +| `name` | Column name in PostgreSQL | +| `dataType` | PostgreSQL type: `serial`, `varchar(n)`, `text`, etc. | +| `size` | Required for `Vector` type | + +### @default +Sets default value for inserts. + +| Argument | Description | +|----------|-------------| +| `value` | Literal value: `@default(value: "draft")` | +| `expr` | CEL expression: `@default(expr: "uuidV4()")`, `@default(expr: "auth.uid")`, `@default(expr: "request.time")` | +| `sql` | Raw SQL: `@default(sql: "now()")` | + +**Common expressions:** +- `uuidV4()` - Generate UUID +- `auth.uid` - Current user's Firebase Auth UID +- `request.time` - Server timestamp + +### @unique +Adds unique constraint. + +```graphql +type User @table { + email: String! @unique +} + +# Composite unique +type Review @table @unique(fields: ["movie", "user"]) { + movie: Movie! + user: User! + rating: Int +} +``` + +### @index +Creates database index for query performance. + +```graphql +type Movie @table @index(fields: ["genre", "releaseYear"], order: [ASC, DESC]) { + title: String! @index + genre: String + releaseYear: Int +} +``` + +| Argument | Description | +|----------|-------------| +| `fields` | Fields for composite index (on @table) | +| `order` | `[ASC]` or `[DESC]` for each field | +| `type` | `BTREE` (default), `GIN` (arrays), `HNSW`/`IVFFLAT` (vectors) | + +### @searchable +Enables full-text search on String fields. + +```graphql +type Post @table { + title: String! @searchable + body: String! @searchable(language: "english") +} + +# Usage +query SearchPosts($q: String!) @auth(level: PUBLIC) { + posts_search(query: $q) { id title body } +} +``` + +--- + +## Relationships + +### One-to-Many (Implicit Foreign Key) + +```graphql +type Post @table { + id: UUID! @default(expr: "uuidV4()") + author: User! # Creates authorId foreign key + title: String! +} + +type User @table { + id: UUID! @default(expr: "uuidV4()") + name: String! + # Auto-generated: posts_on_author: [Post!]! +} +``` + +### @ref Directive +Customizes foreign key reference. + +```graphql +type Post @table { + author: User! @ref(fields: "authorId", references: "id") + authorId: UUID! # Explicit FK field +} +``` + +| Argument | Description | +|----------|-------------| +| `fields` | Local FK field name(s) | +| `references` | Target field(s) in referenced table | +| `constraintName` | PostgreSQL constraint name | + +**Cascade behavior:** +- Required reference (`User!`): CASCADE DELETE (post deleted when user deleted) +- Optional reference (`User`): SET NULL (authorId set to null when user deleted) + +### One-to-One + +Use `@unique` on the reference field: + +```graphql +type User @table { id: UUID! name: String! } + +type UserProfile @table { + user: User! @unique # One profile per user + bio: String + avatarUrl: String +} + +# Query: user.userProfile_on_user +``` + +### Many-to-Many + +Use a join table with composite primary key: + +```graphql +type Movie @table { id: UUID! title: String! } +type Actor @table { id: UUID! name: String! } + +type MovieActor @table(key: ["movie", "actor"]) { + movie: Movie! + actor: Actor! + role: String! # Extra data on relationship +} + +# Generated fields: +# - movie.actors_via_MovieActor: [Actor!]! +# - actor.movies_via_MovieActor: [Movie!]! +# - movie.movieActors_on_movie: [MovieActor!]! +``` + +--- + +## Data Types + +| GraphQL Type | PostgreSQL Default | Other PostgreSQL Types | +|--------------|-------------------|----------------------| +| `String` | `text` | `varchar(n)`, `char(n)` | +| `Int` | `int4` | `int2`, `serial` | +| `Int64` | `bigint` | `bigserial`, `numeric` | +| `Float` | `float8` | `float4`, `numeric` | +| `Boolean` | `boolean` | | +| `UUID` | `uuid` | | +| `Date` | `date` | | +| `Timestamp` | `timestamptz` | Stored as UTC | +| `Any` | `jsonb` | | +| `Vector` | `vector` | Requires `@col(size: N)` | +| `[Type]` | Array | e.g., `[String]` → `text[]` | + +--- + +## Enumerations + +```graphql +enum Status { + DRAFT + PUBLISHED + ARCHIVED +} + +type Post @table { + status: Status! @default(value: DRAFT) + allowedStatuses: [Status!] +} +``` + +**Rules:** +- Enum names: PascalCase, no underscores +- Enum values: UPPER_SNAKE_CASE +- Values are ordered (for comparison operations) +- Changing order or removing values is a breaking change + +--- + +## Views (Advanced) + +Map custom SQL queries to GraphQL types: + +```graphql +type MovieStats @view(sql: """ + SELECT + movie_id, + COUNT(*) as review_count, + AVG(rating) as avg_rating + FROM review + GROUP BY movie_id +""") { + movie: Movie @unique + reviewCount: Int + avgRating: Float +} + +# Query movies with stats +query TopMovies @auth(level: PUBLIC) { + movies(orderBy: [{ rating: DESC }]) { + title + stats: movieStats_on_movie { + reviewCount avgRating + } + } +} +``` diff --git a/skills/firebase-data-connect-basics/reference/sdks.md b/skills/firebase-data-connect-basics/reference/sdks.md new file mode 100644 index 0000000..ff39dcc --- /dev/null +++ b/skills/firebase-data-connect-basics/reference/sdks.md @@ -0,0 +1,275 @@ +# SDK Reference + +## Contents +- [SDK Generation](#sdk-generation) +- [Web SDK](#web-sdk) +- [Android SDK](#android-sdk) +- [iOS SDK](#ios-sdk) +- [Admin SDK](#admin-sdk) + +--- + +## SDK Generation + +Configure SDK generation in `connector.yaml`: + +```yaml +connectorId: my-connector +generate: + javascriptSdk: + outputDir: "../web-app/src/lib/dataconnect" + package: "@movie-app/dataconnect" + kotlinSdk: + outputDir: "../android-app/app/src/main/kotlin/com/example/dataconnect" + package: "com.example.dataconnect" + swiftSdk: + outputDir: "../ios-app/DataConnect" +``` + +Generate SDKs: +```bash +firebase dataconnect:sdk:generate +``` + +--- + +## Web SDK + +### Installation + +```bash +npm install firebase +``` + +### Initialization + +```typescript +import { initializeApp } from 'firebase/app'; +import { getDataConnect, connectDataConnectEmulator } from 'firebase/data-connect'; +import { connectorConfig } from '@movie-app/dataconnect'; + +const app = initializeApp(firebaseConfig); +const dc = getDataConnect(app, connectorConfig); + +// For local development +if (import.meta.env.DEV) { + connectDataConnectEmulator(dc, 'localhost', 9399); +} +``` + +### Calling Operations + +```typescript +// Generated SDK provides typed functions +import { listMovies, createMovie, getMovie } from '@movie-app/dataconnect'; + +// Query +const result = await listMovies(); +console.log(result.data.movies); + +// Query with variables +const movie = await getMovie({ id: 'uuid-here' }); + +// Mutation +const newMovie = await createMovie({ + title: 'New Movie', + genre: 'Action' +}); +console.log(newMovie.data.movie_insert); // Returns key +``` + +### Subscriptions + +```typescript +import { listMoviesRef, subscribe } from '@movie-app/dataconnect'; + +const unsubscribe = subscribe(listMoviesRef(), { + onNext: (result) => { + console.log('Movies updated:', result.data.movies); + }, + onError: (error) => { + console.error('Subscription error:', error); + } +}); + +// Later: unsubscribe(); +``` + +### With Authentication + +```typescript +import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'; + +const auth = getAuth(app); +await signInWithEmailAndPassword(auth, email, password); + +// SDK automatically includes auth token in requests +const myReviews = await myReviews(); // @auth(level: USER) query from examples.md +``` + +--- + +## Android SDK + +### Dependencies (build.gradle.kts) + +```kotlin +dependencies { + implementation(platform("com.google.firebase:firebase-bom:33.0.0")) + implementation("com.google.firebase:firebase-dataconnect") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0") +} +``` + +### Initialization + +```kotlin +import com.google.firebase.Firebase +import com.google.firebase.dataconnect.dataConnect +import com.example.dataconnect.MyConnector + +val connector = MyConnector.instance + +// For emulator +connector.dataConnect.useEmulator("10.0.2.2", 9399) +``` + +### Calling Operations + +```kotlin +// Query +val result = connector.listMovies.execute() +result.data.movies.forEach { movie -> + println(movie.title) +} + +// Query with variables +val movie = connector.getMovie.execute(id = "uuid-here") + +// Mutation +val newMovie = connector.createMovie.execute( + title = "New Movie", + genre = "Action" +) +``` + +### Flow Subscription + +```kotlin +connector.listMovies.flow().collect { result -> + when (result) { + is DataConnectResult.Success -> updateUI(result.data.movies) + is DataConnectResult.Error -> showError(result.exception) + } +} +``` + +--- + +## iOS SDK + +### Dependencies (Package.swift or SPM) + +```swift +dependencies: [ + .package(url: "https://github.com/firebase/firebase-ios-sdk.git", from: "11.0.0") +] +// Add FirebaseDataConnect to target dependencies +``` + +### Initialization + +```swift +import FirebaseCore +import FirebaseDataConnect + +FirebaseApp.configure() +let connector = MyConnector.shared + +// For emulator +connector.useEmulator(host: "localhost", port: 9399) +``` + +### Calling Operations + +```swift +// Query +let result = try await connector.listMovies.execute() +for movie in result.data.movies { + print(movie.title) +} + +// Query with variables +let movie = try await connector.getMovie.execute(id: "uuid-here") + +// Mutation +let newMovie = try await connector.createMovie.execute( + title: "New Movie", + genre: "Action" +) +``` + +### Combine Publisher + +```swift +connector.listMovies.publisher + .sink( + receiveCompletion: { completion in + if case .failure(let error) = completion { + print("Error: \(error)") + } + }, + receiveValue: { result in + self.movies = result.data.movies + } + ) + .store(in: &cancellables) +``` + +--- + + + +## Admin SDK + +Server-side operations with elevated privileges (bypasses @auth): + +### Node.js + +```typescript +import { initializeApp, cert } from 'firebase-admin/app'; +import { getDataConnect } from 'firebase-admin/data-connect'; + +initializeApp({ + credential: cert(serviceAccount) +}); + +const dc = getDataConnect(); + +// Execute operations (bypasses @auth) +const result = await dc.executeGraphql({ + query: `query { users { id email } }`, + operationName: 'ListAllUsers' +}); + +// Or use generated Admin SDK +import { listAllUsers } from './admin-connector'; +const users = await listAllUsers(); +``` + +### Generate Admin SDK + +In `connector.yaml`: + +```yaml +generate: + nodeAdminSdk: + outputDir: "./admin-sdk" + package: "@app/admin-dataconnect" +``` + +Generate: +```bash +firebase dataconnect:sdk:generate +``` diff --git a/skills/firebase-data-connect-basics/reference/security.md b/skills/firebase-data-connect-basics/reference/security.md new file mode 100644 index 0000000..5eacee8 --- /dev/null +++ b/skills/firebase-data-connect-basics/reference/security.md @@ -0,0 +1,289 @@ +# Security Reference + +## Contents +- [@auth Directive](#auth-directive) +- [Access Levels](#access-levels) +- [CEL Expressions](#cel-expressions) +- [@check and @redact](#check-and-redact) +- [Authorization Patterns](#authorization-patterns) +- [Anti-Patterns](#anti-patterns) + +--- + +## @auth Directive + +Every deployable query/mutation must have `@auth`. Without it, operations default to `NO_ACCESS`. + +```graphql +query PublicData @auth(level: PUBLIC) { ... } +query UserData @auth(level: USER) { ... } +query AdminOnly @auth(expr: "auth.token.admin == true") { ... } +``` + +| Argument | Description | +|----------|-------------| +| `level` | Preset access level | +| `expr` | CEL expression (alternative to level) | +| `insecureReason` | Suppress deploy warning for PUBLIC/unfiltered USER | + +--- + +## Access Levels + +| Level | Who Can Access | CEL Equivalent | +|-------|----------------|----------------| +| `PUBLIC` | Anyone, authenticated or not | `true` | +| `USER_ANON` | Any authenticated user (including anonymous) | `auth.uid != nil` | +| `USER` | Authenticated users (excludes anonymous) | `auth.uid != nil && auth.token.firebase.sign_in_provider != 'anonymous'` | +| `USER_EMAIL_VERIFIED` | Users with verified email | `auth.uid != nil && auth.token.email_verified` | +| `NO_ACCESS` | Admin SDK only | `false` | + +> **Important:** Levels like `USER` are starting points. Always add filters or expressions to verify the user can access specific data. + +--- + +## CEL Expressions + +### Available Bindings + +| Binding | Description | +|---------|-------------| +| `auth.uid` | Current user's Firebase UID | +| `auth.token` | Auth token claims (see below) | +| `vars` | Operation variables (e.g., `vars.movieId`) | +| `request.time` | Server timestamp | +| `request.operationName` | "query" or "mutation" | + +### auth.token Fields + +| Field | Description | +|-------|-------------| +| `email` | User's email address | +| `email_verified` | Boolean: email verified | +| `phone_number` | User's phone | +| `name` | Display name | +| `sub` | Firebase UID (same as auth.uid) | +| `firebase.sign_in_provider` | `password`, `google.com`, `anonymous`, etc. | +| `` | Custom claims set via Admin SDK | + +### Expression Examples + +```graphql +# Check custom claim +@auth(expr: "auth.token.role == 'admin'") + +# Check verified email domain +@auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@company.com')") + +# Check multiple conditions +@auth(expr: "auth.uid != nil && (auth.token.role == 'editor' || auth.token.role == 'admin')") + +# Check variable +@auth(expr: "has(vars.status) && vars.status in ['draft', 'published']") +``` + +### Using eq_expr in Filters + +Compare database fields with auth values: + +```graphql +query MyPosts @auth(level: USER) { + posts(where: { authorUid: { eq_expr: "auth.uid" }}) { + id title + } +} + +mutation UpdateMyPost($id: UUID!, $title: String!) @auth(level: USER) { + post_update( + first: { where: { + id: { eq: $id }, + authorUid: { eq_expr: "auth.uid" } + }}, + data: { title: $title } + ) +} +``` + +--- + +## @check and @redact + +Use `@check` to validate data and `@redact` to hide results from client: + +### @check +Validates a field value; aborts if check fails. + +```graphql +@check(expr: "this != null", message: "Not found") +@check(expr: "this == 'editor'", message: "Must be editor") +@check(expr: "this.exists(p, p.role == 'admin')", message: "No admin found") +``` + +| Argument | Description | +|----------|-------------| +| `expr` | CEL expression; `this` = current field value | +| `message` | Error message if check fails | +| `optional` | If `true`, pass when field not present | + +### @redact +Hides field from response (still evaluated for @check): + +```graphql +query @redact { ... } # Query result hidden but @check still runs +``` + +### Authorization Data Lookup + +Check database permissions before allowing mutation: + +```graphql +mutation UpdateMovie($id: UUID!, $title: String!) + @auth(level: USER) + @transaction { + # Step 1: Check user has permission + query @redact { + moviePermission( + key: { movieId: $id, userId_expr: "auth.uid" } + ) @check(expr: "this != null", message: "No access to movie") { + role @check(expr: "this == 'editor'", message: "Must be editor") + } + } + # Step 2: Update if authorized + movie_update(id: $id, data: { title: $title }) +} +``` + +### Validate Key Exists + +```graphql +mutation MustDeleteMovie($id: UUID!) @auth(level: USER) @transaction { + movie_delete(id: $id) + @check(expr: "this != null", message: "Movie not found") +} +``` + +--- + +## Authorization Patterns + +### User-Owned Resources + +```graphql +# Create with owner +mutation CreatePost($content: String!) @auth(level: USER) { + post_insert(data: { + authorUid_expr: "auth.uid", + content: $content + }) +} + +# Read own data only +query MyPosts @auth(level: USER) { + posts(where: { authorUid: { eq_expr: "auth.uid" }}) { + id content + } +} + +# Update own data only +mutation UpdatePost($id: UUID!, $content: String!) @auth(level: USER) { + post_update( + first: { where: { id: { eq: $id }, authorUid: { eq_expr: "auth.uid" }}}, + data: { content: $content } + ) +} + +# Delete own data only +mutation DeletePost($id: UUID!) @auth(level: USER) { + post_delete( + first: { where: { id: { eq: $id }, authorUid: { eq_expr: "auth.uid" }}} + ) +} +``` + +### Role-Based Access + +```graphql +# Admin-only query +query AllUsers @auth(expr: "auth.token.admin == true") { + users { id email name } +} + +# Role from database +mutation AdminAction($id: UUID!) @auth(level: USER) @transaction { + query @redact { + user(key: { uid_expr: "auth.uid" }) { + role @check(expr: "this == 'admin'", message: "Admin required") + } + } + # ... admin action +} +``` + +### Public Data with Filters + +```graphql +query PublicPosts @auth(level: PUBLIC) { + posts(where: { + visibility: { eq: "public" }, + publishedAt: { lt_expr: "request.time" } + }) { + id title content + } +} +``` + +### Tiered Access (Pro Content) + +```graphql +query ProContent @auth(expr: "auth.token.plan == 'pro'") { + posts(where: { visibility: { in: ["public", "pro"] }}) { + id title content + } +} +``` + +--- + +## Anti-Patterns + +### ❌ Don't Pass User ID as Variable + +```graphql +# BAD - any user can pass any userId +query GetUserPosts($userId: String!) @auth(level: USER) { + posts(where: { authorUid: { eq: $userId }}) { ... } +} + +# GOOD - use auth.uid +query GetMyPosts @auth(level: USER) { + posts(where: { authorUid: { eq_expr: "auth.uid" }}) { ... } +} +``` + +### ❌ Don't Use USER Without Filters + +```graphql +# BAD - any authenticated user sees all documents +query AllDocs @auth(level: USER) { + documents { id title content } +} + +# GOOD - filter to user's documents +query MyDocs @auth(level: USER) { + documents(where: { ownerId: { eq_expr: "auth.uid" }}) { ... } +} +``` + +### ❌ Don't Trust Unverified Email + +```graphql +# BAD - email not verified +@auth(expr: "auth.token.email.endsWith('@company.com')") + +# GOOD - verify email first +@auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@company.com')") +``` + +### ❌ Don't Use PUBLIC/USER for Prototyping + +During development, set operations to `NO_ACCESS` until you implement proper authorization. Use emulator and VS Code extension for testing. diff --git a/skills/firebase-data-connect-basics/templates.md b/skills/firebase-data-connect-basics/templates.md new file mode 100644 index 0000000..10f0538 --- /dev/null +++ b/skills/firebase-data-connect-basics/templates.md @@ -0,0 +1,269 @@ +# Templates + +Ready-to-use templates for common Firebase Data Connect patterns. + +--- + +## Basic CRUD Schema + +```graphql +# schema.gql +type Item @table { + id: UUID! @default(expr: "uuidV4()") + name: String! + description: String + createdAt: Timestamp! @default(expr: "request.time") + updatedAt: Timestamp! @default(expr: "request.time") +} +``` + +```graphql +# queries.gql +query ListItems @auth(level: PUBLIC) { + items(orderBy: [{ createdAt: DESC }]) { + id name description createdAt + } +} + +query GetItem($id: UUID!) @auth(level: PUBLIC) { + item(id: $id) { id name description createdAt updatedAt } +} +``` + +```graphql +# mutations.gql +mutation CreateItem($name: String!, $description: String) @auth(level: USER) { + item_insert(data: { name: $name, description: $description }) +} + +mutation UpdateItem($id: UUID!, $name: String, $description: String) @auth(level: USER) { + item_update(id: $id, data: { + name: $name, + description: $description, + updatedAt_expr: "request.time" + }) +} + +mutation DeleteItem($id: UUID!) @auth(level: USER) { + item_delete(id: $id) +} +``` + +--- + +## User-Owned Resources + +```graphql +# schema.gql +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + email: String! @unique + displayName: String +} + +type Note @table { + id: UUID! @default(expr: "uuidV4()") + owner: User! + title: String! + content: String + createdAt: Timestamp! @default(expr: "request.time") +} +``` + +```graphql +# queries.gql +query MyNotes @auth(level: USER) { + notes( + where: { owner: { uid: { eq_expr: "auth.uid" }}}, + orderBy: [{ createdAt: DESC }] + ) { id title content createdAt } +} + +query GetMyNote($id: UUID!) @auth(level: USER) { + note( + first: { where: { + id: { eq: $id }, + owner: { uid: { eq_expr: "auth.uid" }} + }} + ) { id title content } +} +``` + +```graphql +# mutations.gql +mutation CreateNote($title: String!, $content: String) @auth(level: USER) { + note_insert(data: { + owner: { uid_expr: "auth.uid" }, + title: $title, + content: $content + }) +} + +mutation UpdateNote($id: UUID!, $title: String, $content: String) @auth(level: USER) { + note_update( + first: { where: { id: { eq: $id }, owner: { uid: { eq_expr: "auth.uid" }}}}, + data: { title: $title, content: $content } + ) +} + +mutation DeleteNote($id: UUID!) @auth(level: USER) { + note_delete( + first: { where: { id: { eq: $id }, owner: { uid: { eq_expr: "auth.uid" }}}} + ) +} +``` + +--- + +## Many-to-Many Relationship + +```graphql +# schema.gql +type Tag @table { + id: UUID! @default(expr: "uuidV4()") + name: String! @unique +} + +type Article @table { + id: UUID! @default(expr: "uuidV4()") + title: String! + content: String! +} + +type ArticleTag @table(key: ["article", "tag"]) { + article: Article! + tag: Tag! +} +``` + +```graphql +# queries.gql +query ArticlesByTag($tagName: String!) @auth(level: PUBLIC) { + articles(where: { + articleTags_on_article: { tag: { name: { eq: $tagName }}} + }) { + id title + tags: tags_via_ArticleTag { name } + } +} + +query ArticleWithTags($id: UUID!) @auth(level: PUBLIC) { + article(id: $id) { + id title content + tags: tags_via_ArticleTag { id name } + } +} +``` + +```graphql +# mutations.gql +mutation AddTagToArticle($articleId: UUID!, $tagId: UUID!) @auth(level: USER) { + articleTag_insert(data: { + article: { id: $articleId }, + tag: { id: $tagId } + }) +} + +mutation RemoveTagFromArticle($articleId: UUID!, $tagId: UUID!) @auth(level: USER) { + articleTag_delete(key: { articleId: $articleId, tagId: $tagId }) +} +``` + +--- + +## dataconnect.yaml Template + +```yaml +specVersion: "v1" +serviceId: "my-service" +location: "us-central1" +schema: + source: "./schema" + datasource: + postgresql: + database: "fdcdb" + cloudSql: + instanceId: "my-instance" +connectorDirs: ["./connector"] +``` + +--- + +## connector.yaml Template + +```yaml +connectorId: "default" +generate: + javascriptSdk: + outputDir: "../web/src/lib/dataconnect" + package: "@myapp/dataconnect" + kotlinSdk: + outputDir: "../android/app/src/main/kotlin/com/myapp/dataconnect" + package: "com.myapp.dataconnect" + swiftSdk: + outputDir: "../ios/MyApp/DataConnect" + dartSdk: + outputDir: "../flutter/lib/dataconnect" + package: myapp_dataconnect +``` + +--- + +## Firebase Init Commands + +```bash +# Initialize Data Connect in project +firebase init dataconnect + +# Initialize with specific project +firebase use +firebase init dataconnect + +# Start emulator for development +firebase emulators:start --only dataconnect + +# Generate SDKs +firebase dataconnect:sdk:generate + +# Deploy to production +firebase deploy --only dataconnect +``` + +--- + +## SDK Initialization (Web) + +```typescript +// lib/firebase.ts +import { initializeApp } from 'firebase/app'; +import { getAuth } from 'firebase/auth'; +import { getDataConnect, connectDataConnectEmulator } from 'firebase/data-connect'; +import { connectorConfig } from '@myapp/dataconnect'; + +const firebaseConfig = { + apiKey: "...", + authDomain: "...", + projectId: "...", +}; + +export const app = initializeApp(firebaseConfig); +export const auth = getAuth(app); +export const dataConnect = getDataConnect(app, connectorConfig); + +// Connect to emulator in development +if (import.meta.env.DEV) { + connectDataConnectEmulator(dataConnect, 'localhost', 9399); +} +``` + +```typescript +// Example usage +import { listItems, createItem } from '@myapp/dataconnect'; + +// List items +const { data } = await listItems(); +console.log(data.items); + +// Create item (requires auth) +await createItem({ name: 'New Item', description: 'Description' }); +``` From 88480acf965c611af09710bfd9ea9b78da59dc41 Mon Sep 17 00:00:00 2001 From: Muhammad Talha <126821605+mtr002@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:14:53 -0800 Subject: [PATCH 02/10] [Dataconnect] Restructure SKILL.md into a development workflow guide --- skills/firebase-data-connect-basics/SKILL.md | 195 +++++------------- .../reference/sdks.md | 12 ++ 2 files changed, 65 insertions(+), 142 deletions(-) diff --git a/skills/firebase-data-connect-basics/SKILL.md b/skills/firebase-data-connect-basics/SKILL.md index 7d22d3b..ef4e6e9 100644 --- a/skills/firebase-data-connect-basics/SKILL.md +++ b/skills/firebase-data-connect-basics/SKILL.md @@ -7,27 +7,6 @@ description: Build and deploy Firebase Data Connect backends with PostgreSQL. Us Firebase Data Connect is a relational database service using Cloud SQL for PostgreSQL with GraphQL schema, auto-generated queries/mutations, and type-safe SDKs. -## Quick Start - -```graphql -# schema.gql - Define your data model -type Movie @table { - id: UUID! @default(expr: "uuidV4()") - title: String! - releaseYear: Int - genre: String -} - -# queries.gql - Define operations -query ListMovies @auth(level: PUBLIC) { - movies { id title genre } -} - -mutation CreateMovie($title: String!, $genre: String) @auth(level: USER) { - movie_insert(data: { title: $title, genre: $genre }) -} -``` - ## Project Structure ``` @@ -41,144 +20,76 @@ dataconnect/ └── mutations.gql # Mutations ``` -## Core Concepts - -| Concept | Description | -|---------|-------------| -| **Schema** | GraphQL types with `@table` → PostgreSQL tables | -| **Connector** | Collection of queries/mutations as API endpoints | -| **Generated Fields** | Auto-generated `movie`, `movies`, `movie_insert`, `movie_update`, `movie_delete` | -| **Key Scalars** | `Movie_Key` type for record identification | -| **@auth** | Authorization directive: `PUBLIC`, `USER`, `USER_EMAIL_VERIFIED`, `NO_ACCESS` | - -## Detailed References - -**Design your data model** → See [schema.md](reference/schema.md) -- Types, @table, @col, @default directives -- Relationships with @ref (one-to-one, one-to-many, many-to-many) -- Data types: UUID, String, Int, Int64, Float, Boolean, Date, Timestamp, Vector - -**Build queries and mutations** → See [operations.md](reference/operations.md) -- Generated fields and key scalars -- Filtering with `where`, `orderBy`, `limit` -- Relational queries with `_on_` and `_via_` syntax -- Multi-step mutations with `@transaction` - -**Secure your operations** → See [security.md](reference/security.md) -- @auth directive and access levels -- CEL expressions for custom authorization -- @check and @redact for data lookup authorization -- Common authorization patterns and anti-patterns - -**Integrate with client apps** → See [sdks.md](reference/sdks.md) -- Web, Android, iOS, Flutter SDK usage -- SDK generation with Firebase CLI -- Calling queries/mutations from client code - -**Configure and deploy** → See [config.md](reference/config.md) -- dataconnect.yaml and connector.yaml structure -- Firebase CLI commands -- Local emulator setup -- Deployment workflow - -**Advanced features** → See [advanced.md](reference/advanced.md) -- Vector similarity search with Vertex AI embeddings -- Full-text search with @searchable directive -- Cloud Functions integration (mutation triggers) -- Data seeding and bulk operations - -## Common Patterns - -### User-Owned Resources - -```graphql -type Post @table { - id: UUID! @default(expr: "uuidV4()") - authorUid: String! @default(expr: "auth.uid") - content: String! -} - -mutation CreatePost($content: String!) @auth(level: USER) { - post_insert(data: { authorUid_expr: "auth.uid", content: $content }) -} - -query MyPosts @auth(level: USER) { - posts(where: { authorUid: { eq_expr: "auth.uid" }}) { id content } -} -``` - -### Many-to-Many Relationship - -```graphql -type Movie @table { - id: UUID! @default(expr: "uuidV4()") - title: String! -} - -type Actor @table { - id: UUID! @default(expr: "uuidV4()") - name: String! -} +## Development Workflow + +Follow this strict workflow to build your application. You **must** read the linked reference files for each step to understand the syntax and available features. + +### 1. Define Data Model (`schema/schema.gql`) +Define your GraphQL types, tables, and relationships. +> **Read [reference/schema.md](reference/schema.md)** for: +> * `@table`, `@col`, `@default` +> * Relationships (`@ref`, one-to-many, many-to-many) +> * Data types (UUID, Vector, JSON, etc.) + +### 2. Define Operations (`connector/queries.gql`, `connector/mutations.gql`) +Write the queries and mutations your client will use. Data Connect generates the underlying SQL. +> **Read [reference/operations.md](reference/operations.md)** for: +> * **Queries**: Filtering (`where`), Ordering (`orderBy`), Pagination (`limit`/`offset`). +> * **Mutations**: Create (`_insert`), Update (`_update`), Delete (`_delete`). +> * **Upserts**: Use `_upsert` to "insert or update" records (CRITICAL for user profiles). +> * **Transactions**: use `@transaction` for multi-step atomic operations. + +### 3. Secure Your App (`connector/` files) +Add authorization logic closely with your operations. +> **Read [reference/security.md](reference/security.md)** for: +> * `@auth(level: ...)` for PUBLIC, USER, or NO_ACCESS. +> * `@check` and `@redact` for row-level security and validation. + +### 4. Generate & Use SDKs +Generate type-safe code for your client platform. +> **Read [reference/sdks.md](reference/sdks.md)** for: +> * Android (Kotlin), iOS (Swift), Web (TypeScript), Flutter (Dart). +> * How to initialize and call your queries/mutations. +> * **Nested Data**: See how to access related fields (e.g., `movie.reviews`). -type MovieActor @table(key: ["movie", "actor"]) { - movie: Movie! - actor: Actor! - role: String! -} -``` +--- -### Filtered Queries +## Feature Capability Map -```graphql -query MoviesByGenre($genre: String!, $minRating: Int) @auth(level: PUBLIC) { - movies( - where: { genre: { eq: $genre }, rating: { ge: $minRating }}, - orderBy: [{ rating: DESC }], - limit: 10 - ) { id title rating } -} -``` +If you need to implement a specific feature, consult the mapped reference file: -## Examples & Templates +| Feature | Reference File | Key Concepts | +| :--- | :--- | :--- | +| **Data Modeling** | [reference/schema.md](reference/schema.md) | `@table`, `@unique`, `@index`, Relations | +| **Vector Search** | [reference/advanced.md](reference/advanced.md) | `Vector`, `@col(dataType: "vector")` | +| **Full-Text Search** | [reference/advanced.md](reference/advanced.md) | `@searchable` | +| **Upserting Data** | [reference/operations.md](reference/operations.md) | `_upsert` mutations | +| **Complex Filters** | [reference/operations.md](reference/operations.md) | `_or`, `_and`, `_not`, `eq`, `contains` | +| **Transactions** | [reference/operations.md](reference/operations.md) | `@transaction`, `response` binding | +| **Environment Config** | [reference/config.md](reference/config.md) | `dataconnect.yaml`, `connector.yaml` | -**Complete working examples** → See [examples.md](examples.md) -**Ready-to-use templates** → See [templates.md](templates.md) +--- -## MCP Tools Available +## Deployment & CLI -- `firebase_init` - Initialize Data Connect with `dataconnect` feature -- `firebase_get_sdk_config` - Get Firebase configuration for client apps -- `firebase_get_project` - Get current project information -- `firebase_update_environment` - Set project directory and active project +> **Read [reference/config.md](reference/config.md)** for deep dive on configuration. -## CLI Commands +Common commands (run from project root): ```bash # Initialize Data Connect firebase init dataconnect -# Start emulator for local development +# Start local emulator firebase emulators:start --only dataconnect -# Generate SDKs +# Generate SDK code firebase dataconnect:sdk:generate -# Deploy to Firebase +# Deploy to production firebase deploy --only dataconnect ``` -## Key Directives Quick Reference - -| Directive | Purpose | Example | -|-----------|---------|---------| -| `@table` | Define PostgreSQL table | `type Movie @table { ... }` | -| `@col` | Customize column name/type | `@col(name: "movie_id", dataType: "serial")` | -| `@default` | Set default value | `@default(expr: "uuidV4()")` | -| `@ref` | Foreign key reference | `author: User!` (implicit) or `@ref(fields: "authorId")` | -| `@unique` | Unique constraint | `email: String! @unique` | -| `@index` | Database index | `title: String! @index` | -| `@searchable` | Enable full-text search | `title: String! @searchable` | -| `@auth` | Authorization level | `@auth(level: USER)` or `@auth(expr: "auth.uid != nil")` | -| `@check` | Validate field in mutation | `@check(expr: "this != null", message: "Not found")` | -| `@redact` | Hide field from response | Used with @check for auth lookups | -| `@transaction` | Atomic multi-step mutation | `mutation Multi @transaction { ... }` | +## Examples + +For complete, working code examples of schemas and operations, see **[examples.md](examples.md)**. diff --git a/skills/firebase-data-connect-basics/reference/sdks.md b/skills/firebase-data-connect-basics/reference/sdks.md index ff39dcc..5651153 100644 --- a/skills/firebase-data-connect-basics/reference/sdks.md +++ b/skills/firebase-data-connect-basics/reference/sdks.md @@ -63,6 +63,12 @@ if (import.meta.env.DEV) { // Generated SDK provides typed functions import { listMovies, createMovie, getMovie } from '@movie-app/dataconnect'; +// Accessing Nested Fields +const movie = await getMovie({ id: '...' }); +// Relations are just properties on the object +const director = movie.data.movie.metadata.director; +const firstActor = movie.data.movie.actors[0].name; + // Query const result = await listMovies(); console.log(result.data.movies); @@ -142,6 +148,9 @@ connector.dataConnect.useEmulator("10.0.2.2", 9399) val result = connector.listMovies.execute() result.data.movies.forEach { movie -> println(movie.title) + // Access nested fields directly + println(movie.metadata?.director) + println(movie.actors.firstOrNull()?.name) } // Query with variables @@ -198,6 +207,9 @@ connector.useEmulator(host: "localhost", port: 9399) let result = try await connector.listMovies.execute() for movie in result.data.movies { print(movie.title) + // Access nested fields directly + print(movie.metadata?.director ?? "Unknown") + print(movie.actors.first?.name ?? "No actors") } // Query with variables From b9eaeabab64b5ea3972b22e964adb58efb4026f5 Mon Sep 17 00:00:00 2001 From: Vinay Guthal Date: Mon, 30 Mar 2026 13:51:27 -0400 Subject: [PATCH 03/10] add android skills --- skills/firebase-ai-logic-basics/SKILL.md | 2 + .../references/usage_patterns_android.md | 108 +++++++++++++ skills/firebase-auth-basics/SKILL.md | 3 + .../references/client_sdk_android.md | 123 +++++++++++++++ skills/firebase-basics/SKILL.md | 2 +- .../references/android_setup.md | 53 +++++++ .../SKILL.md | 1 + .../references/android_sdk_usage.md | 102 ++++++++++++ skills/firebase-firestore-standard/SKILL.md | 1 + .../references/android_sdk_usage.md | 149 ++++++++++++++++++ 10 files changed, 543 insertions(+), 1 deletion(-) create mode 100644 skills/firebase-ai-logic-basics/references/usage_patterns_android.md create mode 100644 skills/firebase-auth-basics/references/client_sdk_android.md create mode 100644 skills/firebase-basics/references/android_setup.md create mode 100644 skills/firebase-firestore-enterprise-native-mode/references/android_sdk_usage.md create mode 100644 skills/firebase-firestore-standard/references/android_sdk_usage.md diff --git a/skills/firebase-ai-logic-basics/SKILL.md b/skills/firebase-ai-logic-basics/SKILL.md index f7ec367..d5f4c5f 100644 --- a/skills/firebase-ai-logic-basics/SKILL.md +++ b/skills/firebase-ai-logic-basics/SKILL.md @@ -105,5 +105,7 @@ Consider that you do not need to hardcode model names (e.g., `gemini-flash-lite- [Web SDK code examples and usage patterns](references/usage_patterns_web.md) +[Android (Kotlin) SDK usage patterns](references/usage_patterns_android.md) + diff --git a/skills/firebase-ai-logic-basics/references/usage_patterns_android.md b/skills/firebase-ai-logic-basics/references/usage_patterns_android.md new file mode 100644 index 0000000..f3431cd --- /dev/null +++ b/skills/firebase-ai-logic-basics/references/usage_patterns_android.md @@ -0,0 +1,108 @@ +# Firebase AI Logic on Android (Kotlin) + +This guide walks you through using Firebase AI Logic (accessing Gemini models) in your Android app using Kotlin. + +### 1. Add Dependencies + +In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add the dependency for Firebase AI: + +```kotlin +dependencies { + // Import the BoM (verify latest version) + implementation(platform("com.google.firebase:firebase-bom:33.0.0")) + + // Add the dependency for the Firebase AI library + implementation("com.google.firebase:firebase-ai") +} +``` + +--- + +### 2. Initialize and Generate Content + +In your Activity or Fragment, initialize the `FirebaseAI` service and generate content using a Gemini model: + +```kotlin +import com.google.firebase.ai.FirebaseAI +import com.google.firebase.ai.ktx.ai +import com.google.firebase.ktx.Firebase + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + // Initialize Firebase AI + val ai = Firebase.ai + + // Use a model (e.g., gemini-2.5-flash) + val model = ai.generativeModel("gemini-2.5-flash") + + // Generate content + lifecycleScope.launch { + try { + val response = model.generateContent("Write a story about a magic backpack.") + Log.d(TAG, "Response: ${response.text}") + } catch (e: Exception) { + Log.e(TAG, "Error generating content", e) + } + } + } +} +``` + +--- + +### 3. Multimodal Input (Text and Images) + +Pass bitmap data along with text prompts: + +```kotlin +val image1: Bitmap = ... // Load your bitmap +val image2: Bitmap = ... + +val response = model.generateContent( + content("Analyze these images for me") { + image(image1) + image(image2) + text("Compare these two items.") + } +) +Log.d(TAG, response.text) +``` + +--- + +### 4. Chat Session (Multi-turn) + +Maintain chat history automatically: + +```kotlin +val chat = model.startChat( + history = listOf( + content("user") { text("Hello, I am a software engineer.") }, + content("model") { text("Hello! How can I help you today?") } + ) +) + +lifecycleScope.launch { + val response = chat.sendMessage("What should I learn next?") + Log.d(TAG, response.text) +} +``` + +--- + +### 5. Streaming Responses + +For faster display, stream the response: + +```kotlin +lifecycleScope.launch { + model.generateContentStream("Tell me a long story.") + .collect { chunk -> + print(chunk.text) // Update UI incrementally + } +} +``` diff --git a/skills/firebase-auth-basics/SKILL.md b/skills/firebase-auth-basics/SKILL.md index 6a73957..f8e950c 100644 --- a/skills/firebase-auth-basics/SKILL.md +++ b/skills/firebase-auth-basics/SKILL.md @@ -79,6 +79,9 @@ Enable other providers in the Firebase Console. **Web** See [references/client_sdk_web.md](references/client_sdk_web.md). +**Android (Kotlin)** +See [references/client_sdk_android.md](references/client_sdk_android.md). + ### 3. Security Rules Secure your data using `request.auth` in Firestore/Storage rules. diff --git a/skills/firebase-auth-basics/references/client_sdk_android.md b/skills/firebase-auth-basics/references/client_sdk_android.md new file mode 100644 index 0000000..1e667ea --- /dev/null +++ b/skills/firebase-auth-basics/references/client_sdk_android.md @@ -0,0 +1,123 @@ +# Firebase Authentication on Android (Kotlin) + +This guide walks you through using Firebase Authentication in your Android app using Kotlin DSL (`build.gradle.kts`) and Kotlin code. + +### 1. Enable Authentication in the Firebase Console + +Before you begin, make sure you have enabled the sign-in providers you want to use in the Firebase Console: +1. Go to **Build > Authentication > Sign-in method**. +2. Enable **Email/Password** or **Google** (or any other provider you plan to use). + +### 2. Add Dependencies + +In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add the dependency for Firebase Authentication: + +```kotlin +dependencies { + // Import the BoM (verify latest version) + implementation(platform("com.google.firebase:firebase-bom:33.0.0")) + + // Add the dependency for the Firebase Authentication library + // When using the BoM, you don't specify versions in Firebase library dependencies + implementation("com.google.firebase:firebase-auth") +} +``` + +--- + +### 3. Initialize FirebaseAuth + +In your Activity or Fragment, initialize the `FirebaseAuth` instance: + +```kotlin +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.ktx.auth +import com.google.firebase.ktx.Firebase + +class MainActivity : AppCompatActivity() { + + private lateinit var auth: FirebaseAuth + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + // Initialize Firebase Auth + auth = Firebase.auth + } +} +``` + +--- + +### 4. Check Current Auth State + +You should check if a user is already signed in when your activity starts: + +```kotlin +public override fun onStart() { + super.onStart() + // Check if user is signed in (non-null) and update UI accordingly. + val currentUser = auth.currentUser + if (currentUser != null) { + // User is signed in, navigate to main screen or update UI + } else { + // No user is signed in, prompt for login + } +} +``` + +--- + +### 5. Sign Up New Users (Email/Password) + +Use `createUserWithEmailAndPassword` to register new users: + +```kotlin +fun signUpUser(email: String, password: String) { + auth.createUserWithEmailAndPassword(email, password) + .addOnCompleteListener(this) { task -> + if (task.isSuccessful) { + // Sign up success, update UI with the signed-in user's information + val user = auth.currentUser + // Navigate to main screen + } else { + // If sign up fails, display a message to the user. + Toast.makeText(baseContext, "Authentication failed.", Toast.LENGTH_SHORT).show() + } + } +} +``` + +--- + +### 6. Sign In Existing Users (Email/Password) + +Use `signInWithEmailAndPassword` to log in existing users: + +```kotlin +fun signInUser(email: String, password: String) { + auth.signInWithEmailAndPassword(email, password) + .addOnCompleteListener(this) { task -> + if (task.isSuccessful) { + // Sign in success, update UI with the signed-in user's information + val user = auth.currentUser + // Navigate to main screen + } else { + // If sign in fails, display a message to the user. + Toast.makeText(baseContext, "Authentication failed.", Toast.LENGTH_SHORT).show() + } + } +} +``` + +--- + +### 7. Sign Out + +To sign out a user, call `signOut()` on the `FirebaseAuth` instance: + +```kotlin +auth.signOut() +// Navigate to login screen +``` diff --git a/skills/firebase-basics/SKILL.md b/skills/firebase-basics/SKILL.md index b192ad6..563f90b 100644 --- a/skills/firebase-basics/SKILL.md +++ b/skills/firebase-basics/SKILL.md @@ -45,7 +45,7 @@ Please adhere to these principles when working with Firebase, as they ensure rel - **Initialize Firebase:** See [references/firebase-service-init.md](references/firebase-service-init.md) when you need to initialize new Firebase services using the CLI. - **Exploring Commands:** See [references/firebase-cli-guide.md](references/firebase-cli-guide.md) to discover and understand CLI functionality. -- **SDK Setup:** For detailed guides on adding Firebase to a web app, see [references/web_setup.md](references/web_setup.md). +- **SDK Setup:** For detailed guides on adding Firebase to a web app, see [references/web_setup.md](references/web_setup.md), or for an **Android app**, see [references/android_setup.md](references/android_setup.md). # Common Issues diff --git a/skills/firebase-basics/references/android_setup.md b/skills/firebase-basics/references/android_setup.md new file mode 100644 index 0000000..1ac6075 --- /dev/null +++ b/skills/firebase-basics/references/android_setup.md @@ -0,0 +1,53 @@ +# Adding Firebase to your Android App + +This guide walks you through adding Firebase to your Android project using Kotlin DSL (`build.gradle.kts`). + +### 1. Register your app in the Firebase Console + +1. Go to the [Firebase Console](https://console.firebase.google.com/). +2. Select your Firebase project. +3. Click the **Android icon** to add a new app. +4. Enter your app's package name (e.g., `com.example.myapp`) and follow the workflow. +5. Download the `google-services.json` file and place it in your app module directory (usually `app/`). + +--- + +### 2. Configure Gradle Files + +#### Project-level `build.gradle.kts` +Add the Google Services plugin to your root-level `build.gradle.kts` file: + +```kotlin +plugins { + // ... other plugins + id("com.google.gms.google-services") version "4.4.2" apply false +} +``` + +#### Module-level (app) `build.gradle.kts` +Apply the plugin and add the Firebase BOM (Bill of Materials) to manage your Firebase library versions: + +```kotlin +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + // Add the Google services plugin + id("com.google.gms.google-services") +} + +dependencies { + // Import the Firebase BoM + implementation(platform("com.google.firebase:firebase-bom:33.0.0")) // Check for latest version + + // Add the dependency for the Firebase SDKs you want to use + // When using the BoM, don't specify versions in Firebase library dependencies + implementation("com.google.firebase:firebase-analytics") + + // Add other Firebase products as needed (e.g., Auth, Firestore) + // implementation("com.google.firebase:firebase-auth") +} +``` + +### Next Steps: +* **Sync your project:** In Android Studio, click **"Sync Now"** in the notification bar that appears after file changes. +* **Verify Connection:** Run your app to send verification to the Firebase console that you've successfully installed the SDK. diff --git a/skills/firebase-firestore-enterprise-native-mode/SKILL.md b/skills/firebase-firestore-enterprise-native-mode/SKILL.md index 19410f4..5487e52 100644 --- a/skills/firebase-firestore-enterprise-native-mode/SKILL.md +++ b/skills/firebase-firestore-enterprise-native-mode/SKILL.md @@ -24,6 +24,7 @@ For guidance on writing and deploying Firestore Security Rules to protect your d To learn how to use Firestore Enterprise Native Mode in your application code, see: - [Web SDK Usage](references/web_sdk_usage.md) +- [Android (Kotlin) SDK Usage](references/android_sdk_usage.md) - [Python SDK Usage](references/python_sdk_usage.md) ## Indexes diff --git a/skills/firebase-firestore-enterprise-native-mode/references/android_sdk_usage.md b/skills/firebase-firestore-enterprise-native-mode/references/android_sdk_usage.md new file mode 100644 index 0000000..8177911 --- /dev/null +++ b/skills/firebase-firestore-enterprise-native-mode/references/android_sdk_usage.md @@ -0,0 +1,102 @@ +# Firestore Enterprise Native Mode on Android (Kotlin) + +This guide walks you through using the Cloud Firestore SDK in your Android app using Kotlin. The SDK for Firestore Enterprise Native Mode is the same as the standard Cloud Firestore SDK. + +### 1. Add Dependencies + +In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add the dependency for Cloud Firestore: + +```kotlin +dependencies { + // Import the BoM (verify latest version) + implementation(platform("com.google.firebase:firebase-bom:33.0.0")) + + // Add the dependency for the Cloud Firestore library + implementation("com.google.firebase:firebase-firestore") +} +``` + +--- + +### 2. Initialize Firestore + +In your Activity or Fragment, initialize the `FirebaseFirestore` instance: + +```kotlin +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.ktx.Firebase + +class MainActivity : AppCompatActivity() { + + private lateinit var db: FirebaseFirestore + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + // Initialize Firestore + db = Firebase.firestore + } +} +``` + +--- + +### 3. Basic CRUD Operations + +The operations are identical to standard Firestore. + +#### Add Data + +```kotlin +val user = hashMapOf( + "first" to "Alan", + "last" to "Turing", + "born" to 1912 +) + +db.collection("users") + .add(user) + .addOnSuccessListener { documentReference -> + Log.d(TAG, "DocumentSnapshot added with ID: ${documentReference.id}") + } + .addOnFailureListener { e -> + Log.w(TAG, "Error adding document", e) + } +``` + +#### Read Data + +```kotlin +db.collection("users") + .get() + .addOnSuccessListener { result -> + for (document in result) { + Log.d(TAG, "${document.id} => ${document.data}") + } + } + .addOnFailureListener { exception -> + Log.w(TAG, "Error getting documents.", exception) + } +``` + +#### Update Data + +```kotlin +val userRef = db.collection("users").document("your-document-id") + +userRef + .update("born", 1913) + .addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully updated!") } + .addOnFailureListener { e -> Log.w(TAG, "Error updating document", e) } +``` + +#### Delete Data + +```kotlin +db.collection("users").document("your-document-id") + .delete() + .addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully deleted!") } + .addOnFailureListener { e -> Log.w(TAG, "Error deleting document", e) } +``` diff --git a/skills/firebase-firestore-standard/SKILL.md b/skills/firebase-firestore-standard/SKILL.md index 55742d3..ba17add 100644 --- a/skills/firebase-firestore-standard/SKILL.md +++ b/skills/firebase-firestore-standard/SKILL.md @@ -21,6 +21,7 @@ For guidance on writing and deploying Firestore Security Rules to protect your d 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) ## Indexes diff --git a/skills/firebase-firestore-standard/references/android_sdk_usage.md b/skills/firebase-firestore-standard/references/android_sdk_usage.md new file mode 100644 index 0000000..45fe06a --- /dev/null +++ b/skills/firebase-firestore-standard/references/android_sdk_usage.md @@ -0,0 +1,149 @@ +# Cloud Firestore on Android (Kotlin) + +This guide walks you through using Cloud Firestore in your Android app using Kotlin. + +### 1. Add Dependencies + +In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add the dependency for Cloud Firestore: + +```kotlin +dependencies { + // Import the BoM (verify latest version) + implementation(platform("com.google.firebase:firebase-bom:33.0.0")) + + // Add the dependency for the Cloud Firestore library + // When using the BoM, you don't specify versions in Firebase library dependencies + implementation("com.google.firebase:firebase-firestore") +} +``` + +--- + +### 2. Initialize Firestore + +In your Activity or Fragment, initialize the `FirebaseFirestore` instance: + +```kotlin +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.ktx.Firebase + +class MainActivity : AppCompatActivity() { + + private lateinit var db: FirebaseFirestore + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + // Initialize Firestore + db = Firebase.firestore + } +} +``` + +--- + +### 3. Add Data + +Add a new document with a generated ID using `add()`: + +```kotlin +// Create a new user with a first and last name +val user = hashMapOf( + "first" to "Ada", + "last" to "Lovelace", + "born" to 1815 +) + +// Add a new document with a generated ID +db.collection("users") + .add(user) + .addOnSuccessListener { documentReference -> + Log.d(TAG, "DocumentSnapshot added with ID: ${documentReference.id}") + } + .addOnFailureListener { e -> + Log.w(TAG, "Error adding document", e) + } +``` + +Or set a document with a specific ID using `set()`: + +```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) } +``` + +--- + +### 4. Read Data + +Read a single document using `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") + } + } + .addOnFailureListener { exception -> + Log.d(TAG, "get failed with ", exception) + } +``` + +Read multiple documents using a query: + +```kotlin +db.collection("cities") + .whereEqualTo("capital", true) + .get() + .addOnSuccessListener { documents -> + for (document in documents) { + Log.d(TAG, "${document.id} => ${document.data}") + } + } + .addOnFailureListener { exception -> + Log.w(TAG, "Error getting documents: ", exception) + } +``` + +--- + +### 5. Update Data + +Update some fields of a document using `update()` without overwriting the entire document: + +```kotlin +val washingtonRef = db.collection("cities").document("DC") + +// Set the "isCapital" field to true +washingtonRef + .update("capital", true) + .addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully updated!") } + .addOnFailureListener { e -> Log.w(TAG, "Error updating document", e) } +``` + +--- + +### 6. Delete Data + +Delete a document using `delete()`: + +```kotlin +db.collection("cities").document("DC") + .delete() + .addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully deleted!") } + .addOnFailureListener { e -> Log.w(TAG, "Error deleting document", e) } +``` From 9aa0ce68e7c5db3a0fffd026c9cd5d3812a2519a Mon Sep 17 00:00:00 2001 From: Vinay Guthal Date: Mon, 30 Mar 2026 14:05:16 -0400 Subject: [PATCH 04/10] small edits --- .../references/usage_patterns_android.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/firebase-ai-logic-basics/references/usage_patterns_android.md b/skills/firebase-ai-logic-basics/references/usage_patterns_android.md index f3431cd..ce43dec 100644 --- a/skills/firebase-ai-logic-basics/references/usage_patterns_android.md +++ b/skills/firebase-ai-logic-basics/references/usage_patterns_android.md @@ -36,8 +36,8 @@ class MainActivity : AppCompatActivity() { // Initialize Firebase AI val ai = Firebase.ai - // Use a model (e.g., gemini-2.5-flash) - val model = ai.generativeModel("gemini-2.5-flash") + // Use a model (e.g., gemini-2.5-flash-lite) + val model = ai.generativeModel("gemini-2.5-flash-lite") // Generate content lifecycleScope.launch { From 8cba6d22a0be12e9b0d0ba04b8fa178b37e3d841 Mon Sep 17 00:00:00 2001 From: Vinay Guthal Date: Tue, 31 Mar 2026 14:48:00 -0400 Subject: [PATCH 05/10] Update instructions to include firebase cli --- .idea/agent-skills.iml | 9 ++ .../references/usage_patterns_android.md | 13 ++- .../references/client_sdk_android.md | 10 +++ .../references/android_setup.md | 84 +++++++++---------- .../references/android_sdk_usage.md | 10 +++ 5 files changed, 80 insertions(+), 46 deletions(-) create mode 100644 .idea/agent-skills.iml diff --git a/.idea/agent-skills.iml b/.idea/agent-skills.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/agent-skills.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/skills/firebase-ai-logic-basics/references/usage_patterns_android.md b/skills/firebase-ai-logic-basics/references/usage_patterns_android.md index ce43dec..daeb63a 100644 --- a/skills/firebase-ai-logic-basics/references/usage_patterns_android.md +++ b/skills/firebase-ai-logic-basics/references/usage_patterns_android.md @@ -1,6 +1,17 @@ # Firebase AI Logic on Android (Kotlin) -This guide walks you through using Firebase AI Logic (accessing Gemini models) in your Android app using Kotlin. +First, ensure you have initialized the Firebase App (see `firebase-basics` skill). Then, initialize +the AI Logic service as below +### 0. Enable Firebase AI Logic via CLI + +Before adding dependencies in your app, make sure you enable the AI Logic service in your Firebase Project using the Firebase CLI: + +```bash +npx -y firebase-tools@latest init +# When prompted, select 'AI logic' to enable the Gemini API in your project. +``` + + --- ### 1. Add Dependencies diff --git a/skills/firebase-auth-basics/references/client_sdk_android.md b/skills/firebase-auth-basics/references/client_sdk_android.md index 1e667ea..93e2fc1 100644 --- a/skills/firebase-auth-basics/references/client_sdk_android.md +++ b/skills/firebase-auth-basics/references/client_sdk_android.md @@ -2,6 +2,16 @@ This guide walks you through using Firebase Authentication in your Android app using Kotlin DSL (`build.gradle.kts`) and Kotlin code. +### Enable Authentication via CLI + +Before adding dependencies in your app, make sure you enable the Auth service in your Firebase Project using the Firebase CLI: + +```bash +npx -y firebase-tools@latest init auth +``` + + --- + ### 1. Enable Authentication in the Firebase Console Before you begin, make sure you have enabled the sign-in providers you want to use in the Firebase Console: diff --git a/skills/firebase-basics/references/android_setup.md b/skills/firebase-basics/references/android_setup.md index 1ac6075..045381c 100644 --- a/skills/firebase-basics/references/android_setup.md +++ b/skills/firebase-basics/references/android_setup.md @@ -1,53 +1,47 @@ -# Adding Firebase to your Android App - -This guide walks you through adding Firebase to your Android project using Kotlin DSL (`build.gradle.kts`). - -### 1. Register your app in the Firebase Console - -1. Go to the [Firebase Console](https://console.firebase.google.com/). -2. Select your Firebase project. -3. Click the **Android icon** to add a new app. -4. Enter your app's package name (e.g., `com.example.myapp`) and follow the workflow. -5. Download the `google-services.json` file and place it in your app module directory (usually `app/`). +# 🛠️ Firebase Android Setup Guide +--- +## 📋 Prerequisites +Before running these commands, ensure you are authenticated: +` firebase login` (or `firebase login --no-localhost` on remote servers) --- -### 2. Configure Gradle Files - -#### Project-level `build.gradle.kts` -Add the Google Services plugin to your root-level `build.gradle.kts` file: - -```kotlin -plugins { - // ... other plugins - id("com.google.gms.google-services") version "4.4.2" apply false -} -``` +## 1. Create a Firebase Project +If you haven't already created a project, create a new cloud project with a unique ID: +` firebase projects:create --display-name ''` +*Example:* +` firebase projects:create my-cool-app-vguthal-20260330 --display-name 'MyCoolApp'` +### 2. Register Your Android App +Link your Android app module (package name) to your project. Notice that the display name is passed as a positional argument at the end: +` firebase apps:create ANDROID '' --package-name '' --project ` +*Example:* +` firebase apps:create ANDROID 'MyApplication' --package-name 'com.example.myapplication' --project my-cool-app-vguthal-2b` +### 3. Download `google-services.json` +Fetch the configuration file using the App ID (which is printed in the output of the previous command): +` firebase apps:sdkconfig ANDROID --project ` +*Example output extraction to file:* +` # (Output must be saved as app/google-services.json)` +--- +## ✅ Verification Plan +### Manual Verification +Validate that the project was created and registered successfully: +` firebase projects:list` +` firebase apps:list --project ` -#### Module-level (app) `build.gradle.kts` -Apply the plugin and add the Firebase BOM (Bill of Materials) to manage your Firebase library versions: +--- +## 🤖 AI Automation Workflow -```kotlin -plugins { - id("com.android.application") - id("org.jetbrains.kotlin.android") - // Add the Google services plugin - id("com.google.gms.google-services") -} +If you are working with an AI agent (like Antigravity), you can ask it to automate these steps for you! -dependencies { - // Import the Firebase BoM - implementation(platform("com.google.firebase:firebase-bom:33.0.0")) // Check for latest version +**Usage:** Ask the agent to create the app and pass the display name. - // Add the dependency for the Firebase SDKs you want to use - // When using the BoM, don't specify versions in Firebase library dependencies - implementation("com.google.firebase:firebase-analytics") - - // Add other Firebase products as needed (e.g., Auth, Firestore) - // implementation("com.google.firebase:firebase-auth") -} -``` +**Example Prompt:** +"Create a Firebase app for this project. Use your own unique project ID and ask me for the display name." -### Next Steps: -* **Sync your project:** In Android Studio, click **"Sync Now"** in the notification bar that appears after file changes. -* **Verify Connection:** Run your app to send verification to the Firebase console that you've successfully installed the SDK. +The agent will: +1. Generate a unique project ID. +2. Ask you for the app display name. +3. Automatically run the CLI commands to: + - Create the project. + - Register the app. + - Download `google-services.json` to the correct location. \ No newline at end of file diff --git a/skills/firebase-firestore-standard/references/android_sdk_usage.md b/skills/firebase-firestore-standard/references/android_sdk_usage.md index 45fe06a..d3cc96e 100644 --- a/skills/firebase-firestore-standard/references/android_sdk_usage.md +++ b/skills/firebase-firestore-standard/references/android_sdk_usage.md @@ -2,6 +2,16 @@ This guide walks you through using Cloud Firestore in your Android app using Kotlin. +### Enable Firestore via CLI + +Before adding dependencies in your app, make sure you enable the Firestore service in your Firebase Project using the Firebase CLI: + +```bash +npx -y firebase-tools@latest init firestore +``` + + --- + ### 1. Add Dependencies In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add the dependency for Cloud Firestore: From 3cec905f422a40943dd0e6b4aace3f4423b2a83e Mon Sep 17 00:00:00 2001 From: Vinay Guthal Date: Tue, 31 Mar 2026 14:52:28 -0400 Subject: [PATCH 06/10] add instructions for firestore --- .../references/android_sdk_usage.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/skills/firebase-firestore-enterprise-native-mode/references/android_sdk_usage.md b/skills/firebase-firestore-enterprise-native-mode/references/android_sdk_usage.md index 8177911..4eb8f58 100644 --- a/skills/firebase-firestore-enterprise-native-mode/references/android_sdk_usage.md +++ b/skills/firebase-firestore-enterprise-native-mode/references/android_sdk_usage.md @@ -2,6 +2,16 @@ This guide walks you through using the Cloud Firestore SDK in your Android app using Kotlin. The SDK for Firestore Enterprise Native Mode is the same as the standard Cloud Firestore SDK. +### Enable Firestore via CLI + +Before adding dependencies in your app, make sure you enable the Firestore service in your Firebase Project using the Firebase CLI: + +```bash +npx -y firebase-tools@latest init firestore +``` + + --- + ### 1. Add Dependencies In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add the dependency for Cloud Firestore: From 32a937a9c63eb351db55be60ff69b1f29b240201 Mon Sep 17 00:00:00 2001 From: Vinay Guthal Date: Tue, 31 Mar 2026 14:53:14 -0400 Subject: [PATCH 07/10] remove .idea files --- .idea/agent-skills.iml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .idea/agent-skills.iml diff --git a/.idea/agent-skills.iml b/.idea/agent-skills.iml deleted file mode 100644 index d6ebd48..0000000 --- a/.idea/agent-skills.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file From 1793ff00fa1761ce02bb0db341d72326eb73145f Mon Sep 17 00:00:00 2001 From: Vinay Guthal Date: Tue, 31 Mar 2026 15:13:42 -0400 Subject: [PATCH 08/10] use compose and use bom --- .../references/usage_patterns_android.md | 37 ++++++++++++++++- .../references/client_sdk_android.md | 40 ++++++++++++++++--- .../reference/sdks.md | 3 +- .../references/android_sdk_usage.md | 40 ++++++++++++++++--- .../references/android_sdk_usage.md | 40 ++++++++++++++++--- 5 files changed, 142 insertions(+), 18 deletions(-) diff --git a/skills/firebase-ai-logic-basics/references/usage_patterns_android.md b/skills/firebase-ai-logic-basics/references/usage_patterns_android.md index daeb63a..5787c37 100644 --- a/skills/firebase-ai-logic-basics/references/usage_patterns_android.md +++ b/skills/firebase-ai-logic-basics/references/usage_patterns_android.md @@ -19,8 +19,8 @@ In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add th ```kotlin dependencies { - // Import the BoM (verify latest version) - implementation(platform("com.google.firebase:firebase-bom:33.0.0")) + // [AGENT] Fetch the latest available BoM version from Maven Central / Web before adding this + implementation(platform("com.google.firebase:firebase-bom:")) // Add the dependency for the Firebase AI library implementation("com.google.firebase:firebase-ai") @@ -63,6 +63,39 @@ class MainActivity : AppCompatActivity() { } ``` +#### Jetpack Compose (Modern) + +Initialize inside a `ComponentActivity` and use `setContent`: + +```kotlin +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.lifecycle.lifecycleScope +import com.google.firebase.Firebase +import com.google.firebase.ai.ai +import kotlinx.coroutines.launch + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val ai = Firebase.ai + val model = ai.generativeModel("gemini-2.5-flash-lite") + + lifecycleScope.launch { + val response = model.generateContent("Hello Gemini!") + setContent { + MaterialTheme { + Text("AI Response: ${response.text}") + } + } + } + } +} +``` + --- ### 3. Multimodal Input (Text and Images) diff --git a/skills/firebase-auth-basics/references/client_sdk_android.md b/skills/firebase-auth-basics/references/client_sdk_android.md index 93e2fc1..696e7eb 100644 --- a/skills/firebase-auth-basics/references/client_sdk_android.md +++ b/skills/firebase-auth-basics/references/client_sdk_android.md @@ -24,8 +24,8 @@ In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add th ```kotlin dependencies { - // Import the BoM (verify latest version) - implementation(platform("com.google.firebase:firebase-bom:33.0.0")) + // [AGENT] Fetch the latest available BoM version from Maven Central / Web before adding this + implementation(platform("com.google.firebase:firebase-bom:")) // Add the dependency for the Firebase Authentication library // When using the BoM, you don't specify versions in Firebase library dependencies @@ -50,10 +50,40 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + val auth = Firebase.auth + + setContent { + MaterialTheme { + Text("Auth initialized!") + } + } + } +} +``` + +#### Jetpack Compose (Modern) - // Initialize Firebase Auth - auth = Firebase.auth +Initialize inside a `ComponentActivity` using `setContent`: + +```kotlin +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import com.google.firebase.Firebase +import com.google.firebase.auth.auth + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val auth = Firebase.auth + + setContent { + MaterialTheme { + Text("Auth initialized!") + } + } } } ``` diff --git a/skills/firebase-data-connect-basics/reference/sdks.md b/skills/firebase-data-connect-basics/reference/sdks.md index 9885056..358ea57 100644 --- a/skills/firebase-data-connect-basics/reference/sdks.md +++ b/skills/firebase-data-connect-basics/reference/sdks.md @@ -121,7 +121,8 @@ const myReviews = await myReviews(); // @auth(level: USER) query from examples.m ```kotlin dependencies { - implementation(platform("com.google.firebase:firebase-bom:33.0.0")) + // [AGENT] Fetch the latest available BoM version from Maven Central / Web before adding this + implementation(platform("com.google.firebase:firebase-bom:")) implementation("com.google.firebase:firebase-dataconnect") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0") diff --git a/skills/firebase-firestore-enterprise-native-mode/references/android_sdk_usage.md b/skills/firebase-firestore-enterprise-native-mode/references/android_sdk_usage.md index 4eb8f58..823e0eb 100644 --- a/skills/firebase-firestore-enterprise-native-mode/references/android_sdk_usage.md +++ b/skills/firebase-firestore-enterprise-native-mode/references/android_sdk_usage.md @@ -18,8 +18,8 @@ In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add th ```kotlin dependencies { - // Import the BoM (verify latest version) - implementation(platform("com.google.firebase:firebase-bom:33.0.0")) + // [AGENT] Fetch the latest available BoM version from Maven Central / Web before adding this + implementation(platform("com.google.firebase:firebase-bom:")) // Add the dependency for the Cloud Firestore library implementation("com.google.firebase:firebase-firestore") @@ -43,10 +43,40 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + val db = Firebase.firestore + + setContent { + MaterialTheme { + Text("Firestore initialized!") + } + } + } +} +``` + +#### Jetpack Compose (Modern) - // Initialize Firestore - db = Firebase.firestore +Initialize inside a `ComponentActivity` using `setContent`: + +```kotlin +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import com.google.firebase.Firebase +import com.google.firebase.firestore.firestore + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val db = Firebase.firestore + + setContent { + MaterialTheme { + Text("Firestore initialized!") + } + } } } ``` diff --git a/skills/firebase-firestore-standard/references/android_sdk_usage.md b/skills/firebase-firestore-standard/references/android_sdk_usage.md index d3cc96e..bcf7676 100644 --- a/skills/firebase-firestore-standard/references/android_sdk_usage.md +++ b/skills/firebase-firestore-standard/references/android_sdk_usage.md @@ -18,8 +18,8 @@ In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add th ```kotlin dependencies { - // Import the BoM (verify latest version) - implementation(platform("com.google.firebase:firebase-bom:33.0.0")) + // [AGENT] Fetch the latest available BoM version from Maven Central / Web before adding this + implementation(platform("com.google.firebase:firebase-bom:")) // Add the dependency for the Cloud Firestore library // When using the BoM, you don't specify versions in Firebase library dependencies @@ -44,10 +44,40 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + val db = Firebase.firestore + + setContent { + MaterialTheme { + Text("Firestore initialized!") + } + } + } +} +``` + +#### Jetpack Compose (Modern) - // Initialize Firestore - db = Firebase.firestore +Initialize inside a `ComponentActivity` using `setContent`: + +```kotlin +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import com.google.firebase.Firebase +import com.google.firebase.firestore.firestore + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val db = Firebase.firestore + + setContent { + MaterialTheme { + Text("Firestore initialized!") + } + } } } ``` From 84b8db85aeabb6eeef1a5b2dc6a0261517c86ec8 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Thu, 2 Apr 2026 10:03:56 -0700 Subject: [PATCH 09/10] Sync main & next (#64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Explanding out contributor guidelines (#3) * update claude plugin marketplace command to use the new repo (#2) * add kiro power (#4) * update license (#8) * Adding a token counting script (#11) * Next -> Main (#16) * Add firebase-data-connect-basics skill (#7) * Add firebase-data-connect-basics skill * Add firebase dataconnect basic skills --------- Co-authored-by: Muhammad Talha * [Dataconnect] Restructure SKILL.md into a development workflow guide --------- Co-authored-by: Muhammad Talha Co-authored-by: Muhammad Talha <126821605+mtr002@users.noreply.github.com> * Adding a Cursor plugin (#13) * Add Cursor plugin configuration * Add logo to cursor plugin config * Move logo to assets directory * Restructure Cursor plugin to multi-plugin format * Clean up language * Use a symlink instead * Add theme to GCLI extension (#17) * Remove curl|bash (#18) * Update SKILL.md to remove references to 2.5 and instead use `latest`. (#19) * feat: Add sync job for genkit-ai/skills (#23) * Change to use `npx` to invoke Firebase CLI (#26) Change all `firebase` shell command mentioned in the skills to use `npx -y firebase-tools@latest` instead to ensure freshness and reduce frictions to the agents. * use pat for cla reasons (#29) * fix: update workflow title and committer details (#33) * Move the local environment setup to a new skill + MCP setup (#31) - Move all one-time-only local environment setup from `firebase-basic` to `firebase-local-env-setup` skill - Add description about how to install skills and MCP server. * fix: Add reviewers to sync-genkit-skills workflow (#35) Based on https://github.com/peter-evans/create-pull-request?tab=readme-ov-file#action-inputs * chore: sync updated skills (#34) Co-authored-by: ssbushi <66321939+ssbushi@users.noreply.github.com> * Update skill names in security rules documentation (#37) * Support Agent Skills for Firestore Enterprise with Native Mode (#27) * Add agent skills for firestore enterprise with native mode * Amend SKILL.md * Format SKILL * Address comments and rename standard edition * Remove files & update frontmatter * Updated to use "npx firebase-tools@latest" --------- Co-authored-by: Sichen Liu Co-authored-by: Joe Hanley * update the skill verification instruction (#38) * chore: sync updated skills (#39) * gemini-3-pro-preview --> gemini-3.1-pro-preview (#40) * Refine `firebase-local-env-setup` instructions for agents (#41) Updated the installation reference guides for all supported agents (Antigravity, Claude Code, Cursor, Gemini CLI, GitHub Copilot, and others) in the `firebase-local-env-setup` skill. - Added detailed steps to locate, verify, and safely merge MCP configurations (`mcp.json`, `claude_desktop_config.json`, etc.) without overwriting existing entries. - Added explicit instructions to check for existing skill installations using `npx skills list` before installing. - Enforced "stop and wait" instructions for user restarts to ensure the agent correctly verifies the MCP server connection securely. * Update `firebase-basics` skills (#42) * Update `firebase-basics` skills - Optimize description so that it is more likely to be loaded - Clearly state the prerequisites, how to validate them and how to meet them. - Add principles about how to optimize agent ability to help with Firebase related task - Indexing other knowledges for progressive disclosure. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Update `firebase-basics` skill and `firebase-local-env-setup` skill (#43) - Update their description so that firebase-basics can be picked up more consistently - Update instructions about how to refresh skills for Antigravity and other agents. * Split out python sdk content (#45) * Fixing up Cursor plugin format (#46) * docs: Improve local installation instructions (#47) * docs: improve local installation instructions * docs: address reviewer feedback on local installation instructions * adjusting cursor plugin to reflect skills and mcp setup path correctly (#52) * adjusting cursor plugin to reflect skills and mcp setup path correctly * Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * update mcp path to ensure it can find it * correct the path --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Add new skills logo (#51) * re-organize claude plugins (#54) * Moving battlehardened AI studio prompt into skills (#53) * Moving battlehardened AI studio prompt into skills * PR fixes * Address PR feedback: add isRecent, isAdmin, fix regex escaping and numbering * PR fixes * PR fixes * Merge `firebase-local-env-setup` skills into `firebase-basics` (#56) * Change all `firebase` CLI reference to use `npx firebase-tools` (#61) --------- Co-authored-by: Charlotte Liang Co-authored-by: Muhammad Talha Co-authored-by: Muhammad Talha <126821605+mtr002@users.noreply.github.com> Co-authored-by: christhompsongoogle <106194718+christhompsongoogle@users.noreply.github.com> Co-authored-by: Samuel Bushi <66321939+ssbushi@users.noreply.github.com> Co-authored-by: chkuang-g <31869252+chkuang-g@users.noreply.github.com> Co-authored-by: Morgan Chen Co-authored-by: Google Open Source Bot Co-authored-by: cmoiccool Co-authored-by: Sichen Liu Co-authored-by: Sichen Liu Co-authored-by: Rosário P. Fernandes Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Peter Friese --- .claude-plugin/marketplace.json | 11 +- .../plugin.json | 0 .cursor-plugin/plugin.json | 30 ++ .github/scripts/prune-skills.sh | 30 ++ .github/scripts/sync-skills.sh | 30 ++ .github/workflows/sync-genkit-skills.yml | 50 +++ .gitignore | 3 + .mcp.json | 17 + CONTRIBUTING.md | 18 + README.md | 37 +- assets/firebase-agent-skills_logo.svg | 15 + assets/firebase_logo.svg | 12 + claude-plugins/firebase/.mcp.json | 12 - claude-plugins/firebase/skills | 1 - gemini-extension.json | 36 +- kiro/POWER.md | 87 ++++ kiro/mcp.json | 12 + scripts/skill-token-counter/index.js | 363 +++++++++++++++ scripts/skill-token-counter/package-lock.json | 24 + scripts/skill-token-counter/package.json | 13 + skills/developing-genkit-dart/SKILL.md | 57 +++ .../references/genkit.md | 380 ++++++++++++++++ .../references/genkit_anthropic.md | 41 ++ .../references/genkit_chrome.md | 23 + .../references/genkit_firebase_ai.md | 23 + .../references/genkit_google_genai.md | 95 ++++ .../references/genkit_mcp.md | 115 +++++ .../references/genkit_middleware.md | 84 ++++ .../references/genkit_openai.md | 54 +++ .../references/genkit_shelf.md | 59 +++ .../references/schemantic.md | 137 ++++++ skills/developing-genkit-js/SKILL.md | 112 +++++ .../references/best-practices.md | 31 ++ .../references/common-errors.md | 132 ++++++ .../references/docs-and-cli.md | 62 +++ .../references/examples.md | 157 +++++++ .../developing-genkit-js/references/setup.md | 46 ++ skills/firebase-ai-logic-basics/SKILL.md | 12 +- skills/firebase-app-hosting-basics/SKILL.md | 4 +- .../references/cli_commands.md | 20 +- .../references/configuration.md | 2 +- .../references/emulation.md | 4 +- skills/firebase-auth-basics/SKILL.md | 4 +- .../references/security_rules.md | 2 +- skills/firebase-basics/SKILL.md | 157 +++---- .../references/firebase-cli-guide.md | 16 + .../references/firebase-project-create.md | 11 + .../references/firebase-service-init.md | 18 + .../references/local-env-setup.md | 65 +++ .../references/refresh-antigravity.md | 46 ++ .../references/refresh-claude.md | 10 + .../references/refresh-gemini-cli.md | 11 + .../references/refresh-other-agents.md | 48 ++ .../references/setup-antigravity.md | 63 +++ .../references/setup-claude_code.md | 30 ++ .../references/setup-cursor.md | 63 +++ .../references/setup-gemini_cli.md | 39 ++ .../references/setup-github_copilot.md | 70 +++ .../references/setup-other_agents.md | 65 +++ .../firebase-basics/references/web_setup.md | 6 +- skills/firebase-data-connect-basics/SKILL.md | 13 + .../reference/advanced.md | 4 +- .../reference/config.md | 40 +- .../reference/sdks.md | 4 +- .../firebase-data-connect-basics/templates.md | 12 +- .../references/security_rules.md | 299 ------------- .../SKILL.md | 31 ++ .../references/data_model.md | 54 +++ .../references/indexes.md | 111 +++++ .../references/provisioning.md | 101 +++++ .../references/python_sdk_usage.md | 126 ++++++ .../references/security_rules.md | 414 ++++++++++++++++++ .../references/web_sdk_usage.md | 201 +++++++++ .../SKILL.md | 10 +- .../references/indexes.md | 4 +- .../references/provisioning.md | 26 +- .../references/security_rules.md | 414 ++++++++++++++++++ .../references/web_sdk_usage.md | 2 +- skills/firebase-hosting-basics/SKILL.md | 2 +- .../references/deploying.md | 10 +- 80 files changed, 4535 insertions(+), 518 deletions(-) rename {claude-plugins/firebase/.claude-plugin => .claude-plugin}/plugin.json (100%) create mode 100644 .cursor-plugin/plugin.json create mode 100755 .github/scripts/prune-skills.sh create mode 100755 .github/scripts/sync-skills.sh create mode 100644 .github/workflows/sync-genkit-skills.yml create mode 100644 .gitignore create mode 100644 .mcp.json create mode 100644 assets/firebase-agent-skills_logo.svg create mode 100644 assets/firebase_logo.svg delete mode 100644 claude-plugins/firebase/.mcp.json delete mode 120000 claude-plugins/firebase/skills create mode 100644 kiro/POWER.md create mode 100644 kiro/mcp.json create mode 100644 scripts/skill-token-counter/index.js create mode 100644 scripts/skill-token-counter/package-lock.json create mode 100644 scripts/skill-token-counter/package.json create mode 100644 skills/developing-genkit-dart/SKILL.md create mode 100644 skills/developing-genkit-dart/references/genkit.md create mode 100644 skills/developing-genkit-dart/references/genkit_anthropic.md create mode 100644 skills/developing-genkit-dart/references/genkit_chrome.md create mode 100644 skills/developing-genkit-dart/references/genkit_firebase_ai.md create mode 100644 skills/developing-genkit-dart/references/genkit_google_genai.md create mode 100644 skills/developing-genkit-dart/references/genkit_mcp.md create mode 100644 skills/developing-genkit-dart/references/genkit_middleware.md create mode 100644 skills/developing-genkit-dart/references/genkit_openai.md create mode 100644 skills/developing-genkit-dart/references/genkit_shelf.md create mode 100644 skills/developing-genkit-dart/references/schemantic.md create mode 100644 skills/developing-genkit-js/SKILL.md create mode 100644 skills/developing-genkit-js/references/best-practices.md create mode 100644 skills/developing-genkit-js/references/common-errors.md create mode 100644 skills/developing-genkit-js/references/docs-and-cli.md create mode 100644 skills/developing-genkit-js/references/examples.md create mode 100644 skills/developing-genkit-js/references/setup.md create mode 100644 skills/firebase-basics/references/firebase-cli-guide.md create mode 100644 skills/firebase-basics/references/firebase-project-create.md create mode 100644 skills/firebase-basics/references/firebase-service-init.md create mode 100644 skills/firebase-basics/references/local-env-setup.md create mode 100644 skills/firebase-basics/references/refresh-antigravity.md create mode 100644 skills/firebase-basics/references/refresh-claude.md create mode 100644 skills/firebase-basics/references/refresh-gemini-cli.md create mode 100644 skills/firebase-basics/references/refresh-other-agents.md create mode 100644 skills/firebase-basics/references/setup-antigravity.md create mode 100644 skills/firebase-basics/references/setup-claude_code.md create mode 100644 skills/firebase-basics/references/setup-cursor.md create mode 100644 skills/firebase-basics/references/setup-gemini_cli.md create mode 100644 skills/firebase-basics/references/setup-github_copilot.md create mode 100644 skills/firebase-basics/references/setup-other_agents.md delete mode 100644 skills/firebase-firestore-basics/references/security_rules.md create mode 100644 skills/firebase-firestore-enterprise-native-mode/SKILL.md create mode 100644 skills/firebase-firestore-enterprise-native-mode/references/data_model.md create mode 100644 skills/firebase-firestore-enterprise-native-mode/references/indexes.md create mode 100644 skills/firebase-firestore-enterprise-native-mode/references/provisioning.md create mode 100644 skills/firebase-firestore-enterprise-native-mode/references/python_sdk_usage.md create mode 100644 skills/firebase-firestore-enterprise-native-mode/references/security_rules.md create mode 100644 skills/firebase-firestore-enterprise-native-mode/references/web_sdk_usage.md rename skills/{firebase-firestore-basics => firebase-firestore-standard}/SKILL.md (58%) rename skills/{firebase-firestore-basics => firebase-firestore-standard}/references/indexes.md (94%) rename skills/{firebase-firestore-basics => firebase-firestore-standard}/references/provisioning.md (70%) create mode 100644 skills/firebase-firestore-standard/references/security_rules.md rename skills/{firebase-firestore-basics => firebase-firestore-standard}/references/web_sdk_usage.md (97%) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7da90ce..715f359 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,8 +9,13 @@ "name": "firebase", "description": "Official Claude plugin for Firebase. Manage projects, add backend services, develop AI features, deploy & host apps, and more", "category": "development", - "tags": ["firebase", "backend", "database", "cloud-services"], - "source": "./claude-plugins/firebase" + "tags": [ + "firebase", + "backend", + "database", + "cloud-services" + ], + "source": "./" } ] -} +} \ No newline at end of file diff --git a/claude-plugins/firebase/.claude-plugin/plugin.json b/.claude-plugin/plugin.json similarity index 100% rename from claude-plugins/firebase/.claude-plugin/plugin.json rename to .claude-plugin/plugin.json diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json new file mode 100644 index 0000000..7d15fc6 --- /dev/null +++ b/.cursor-plugin/plugin.json @@ -0,0 +1,30 @@ +{ + "name": "firebase", + "displayName": "Firebase", + "version": "1.0.1", + "description": "The official Firebase Cursor plugin. Prototype, build, and run modern apps with Firebase's backend and AI infrastructure.", + "author": { + "name": "Firebase", + "email": "firebase-support@google.com", + "url": "https://firebase.google.com" + }, + "license": "Apache-2.0", + "keywords": [ + "firebase", + "cursor", + "skills", + "mcp", + "firestore", + "hosting", + "auth", + "storage", + "ailogic", + "backend", + "cloud-services" + ], + "skills": "./skills/", + "mcpServers": "./.mcp.json", + "logo": "./assets/firebase_logo.svg", + "homepage": "https://github.com/firebase/agent-skills", + "repository": "https://github.com/firebase/agent-skills" +} \ No newline at end of file diff --git a/.github/scripts/prune-skills.sh b/.github/scripts/prune-skills.sh new file mode 100755 index 0000000..ccb5d4e --- /dev/null +++ b/.github/scripts/prune-skills.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +# Script to prune deleted Genkit skills +# Usage: ./prune-skills.sh + +echo "🧹 Pruning deleted Genkit skills..." + +for skill_dir in firebase-skills/skills/*; do + [ -d "$skill_dir" ] || continue + skill_name=$(basename "$skill_dir") + skill_md="$skill_dir/SKILL.md" + + # Check if this skill is managed by Genkit + if [ -f "$skill_md" ]; then + # We use yq to check the metadata. + # The '|| echo false' handles cases where the field is missing or yq fails. + is_managed=$(yq -f extract '.metadata["genkit-managed"] == true' "$skill_md" 2>/dev/null || echo false) + + if [ "$is_managed" = "true" ]; then + # If managed by Genkit but not in source, delete it + if [ ! -d "genkit-skills/skills/$skill_name" ]; then + echo "🗑️ Pruning deleted skill: $skill_name" + rm -rf "$skill_dir" + fi + fi + fi +done + +echo "✅ Pruning complete." diff --git a/.github/scripts/sync-skills.sh b/.github/scripts/sync-skills.sh new file mode 100755 index 0000000..f0f2a01 --- /dev/null +++ b/.github/scripts/sync-skills.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +# Script to sync and tag Genkit skills +# Usage: ./sync-skills.sh + +echo "🔄 Syncing skills from Genkit repo..." + +for skill_dir in genkit-skills/skills/*; do + [ -d "$skill_dir" ] || continue + skill_name=$(basename "$skill_dir") + dest_dir="firebase-skills/skills/$skill_name" + + echo "📦 Syncing $skill_name..." + + # Clean destination to ensure exact sync (remove old files) + if [ -d "$dest_dir" ]; then + rm -rf "$dest_dir" + fi + mkdir -p "$dest_dir" + + # Copy new files + cp -r "$skill_dir/"* "$dest_dir/" + + # Mark as managed by Genkit using yq + # The -i flag edits in place, -f process runs the filter + yq -i -f process '.metadata["genkit-managed"] = true' "$dest_dir/SKILL.md" +done + +echo "✅ Sync complete." diff --git a/.github/workflows/sync-genkit-skills.yml b/.github/workflows/sync-genkit-skills.yml new file mode 100644 index 0000000..5964844 --- /dev/null +++ b/.github/workflows/sync-genkit-skills.yml @@ -0,0 +1,50 @@ +name: Sync Genkit Skills + +on: + schedule: + # Runs at 9:00 AM EST (14:00 UTC) on weekdays (Monday-Friday) + - cron: '0 14 * * 1-5' + workflow_dispatch: # For manual trigger + +permissions: + # Permission for writing commits and creating PRs is given through + # the bot account PAT. + contents: read + +jobs: + sync: + environment: genkit skills automerger cron + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout firebase/agent-skills + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + path: firebase-skills + + - name: Checkout genkit-ai/skills + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + repository: genkit-ai/skills + path: genkit-skills + + # Identifies skills managed by Genkit that have been deleted from the source repo + # and removes them from this repo. + - name: Prune Deleted Genkit Skills + run: firebase-skills/.github/scripts/prune-skills.sh + + # Copies skills from Genkit repo, tags them with metadata, and handles updates. + # Note: Uses 'yq' which is pre-installed on GitHub ubuntu-latest runners. + - name: Sync & Tag Skills + run: firebase-skills/.github/scripts/sync-skills.sh + + - name: Create Pull Request + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8 + with: + path: firebase-skills + commit-message: "chore: sync updated skills" + title: "chore: 🤖 Sync updated skills from Genkit" + branch: "sync-skills" + committer: Google Open Source Bot + reviewers: joehan, ssbushi + token: ${{ secrets.OSS_BOT_AUTOMERGER_PAT }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aafcb34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.DS_Store +*.log diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..0d61dae --- /dev/null +++ b/.mcp.json @@ -0,0 +1,17 @@ +{ + "mcpServers": { + "firebase": { + "command": "npx", + "args": [ + "-y", + "firebase-tools@latest", + "mcp", + "--dir", + "." + ], + "env": { + "IS_FIREBASE_MCP": "true" + } + } + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b16bd94..2228853 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,24 @@ This project follows ## Contribution process +### main and next branches + +This repo has 2 protected branches, `main` and `next`. `main` is the default branch, and most users will use the skills here. `next` is used for development and will contain new skills and improvements that are being staged for release. + +If you are making an incremental improvement to an existing skill, point your PR to the `main` branch. + +If you are adding a new skill, adding support for a new platform or making a significant change to an existing skill, point your PR to the `next` branch. + +### Testing skills + +To test out your skill, you can install it from a branch using the 'skills' CLI tool: + +```bash +npx skills add https://github.com/firebase/skills/tree/ +``` + +We also have an automated eval pipeline set up in [firebase-tools](https://github.com/firebase/firebase-tools/tree/main/scripts/agent-evals) that is set up to pull content from this repo and run it against a set of test cases. You should add your own test cases there for your skill, both to check activation on the prompts you expect to trigger it, and to check that agents succeed on the tasks you expect it to help with. + ### Code reviews All submissions, including submissions by project members, require review. We diff --git a/README.md b/README.md index f82ae86..9100bc4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# Firebase Agent Skills +

+ Logo + Firebase Agent Skills +

+ A collection of skills for AI coding agents, to help them understand and work with Firebase more effectively. @@ -27,7 +31,7 @@ gemini extensions install https://github.com/firebase/skills 1. Add the Firebase marketplace for Claude plugins: ```bash -claude plugin marketplace add firebase/firebase-tools +claude plugin marketplace add firebase/skills ``` Install the Claude plugin for Firebase: @@ -50,7 +54,34 @@ claude plugin marketplace list git clone https://github.com/firebase/skills.git ``` -2. Copy the contents of the `skills` directory to the appropriate location for your AI tool. +2. Copy the contents of the `skills` directory to the appropriate location for your AI tool. Common locations include: + - **Cursor**: `.cursor/rules/` + - **Windsurf**: `.windsurfrules/` + - **GitHub Copilot**: `.github/copilot-instructions.md` (or project-specific instruction files) + +### Option 5: Local Path via Agent Skills CLI + +The `skills` CLI also supports installing skills from a local directory. If you have cloned this repository, you can add skills by pointing the CLI to your local folder: + +```bash +npx skills add /path/to/your/local/firebase-skills/skills +``` + +If you make changes to the local skills repository and want to update your project with the new changes, you can update them by running: + +```bash +npx skills experimental_install +``` + +### Option 6: Local Development (Live Symlinking) + +If you are actively contributing to or developing these skills, using `npx skills add` or copying files means you have to manually update them every time you make a change. Instead, use a symlink so that changes in your local clone are immediately reflected in your test project. + +For example, to test with Cursor: + +```bash +ln -s /path/to/firebase-skills/skills /path/to/your/test-project/.cursor/rules +``` ## 🤝 Contributing diff --git a/assets/firebase-agent-skills_logo.svg b/assets/firebase-agent-skills_logo.svg new file mode 100644 index 0000000..a928e80 --- /dev/null +++ b/assets/firebase-agent-skills_logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/firebase_logo.svg b/assets/firebase_logo.svg new file mode 100644 index 0000000..9fc1a30 --- /dev/null +++ b/assets/firebase_logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/claude-plugins/firebase/.mcp.json b/claude-plugins/firebase/.mcp.json deleted file mode 100644 index 761db96..0000000 --- a/claude-plugins/firebase/.mcp.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mcpServers": { - "firebase": { - "description": "Firebase MCP server - understand and manage your Firebase project, resources, and data", - "command": "npx", - "args": ["-y", "firebase-tools", "mcp", "--dir", "."], - "env": { - "IS_FIREBASE_MCP": "true" - } - } - } -} diff --git a/claude-plugins/firebase/skills b/claude-plugins/firebase/skills deleted file mode 120000 index 5dcab58..0000000 --- a/claude-plugins/firebase/skills +++ /dev/null @@ -1 +0,0 @@ -../../skills \ No newline at end of file diff --git a/gemini-extension.json b/gemini-extension.json index 9b69abe..8308f9b 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -11,5 +11,39 @@ "IS_GEMINI_CLI_EXTENSION": "true" } } - } + }, + "themes": [ + { + "name": "firebase-default-dark", + "type": "custom", + "background": { + "primary": "#212121", + "diff": { + "added": "green", + "removed": "red" + } + }, + "text": { + "primary": "#E8E8E8", + "secondary": "#A6A6A6", + "link": "#7CACF8", + "accent": "#0B57D0", + "response": "#7CACF8" + }, + "status": { + "success": "#6DD58C", + "warning": "#FFB300", + "error": "#E46962" + }, + "border": { + "default": "#2F3032", + "focused": "#2F3032" + }, + "ui": { + "comment": "#DD2C00", + "symbol": "#2F3032", + "gradient": ["#FFC400", "#FF9100", "#DD2C00"] + } + } + ] } diff --git a/kiro/POWER.md b/kiro/POWER.md new file mode 100644 index 0000000..cd32ad5 --- /dev/null +++ b/kiro/POWER.md @@ -0,0 +1,87 @@ +--- +name: "firebase" +displayName: "Build with Firebase" +description: "Build full-stack applications with Firebase's suite of backend services including Authentication, Firestore, App Hosting, Cloud Functions, Storage, Crashlytics, and Cloud Messaging" +keywords: ["firebase", "firestore", "auth", "authentication", "database", "realtime", "cloud functions", "storage", "hosting", "app hosting", "backend", "cloud messaging", "fcm", "nosql", "serverless", "baas", "crashlytics"] +mcpServers: ["firebase"] +--- + +# Onboarding + +## Validate Firebase CLI Installation + +Before using the Firebase MCP server, ensure Node.js and the Firebase CLI are installed and you're authenticated: + +- **Node.js**: Required to run the Firebase CLI + - Check installation: `node --version` + - Install if needed: Download from [nodejs.org](https://nodejs.org/) (LTS version recommended) + +- **Firebase CLI**: Required for managing Firebase projects and services + - Check installation: `npx -y firebase-tools@latest --version` + - **CRITICAL**: If the Firebase CLI is not installed, DO NOT proceed with Firebase setup. + +- **Authentication**: Sign in to Firebase + - Check current user: `npx -y firebase-tools@latest login:list` + - If not signed in, run: `npx -y firebase-tools@latest login` (this will open a browser for Google Account authentication) + +- **Check Projects**: Verify project access and connectivity + - Run `npx -y firebase-tools@latest projects:list` to check for available Firebase projects + - Use this to verify that the CLI is correctly authenticated and can reach the Firebase API; if this fails, try to reconnect using `npx -y firebase-tools@latest login` + +- **Verify MCP Connection**: Ensure the MCP server is connected after authentication + - Use the `firebase_get_environment` tool to check connection status + - Verify it returns the correct current user and project information + - **If connection fails**: The MCP server may need manual setup or restart: + 1. Open Kiro settings and navigate to "MCP Servers" + 2. Find the Firebase MCP server in the list + 3. Click the "Retry" or "Reconnect" button + 4. Wait for the server status to show as "Connected" + 5. Test the connection again with `firebase_get_environment` + + +## Usage and Features + +Once configured, the MCP server will automatically provide Firebase capabilities to your AI assistant. You can: + +- Ask the AI to help set up Firebase services +- Query your Firestore database +- Manage authentication users +- Deploy to Firebase Hosting +- And much more! + +## Firebase Services Overview + +### Core Services Available via MCP +- **Authentication**: User management, sign-in methods, custom claims +- **Firestore**: NoSQL document database with real-time sync +- **App Hosting**: Full-stack app deployment with SSR +- **Storage**: File storage and serving +- **Cloud Functions**: Serverless backend code +- **Hosting**: Web app deployment to global CDN +- **Cloud Messaging**: Push notifications +- **Remote Config**: Dynamic app configuration +- **Crashlytics**: Crash reporting and analysis + + +### Using Firebase MCP Tools +The Firebase MCP server provides tools for: +- Managing Firebase projects and apps +- Initializing and deploying services +- Querying and manipulating Firestore data +- Managing Authentication users +- Validating Security Rules +- Sending Cloud Messaging notifications +- Viewing Cloud Functions logs +- And more... + + +## Additional Resources +- Firebase Documentation: https://firebase.google.com/docs +- Firebase YouTube Channel: https://www.youtube.com/firebase +- Firebase MCP Server: https://firebase.google.com/docs/ai-assistance/mcp-server + +## License and support + +This power integrates with the Firebase MCP Server (Apache-2.0). +- [Privacy Policy](https://firebase.google.com/support/privacy) +- [Support](https://firebase.google.com/support) diff --git a/kiro/mcp.json b/kiro/mcp.json new file mode 100644 index 0000000..5907de1 --- /dev/null +++ b/kiro/mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "firebase-local": { + "command": "npx", + "args": [ + "-y", + "firebase-tools@latest", + "mcp" + ] + } + } +} diff --git a/scripts/skill-token-counter/index.js b/scripts/skill-token-counter/index.js new file mode 100644 index 0000000..e3fb7b2 --- /dev/null +++ b/scripts/skill-token-counter/index.js @@ -0,0 +1,363 @@ +import fs from 'fs'; +import path from 'path'; +import { execSync } from 'child_process'; +import { GoogleGenerativeAI } from '@google/generative-ai'; + +const apiKey = process.env.GEMINI_API_KEY; +if (!apiKey) { + console.error('Error: GEMINI_API_KEY environment variable is missing.'); + process.exit(1); +} + +const modelName = "gemini-3.1-pro-preview"; +const genAI = new GoogleGenerativeAI(apiKey); +const model = genAI.getGenerativeModel({ model: modelName }); + +async function countTokens(text) { + if (!text || text.trim().length === 0) return 0; + try { + const response = await model.countTokens(text); + return response.totalTokens; + } catch (err) { + console.error(`Error counting tokens:`, err.message); + return 0; + } +} + +function parseSkillMd(content) { + const frontmatterRegex = /^---\n([\s\S]*?)\n---\n/; + const match = content.match(frontmatterRegex); + + if (match) { + const fullMatch = match[0]; + const frontmatter = fullMatch; + const body = content.slice(fullMatch.length); + return { frontmatter, body }; + } else { + return { frontmatter: '', body: content }; + } +} + +async function listFilesRecursiveLocal(dir) { + let results = []; + try { + const list = fs.readdirSync(dir); + for (const file of list) { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + if (stat && stat.isDirectory()) { + results = results.concat(await listFilesRecursiveLocal(filePath)); + } else { + results.push(filePath); + } + } + } catch (e) { + // Directory might not exist or be accessible + } + return results; +} + +class GitHelper { + constructor() { + try { + this.root = execSync('git rev-parse --show-toplevel', { stdio: 'pipe' }).toString().trim(); + } catch { + this.root = null; + } + } + + isGitRepo() { + return this.root !== null; + } + + getRepoRelativePath(absolutePath) { + return path.relative(this.root, absolutePath); + } + + getFileContent(ref, relativePath) { + try { + return execSync(`git show ${ref}:"${relativePath}"`, { stdio: 'pipe', cwd: this.root }).toString('utf8'); + } catch { + return null; + } + } + + listReferenceFiles(ref, dirRelativePath) { + try { + // Use bash behavior to catch errors if folder doesn't exist + const out = execSync(`git ls-tree -r --name-only ${ref} "${dirRelativePath}" 2>/dev/null || true`, { stdio: 'pipe', cwd: this.root }).toString(); + return out.split('\n').filter(Boolean); + } catch { + return []; + } + } + + listSkills(ref, targetRelativePath) { + try { + const out = execSync(`git ls-tree -r --name-only ${ref} "${targetRelativePath}" 2>/dev/null || true`, { stdio: 'pipe', cwd: this.root }).toString(); + const files = out.split('\n').filter(Boolean); + const skillDirs = new Set(); + for (const file of files) { + if (file.endsWith('/SKILL.md') || file === 'SKILL.md') { + skillDirs.add(path.dirname(file)); + } + } + return Array.from(skillDirs); + } catch { + return []; + } + } +} + +async function analyzeSkill(skillFolderPath, ref = null, gitHelper = null) { + let totalTokens = 0; + let breakdown = []; + const skillName = path.basename(skillFolderPath); + + const getFile = (p) => { + if (ref && gitHelper) { + return gitHelper.getFileContent(ref, gitHelper.getRepoRelativePath(p)); + } + return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null; + }; + + const getRefFiles = async (p) => { + if (ref && gitHelper) { + const rels = gitHelper.listReferenceFiles(ref, gitHelper.getRepoRelativePath(p)); + return rels.map(r => path.join(gitHelper.root, r)); + } + return await listFilesRecursiveLocal(p); + }; + + // 1. Process SKILL.md + const skillMdPath = path.join(skillFolderPath, 'SKILL.md'); + const skillMdContent = getFile(skillMdPath); + + if (skillMdContent) { + const { frontmatter, body } = parseSkillMd(skillMdContent); + + if (frontmatter) { + const fmTokens = await countTokens(frontmatter); + breakdown.push({ Entity: 'SKILL.md (Frontmatter)', Tokens: fmTokens, Type: 'Frontmatter' }); + totalTokens += fmTokens; + } + + if (body) { + const bodyTokens = await countTokens(body); + breakdown.push({ Entity: 'SKILL.md (Body)', Tokens: bodyTokens, Type: 'Body' }); + totalTokens += bodyTokens; + } + } else { + if (!ref) { + console.warn(`Warning: SKILL.md not found in ${skillFolderPath}`); + } + } + + // 2. Process references + const referencesPath = path.join(skillFolderPath, 'references'); + const referenceFiles = await getRefFiles(referencesPath); + for (const refFile of referenceFiles) { + const relativePath = path.relative(skillFolderPath, refFile); + const fileContent = getFile(refFile); + if (fileContent) { + const fileTokens = await countTokens(fileContent); + breakdown.push({ Entity: relativePath, Tokens: fileTokens, Type: 'Reference' }); + totalTokens += fileTokens; + } + } + + return { skillName, totalTokens, breakdown }; +} + +async function main() { + const args = process.argv.slice(2); + let compareRef = null; + let isJson = false; + let targetParams = []; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--compare') { + compareRef = args[i + 1] || 'main'; + i++; + } else if (args[i] === '--json') { + isJson = true; + } else { + targetParams.push(args[i]); + } + } + + const log = (...logArgs) => { if (!isJson) console.log(...logArgs); }; + const warn = (...logArgs) => { if (!isJson) console.warn(...logArgs); }; + const table = (data) => { if (!isJson) console.table(data); }; + + const targetParam = targetParams[0] || '../../skills'; + const resolvedPath = path.resolve(targetParam); + + const gitHelper = new GitHelper(); + + if (compareRef && !gitHelper.isGitRepo()) { + console.error('Error: --compare used but not in a git repository.'); + process.exit(1); + } + + const localSkillsToProcess = new Set(); + const refSkillsToProcess = new Set(); + + // Find local skills + if (fs.existsSync(resolvedPath)) { + const isSingleSkill = fs.existsSync(path.join(resolvedPath, 'SKILL.md')); + if (isSingleSkill) { + localSkillsToProcess.add(resolvedPath); + } else { + const entries = fs.readdirSync(resolvedPath); + for (const entry of entries) { + const entryPath = path.join(resolvedPath, entry); + if (fs.statSync(entryPath).isDirectory() && fs.existsSync(path.join(entryPath, 'SKILL.md'))) { + localSkillsToProcess.add(entryPath); + } + } + } + } + + // Find remote skills if comparing + if (compareRef) { + const relativeTarget = gitHelper.getRepoRelativePath(resolvedPath); + const isSingleRef = !!gitHelper.getFileContent(compareRef, path.join(relativeTarget, 'SKILL.md')); + if (isSingleRef) { + refSkillsToProcess.add(resolvedPath); + } else { + const refSkillDirs = gitHelper.listSkills(compareRef, relativeTarget); + for (const dir of refSkillDirs) { + refSkillsToProcess.add(path.join(gitHelper.root, dir)); + } + } + } + + const allSkillPaths = new Set([...localSkillsToProcess, ...refSkillsToProcess]); + + if (allSkillPaths.size === 0) { + if (!isJson) console.error(`No skills found in ${resolvedPath} (local${compareRef ? ` or ${compareRef}` : ''})`); + process.exit(1); + } + + log(`Analyzing ${allSkillPaths.size} skill(s)${compareRef ? ` and comparing with [${compareRef}]` : ''}...\n`); + + let grandTotalLocal = 0; + let grandTotalRef = 0; + let allSkillsSummary = []; + let jsonOutput = { skills: {}, summary: [] }; + + // Sort paths to have a consistent output + const sortedPaths = Array.from(allSkillPaths).sort(); + + for (const skillPath of sortedPaths) { + const skillName = path.basename(skillPath); + + // Calculate local + const localStats = await analyzeSkill(skillPath, null, null); + grandTotalLocal += localStats.totalTokens; + jsonOutput.skills[skillName] = { localTotal: localStats.totalTokens, breakdown: [] }; + + // Calculate ref if checking + let refTokens = 0; + if (compareRef) { + const refStats = await analyzeSkill(skillPath, compareRef, gitHelper); + refTokens = refStats.totalTokens; + grandTotalRef += refTokens; + jsonOutput.skills[skillName].refTotal = refTokens; + + const combinedBreakdown = {}; + + for (const item of localStats.breakdown) { + combinedBreakdown[item.Entity] = { + Type: item.Type, + Local: item.Tokens, + [compareRef]: 0, + Delta: `+${item.Tokens}` + }; + } + + for (const item of refStats.breakdown) { + if (!combinedBreakdown[item.Entity]) { + combinedBreakdown[item.Entity] = { + Type: item.Type, + Local: 0, + [compareRef]: item.Tokens, + Delta: `-${item.Tokens}` + }; + } else { + combinedBreakdown[item.Entity][compareRef] = item.Tokens; + const delta = combinedBreakdown[item.Entity].Local - item.Tokens; + combinedBreakdown[item.Entity].Delta = delta > 0 ? `+${delta}` : delta.toString(); + } + } + + const tableData = Object.keys(combinedBreakdown).map(entity => ({ + Entity: entity, + ...combinedBreakdown[entity] + })); + + const typeOrder = { 'Frontmatter': 0, 'Body': 1, 'Reference': 2 }; + tableData.sort((a, b) => { + if (typeOrder[a.Type] !== typeOrder[b.Type]) { + return typeOrder[a.Type] - typeOrder[b.Type]; + } + return a.Entity.localeCompare(b.Entity); + }); + + jsonOutput.skills[skillName].breakdown = tableData; + + log(`\n--- Token Breakdown for ${skillName} ---`); + table(tableData); + + } else { + jsonOutput.skills[skillName].breakdown = localStats.breakdown; + + log(`\n--- Local Token Breakdown for ${skillName} ---`); + table(localStats.breakdown); + } + + if (compareRef) { + const delta = localStats.totalTokens - refTokens; + const deltaStr = delta > 0 ? `+${delta}` : delta.toString(); + allSkillsSummary.push({ + Skill: skillName, + Local: localStats.totalTokens, + [compareRef]: refTokens, + Delta: deltaStr + }); + } else { + allSkillsSummary.push({ + Skill: skillName, + Tokens: localStats.totalTokens + }); + } + } + + jsonOutput.summary = allSkillsSummary; + jsonOutput.grandTotalLocal = grandTotalLocal; + + log('\n========================================='); + log('--- Overall Skills Token Summary ---'); + table(allSkillsSummary); + + if (compareRef) { + const grandDelta = grandTotalLocal - grandTotalRef; + const grandDeltaStr = grandDelta > 0 ? `+${grandDelta}` : grandDelta.toString(); + jsonOutput.grandTotalRef = grandTotalRef; + jsonOutput.grandDelta = grandDeltaStr; + + log(`\nGrand Total (Local): ${grandTotalLocal}`); + log(`Grand Total ([${compareRef}]): ${grandTotalRef}`); + log(`Grand Delta: ${grandDeltaStr}`); + } else { + log(`\nGrand Total Tokens: ${grandTotalLocal}`); + } + log('========================================='); + + if (isJson) { + console.log(JSON.stringify(jsonOutput, null, 2)); + } +} + +main().catch(console.error); diff --git a/scripts/skill-token-counter/package-lock.json b/scripts/skill-token-counter/package-lock.json new file mode 100644 index 0000000..0afd673 --- /dev/null +++ b/scripts/skill-token-counter/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "skill-token-counter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "skill-token-counter", + "version": "1.0.0", + "dependencies": { + "@google/generative-ai": "^0.21.0" + } + }, + "node_modules/@google/generative-ai": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz", + "integrity": "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + } + } +} diff --git a/scripts/skill-token-counter/package.json b/scripts/skill-token-counter/package.json new file mode 100644 index 0000000..c984641 --- /dev/null +++ b/scripts/skill-token-counter/package.json @@ -0,0 +1,13 @@ +{ + "name": "skill-token-counter", + "version": "1.0.0", + "description": "Calculates the token footprint of an agent skill using the Gemini API.", + "main": "index.js", + "scripts": { + "count": "node index.js" + }, + "dependencies": { + "@google/generative-ai": "^0.21.0" + }, + "type": "module" +} diff --git a/skills/developing-genkit-dart/SKILL.md b/skills/developing-genkit-dart/SKILL.md new file mode 100644 index 0000000..706023b --- /dev/null +++ b/skills/developing-genkit-dart/SKILL.md @@ -0,0 +1,57 @@ +--- +name: developing-genkit-dart +description: Generates code and provides documentation for the Genkit Dart SDK. Use when the user asks to build AI agents in Dart, use Genkit flows, or integrate LLMs into Dart/Flutter applications. +metadata: + genkit-managed: true +--- + +# Genkit Dart + +Genkit Dart is an AI SDK for Dart that provides a unified interface for code generation, structured outputs, tools, flows, and AI agents. + +## Core Features and Usage +If you need help with initializing Genkit (`Genkit()`), Generation (`ai.generate`), Tooling (`ai.defineTool`), Flows (`ai.defineFlow`), Embeddings (`ai.embedMany`), streaming, or calling remote flow endpoints, please load the core framework reference: +[references/genkit.md](references/genkit.md) + +## Genkit CLI (recommended) + +The Genkit CLI provides a local development UI for running Flow, tracing executions, playing with models, and evaluating outputs. + +check if the user has it installed: `genkit --version` + +**Installation:** +```bash +curl -sL cli.genkit.dev | bash # Native CLI +# OR +npm install -g genkit-cli # Via npm +``` + +**Usage:** +Wrap your run command with `genkit start` to attach the Genkit developer UI and tracing: +```bash +genkit start -- dart run main.dart +``` + +## Plugin Ecosystem +Genkit relies on a large suite of plugins to perform generative AI actions, interface with external LLMs, or host web servers. + +When asked to use any given plugin, always verify usage by referring to its corresponding reference below. You should load the reference when you need to know the specific initialization arguments, tools, models, and usage patterns for the plugin: + +| Plugin Name | Reference Link | Description | +| ---- | ---- | ---- | +| `genkit_google_genai` | [references/genkit_google_genai.md](references/genkit_google_genai.md) | Load for Google Gemini plugin interface usage. | +| `genkit_anthropic` | [references/genkit_anthropic.md](references/genkit_anthropic.md) | Load for Anthropic plugin interface for Claude models. | +| `genkit_openai` | [references/genkit_openai.md](references/genkit_openai.md) | Load for OpenAI plugin interface for GPT models, Groq, and custom compatible endpoints. | +| `genkit_middleware` | [references/genkit_middleware.md](references/genkit_middleware.md) | Load for Tooling for specific agentic behavior: `filesystem`, `skills`, and `toolApproval` interrupts. | +| `genkit_mcp` | [references/genkit_mcp.md](references/genkit_mcp.md) | Load for Model Context Protocol integration (Server, Host, and Client capabilities). | +| `genkit_chrome` | [references/genkit_chrome.md](references/genkit_chrome.md) | Load for Running Gemini Nano locally inside the Chrome browser using the Prompt API. | +| `genkit_shelf` | [references/genkit_shelf.md](references/genkit_shelf.md) | Load for Integrating Genkit Flow actions over HTTP using Dart Shelf. | +| `genkit_firebase_ai` | [references/genkit_firebase_ai.md](references/genkit_firebase_ai.md) | Load for Firebase AI plugin interface (Gemini API via Vertex AI). | + +## External Dependencies +Whenever you define schemas mapping inside of Tools, Flows, and Prompts, you must use the [schemantic](https://pub.dev/packages/schemantic) library. +To learn how to use schemantic, ensure you read [references/schemantic.md](references/schemantic.md) for how to implement type safe generated Dart code. This is particularly relevant when you encounter symbols like `@Schema()`, `SchemanticType`, or classes with the `$` prefix. Genkit Dart uses schemantic for all of its data models so it's a CRITICAL skill to understand for using Genkit Dart. + +## Best Practices +- Always check that code cleanly compiles using `dart analyze` before generating the final response. +- Always use the Genkit CLI for local development and debugging. diff --git a/skills/developing-genkit-dart/references/genkit.md b/skills/developing-genkit-dart/references/genkit.md new file mode 100644 index 0000000..7dd33e5 --- /dev/null +++ b/skills/developing-genkit-dart/references/genkit.md @@ -0,0 +1,380 @@ +# Genkit Core Framework + +Genkit Dart is an AI SDK for Dart that provides a unified interface for text generation, structured output, tool calling, and agentic workflows. + +## Initialization + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_google_genai/genkit_google_genai.dart'; // Or any other plugin + +void main() async { + // Pass plugins to use into the Genkit constructor + final ai = Genkit(plugins: [googleAI()]); +} +``` + +## Generate Text + +```dart +final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash'), // Needs a model reference from a plugin + prompt: 'Explain quantum computing in simple terms.', +); + +print(response.text); +``` + +## Stream Responses +```dart +final stream = ai.generateStream( + model: googleAI.gemini('gemini-2.5-flash'), + prompt: 'Write a short story about a robot learning to paint.', +); + +await for (final chunk in stream) { + print(chunk.text); +} +``` + +## Embed Text +```dart +final embeddings = await ai.embedMany( + documents: [ + DocumentData(content: [TextPart(text: 'Hello world')]), + ], + embedder: googleAI.textEmbedding('text-embedding-004'), +); + +print(embeddings.first.embedding); +``` + +## Define Tools +Models can use define actions and access external data via custom defined tools. +Requires the `schemantic` library for schema definitions. + +```dart +import 'package:schemantic/schemantic.dart'; + +@Schema() +abstract class $WeatherInput { + String get location; +} + +final weatherTool = ai.defineTool( + name: 'getWeather', + description: 'Gets the current weather for a location', + inputSchema: WeatherInput.$schema, + fn: (input, _) async { + // Call your weather API here + return 'Weather in ${input.location}: 72°F and sunny'; + }, +); + +final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash'), + prompt: 'What\'s the weather like in San Francisco?', + toolNames: ['getWeather'], // Use the tools +); +``` + +## Structured Output + +You can ensure the generative model returns a typed JSON object by providing an `outputSchema`. + +```dart +@Schema() +abstract class $Person { + String get name; + int get age; +} + +// ... inside main ... + +final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash'), + prompt: 'Generate a person named John Doe, age 30', + outputSchema: Person.$schema, // Force the model to return this schema +); + +final person = response.output; // Typed Person object +print('Name: ${person.name}, Age: ${person.age}'); +``` + +## Define Flows +Wrap your AI logic in flows for better observability, testing, and deployment: + +```dart +final jokeFlow = ai.defineFlow( + name: 'tellJoke', + inputSchema: .string(), + outputSchema: .string(), + fn: (topic, _) async { + final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash'), + prompt: 'Tell me a joke about $topic', + ); + return response.text; // Value return + }, +); + +final joke = await jokeFlow('programming'); +print(joke); +``` + +### Streaming Flows +Stream data from your flows using `context.sendChunk(...)` and returning the final value: + +```dart +final streamStory = ai.defineFlow( + name: 'streamStory', + inputSchema: .string(), + outputSchema: .string(), + streamSchema: .string(), + fn: (topic, context) async { + final stream = ai.generateStream( + model: googleAI.gemini('gemini-2.5-flash'), + prompt: 'Write a story about $topic', + ); + + await for (final chunk in stream) { + context.sendChunk(chunk.text); // Stream the chunks + } + return 'Story complete'; // Value return + }, +); +``` + +## Calling remote Flows from a dart client +The `genkit` package provides `package:genkit/client.dart` representing remote Genkit actions that can be invoked or streamed using type-safe definitions. + +1. Defines a remote action +```dart +import 'package:genkit/client.dart'; + +final stringAction = defineRemoteAction( + url: 'http://localhost:3400/my-flow', + inputSchema: .string(), + outputSchema: .string(), +); +``` + +2. Call the Remote Action (Non-streaming) +```dart +final response = await stringAction(input: 'Hello from Dart!'); +print('Flow Response: $response'); +``` + +3. Call the Remote Action (Streaming) +Use the `.stream()` method on the action flow, and access `stream.onResult` to wait on the async return value. +```dart +final streamAction = defineRemoteAction( + url: 'http://localhost:3400/stream-story', + inputSchema: .string(), + outputSchema: .string(), + streamSchema: .string(), +); + +final stream = streamAction.stream( + input: 'Tell me a short story about a Dart developer.', +); + +await for (final chunk in stream) { + print('Chunk: $chunk'); +} + +final finalResult = await stream.onResult; +print('\nFinal Response: $finalResult'); +``` + +## Calling remote Flows from a Javascript client + +Install `genkit` npm package: + +```bash +npm install genkit +``` + +1. Call a remote flow (non-streaming) + +```ts +import { runFlow } from 'genkit/beta/client'; + +async function callHelloFlow() { + try { + const result = await runFlow({ + url: 'http://127.0.0.1:3400/helloFlow', // Replace with your deployed flow's URL + input: { name: 'Genkit User' }, + }); + console.log('Non-streaming result:', result.greeting); + } catch (error) { + console.error('Error calling helloFlow:', error); + } +} + +callHelloFlow(); +``` + +2. Call a remote flow (streaming) + +```ts +import { streamFlow } from 'genkit/beta/client'; + +async function streamHelloFlow() { + try { + const result = streamFlow({ + url: 'http://127.0.0.1:3400/helloFlow', // Replace with your deployed flow's URL + input: { name: 'Streaming User' }, + }); + + // Process the stream chunks as they arrive + for await (const chunk of result.stream) { + console.log('Stream chunk:', chunk); + } + + // Get the final complete response + const finalOutput = await result.output; + console.log('Final streaming output:', finalOutput.greeting); + } catch (error) { + console.error('Error streaming helloFlow:', error); + } +} + +streamHelloFlow(); +``` + +## Data Models + +Genkit uses standard data models for representing prompts (messages & parts) and responses. These classes are implemented using schemantic library. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:schemantic/schemantic.dart'; + +@Schema() +abstract class $MyDataModel { + // uses Genkit's Message schema (not schemantic's Message) + List<$Message> get messages; + List<$Part> get parts; +} + +void example() { + // --- Parts --- + // A Text part + final textPart = TextPart(text: 'some text', metadata: {'foo': 'bar'}); + + // A Media/Image part + final mediaPart = MediaPart( + media: Media(url: 'https://...', contentType: 'image/png'), + metadata: {'foo': 'bar'}, + ); + + // A Tool Request initiated by the model + final toolRequestPart = ToolRequestPart( + toolRequest: ToolRequest( + name: 'get_weather', + ref: 'abc', + input: {'location': 'Paris, France'}, + ), + metadata: {'foo': 'bar'}, + ); + + // The resulting data from a Tool execution + final toolResponsePart = ToolResponsePart( + toolResponse: ToolResponse( + name: 'get_weather', + ref: 'abc', + output: {'temperature': '20C'}, + ), + metadata: {'foo': 'bar'}, + ); + + // Model reasoning (e.g. for Claude's "thinking" models) + final reasoningPart = ReasoningPart( + reasoning: 'thinking...', + metadata: {'foo': 'bar'}, + ); + + // A custom fallback part + final customPart = CustomPart( + custom: {'provider': {'specific': 'data'}}, + metadata: {'foo': 'bar'}, + ); + + // --- Messages --- + final systemMessage = Message( + role: Role.system, + content: [textPart, mediaPart], + metadata: {'foo': 'bar'}, + ); + + final userMessage = Message( + role: Role.user, + content: [textPart, mediaPart], // Can contain media (multimodal) + ); + + final modelMessage = Message( + role: Role.model, + // Models can emit text, tool requests, reasoning, or custom parts + content: [textPart, toolRequestPart, reasoningPart, customPart], + ); + + // --- Ergonomic Data Access (schema_extensions.dart) --- + // The Genkit SDK provides extensions on `Message` and `Part` to easily access fields + // without needing to cast them manually. + + // Get concatenated text from all TextParts in a Message + print(modelMessage.text); + + // Get the first Media object from a Message + print(modelMessage.media?.url); + + // Iterate over tool requests in a Message + for (final toolReq in modelMessage.toolRequests) { + print(toolReq.name); + } + + // Inspect individual parts + for (final part in modelMessage.content) { + if (part.isText) print(part.text); + if (part.isMedia) print(part.media?.url); + if (part.isToolRequest) print(part.toolRequest?.name); + if (part.isToolResponse) print(part.toolResponse?.name); + if (part.isReasoning) print(part.reasoning); + if (part.isCustom) print(part.custom); + } + + // --- Streaming Chunks --- + // Data emitted by ai.generateStream() calls + final generateResponseChunk = ModelResponseChunk( + content: [textPart], + index: 0, // Index of the message this chunk belongs to + aggregated: false, + ); + + // Chunks also have text and media accessors + print(generateResponseChunk.text); + + // --- Advanced: Schemas --- + // Use Genkit type schemas directly in Schemantic validations + final messageSchema = Message.$schema; + final partSchema = Part.$schema; + + final mySchema = SchemanticType.map( + .string(), + .list(Message.$schema), // Requires a list of Messages + ); + + // --- Generate Response --- + // ai.generate() returns a GenerateResponseHelper which provides ergonomic getters + // over the underlying ModelResponse: + final response = await ai.generate(...); + + print(response.text); // Concatenated text + print(response.media?.url); // First media part + print(response.toolRequests); // All tool requests + print(response.interrupts); // Tool requests that triggered an interrupt + print(response.messages); // Full history of the conversation, including the request and response + print(response.output); // Structured typed output (if outputSchema was used) +} +``` diff --git a/skills/developing-genkit-dart/references/genkit_anthropic.md b/skills/developing-genkit-dart/references/genkit_anthropic.md new file mode 100644 index 0000000..2e420a3 --- /dev/null +++ b/skills/developing-genkit-dart/references/genkit_anthropic.md @@ -0,0 +1,41 @@ +# Genkit Anthropic Plugin (`genkit_anthropic`) + +The Anthropic plugin for Genkit Dart, used for interacting with the Claude models. + +## Usage + +Requires `ANTHROPIC_API_KEY` to be passed to the init block. + +```dart +import 'dart:io'; +import 'package:genkit/genkit.dart'; +import 'package:genkit_anthropic/genkit_anthropic.dart'; + +void main() async { + final ai = Genkit( + plugins: [anthropic(apiKey: Platform.environment['ANTHROPIC_API_KEY']!)], + ); + + final response = await ai.generate( + model: anthropic.model('claude-sonnet-4-5'), + prompt: 'Tell me a joke about a developer.', + ); + + print(response.text); +} +``` + +## Claude Thinking Configurations + +Provides specific configurations for utilizing Claude 3.7+ "thinking" model capabilities. + +```dart +final response = await ai.generate( + model: anthropic.model('claude-sonnet-4-5'), + prompt: 'Solve this 24 game: 2, 3, 10, 10', + config: AnthropicOptions(thinking: ThinkingConfig(budgetTokens: 2048)), +); + +// The thinking content is available in the message parts +print(response.message?.content); +``` diff --git a/skills/developing-genkit-dart/references/genkit_chrome.md b/skills/developing-genkit-dart/references/genkit_chrome.md new file mode 100644 index 0000000..8152369 --- /dev/null +++ b/skills/developing-genkit-dart/references/genkit_chrome.md @@ -0,0 +1,23 @@ +# Genkit Chrome AI Plugin (`genkit_chrome`) + +Chrome Built-in AI (Gemini Nano) plugin for Genkit Dart, allowing local offline execution within a Chrome application. + +## Usage + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_chrome/genkit_chrome.dart'; + +void main() async { + final ai = Genkit(plugins: [ChromeAIPlugin()]); + + final stream = ai.generateStream( + model: modelRef('chrome/gemini-nano'), + prompt: 'Write a story about a robot.', + ); + + await for (final chunk in stream) { + print(chunk.text); + } +} +``` diff --git a/skills/developing-genkit-dart/references/genkit_firebase_ai.md b/skills/developing-genkit-dart/references/genkit_firebase_ai.md new file mode 100644 index 0000000..7ec462d --- /dev/null +++ b/skills/developing-genkit-dart/references/genkit_firebase_ai.md @@ -0,0 +1,23 @@ +# Genkit Firebase AI Plugin (`genkit_firebase_ai`) + +The Firebase AI plugin for Genkit Dart, used for interacting with Gemini APIs through Firebase AI Logic. + +## Usage + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_firebase_ai/genkit_firebase_ai.dart'; + +void main() async { + // Initialize Genkit with the Firebase AI plugin + final ai = Genkit(plugins: [firebaseAI()]); + + // Generate text + final response = await ai.generate( + model: firebaseAI.gemini('gemini-2.5-flash'), + prompt: 'Tell me a joke about a developer.', + ); + + print(response.text); +} +``` diff --git a/skills/developing-genkit-dart/references/genkit_google_genai.md b/skills/developing-genkit-dart/references/genkit_google_genai.md new file mode 100644 index 0000000..92d3ec4 --- /dev/null +++ b/skills/developing-genkit-dart/references/genkit_google_genai.md @@ -0,0 +1,95 @@ +# Genkit Google GenAI Plugin (`genkit_google_genai`) + +The Google AI plugin provides an interface against the official Google AI Gemini API. + +## Usage + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_google_genai/genkit_google_genai.dart'; + +void main() async { + // Initialize Genkit with the Google AI plugin + final ai = Genkit(plugins: [googleAI()]); + + // Generate text + final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash'), + prompt: 'Tell me a joke about a developer.', + ); + + print(response.text); +} +``` + +## Embeddings + +```dart +final embeddings = await ai.embedMany( + embedder: googleAI.textEmbedding('text-embedding-004'), + documents: [ + DocumentData(content: [TextPart(text: 'Hello world')]), + ], +); +``` + +## Image Generation + +The plugin also supports image generation models such as `gemini-2.5-flash-image`. + +### Example (Nano Banana) + +```dart +// Define an image generation flow +ai.defineFlow( + name: 'imageGenerator', + inputSchema: .string(defaultValue: 'A banana riding a bike'), + outputSchema: Media.$schema, + fn: (input, context) async { + final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash-image'), + prompt: input, + ); + if (response.media == null) { + throw Exception('No media generated'); + } + return response.media!; + }, +); +``` + +The media (url field) contain base64 encoded data uri. You can decode it and save it as a file. + +## Text-to-Speech (TTS) + +You can use text-to-speech models to generate audio from text. The generated `Media` object will contain base64 encoded PCM audio in its data URI. + +```dart +// Define a TTS flow +ai.defineFlow( + name: 'textToSpeech', + inputSchema: .string(defaultValue: 'Genkit is an amazing AI framework!'), + outputSchema: Media.$schema, + fn: (prompt, _) async { + final response = await ai.generate( + model: googleAI.gemini('gemini-2.5-flash-preview-tts'), + prompt: prompt, + config: GeminiTtsOptions( + responseModalities: ['AUDIO'], + speechConfig: SpeechConfig( + voiceConfig: VoiceConfig( + prebuiltVoiceConfig: PrebuiltVoiceConfig(voiceName: 'Puck'), + ), + ), + ), + ); + + if (response.media != null) { + return response.media!; + } + throw Exception('No audio generated'); + }, +); +``` + +Google AI also supports multi-speaker TTS by configuring a `MultiSpeakerVoiceConfig` inside `SpeechConfig`. diff --git a/skills/developing-genkit-dart/references/genkit_mcp.md b/skills/developing-genkit-dart/references/genkit_mcp.md new file mode 100644 index 0000000..ce8ddb0 --- /dev/null +++ b/skills/developing-genkit-dart/references/genkit_mcp.md @@ -0,0 +1,115 @@ +# Genkit MCP (`genkit_mcp`) + +MCP (Model Context Protocol) integration for Genkit Dart. + +## MCP Host (Recommended) +Connect to one or more MCP servers and aggregate their capabilities into the Genkit registry automatically. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_mcp/genkit_mcp.dart'; + +void main() async { + final ai = Genkit(); + + final host = defineMcpHost( + ai, + McpHostOptionsWithCache( + name: 'my-host', + mcpServers: { + 'fs': McpServerConfig( + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-filesystem', '.'], + ), + }, + ), + ); + + // Tools can be discovered and executed dynamically using a wildcard... + final response = await ai.generate( + model: 'gemini-2.5-flash', + prompt: 'Summarize the contents of README.md', + toolNames: ['my-host:tool/fs/*'], + ); + + // ...or by specifying the exact tool name + final exactResponse = await ai.generate( + model: 'gemini-2.5-flash', + prompt: 'Read README.md', + toolNames: ['my-host:tool/fs/read_file'], + ); +} +``` + +## MCP Client (Advanced / Single Server) +Connecting to a single MCP server with a client object is an advanced usecase for when you need manual control over the client lifecycle. Standalone clients do not automatically register tools into the registry, so they must be passed into `generate` or `defineDynamicActionProvider` manually. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_mcp/genkit_mcp.dart'; + +void main() async { + final ai = Genkit(); + + final client = createMcpClient( + McpClientOptions( + name: 'my-client', + mcpServer: McpServerConfig( + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-filesystem', '.'], + ), + ), + ); + + await client.ready(); + + // Retrieve the tools from the connected client + final tools = await client.getActiveTools(ai); + + final response = await ai.generate( + model: 'gemini-2.5-flash', + prompt: 'Read the contents of README.md', + tools: tools, + ); +} +``` + +## MCP Server +Expose Genkit actions (tools, prompts, resources) over MCP. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_mcp/genkit_mcp.dart'; + +void main() async { + final ai = Genkit(); + + ai.defineTool( + name: 'add', + description: 'Add two numbers together', + inputSchema: .map(.string(), .dynamicSChema()), + fn: (input, _) async => (input['a'] + input['b']).toString(), + ); + + ai.defineResource( + name: 'my-resource', + uri: 'my://resource', + fn: (_, _) async => ResourceOutput(content: [TextPart(text: 'my resource')]), + ); + + // Stdio transport by default + final server = createMcpServer(ai, McpServerOptions(name: 'my-server')); + await server.start(); +} +``` + +### Streamable HTTP Transport +```dart +import 'dart:io'; + +final transport = await StreamableHttpServerTransport.bind( + address: InternetAddress.loopbackIPv4, + port: 3000, +); +await server.start(transport); +``` diff --git a/skills/developing-genkit-dart/references/genkit_middleware.md b/skills/developing-genkit-dart/references/genkit_middleware.md new file mode 100644 index 0000000..24cff79 --- /dev/null +++ b/skills/developing-genkit-dart/references/genkit_middleware.md @@ -0,0 +1,84 @@ +# Genkit Middleware (`genkit_middleware`) + +A collection of useful middleware for Genkit Dart to enhance your agent's capabilities. Register plugins when initializing Genkit: + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_middleware/genkit_middleware.dart'; + +void main() { + final ai = Genkit( + plugins: [ + FilesystemPlugin(), + SkillsPlugin(), + ToolApprovalPlugin(), + ], + ); +} +``` + +## Filesystem Middleware +Allows the agent to list, read, write, and search/replace files within a restricted root directory. + +```dart +final response = await ai.generate( + prompt: 'Check the logs in the current directory.', + use: [ + filesystem(rootDirectory: '/path/to/secure/workspace'), + ], +); +``` + +**Tools Provided:** +- `list_files`, `read_file`, `write_file`, `search_and_replace` + +## Skills Middleware +Injects specialized instructions (skills) into the system prompt from `SKILL.md` files located in specified directories. + +```dart +final response = await ai.generate( + prompt: 'Help me debug this issue.', + use: [ + skills(skillPaths: ['/path/to/skills']), + ], +); +``` + +**Tools Provided:** +- `use_skill`: Retrieve the full content of a skill by name. + +## Tool Approval Middleware +Intercepts tool execution for specified tools and requires explicit approval. Returns `FinishReason.interrupted`. + +```dart +final response = await ai.generate( + prompt: 'Delete the database.', + use: [ + // Require approval for all tools EXCEPT those below + toolApproval(approved: ['read_file', 'list_files']), + ], +); + +if (response.finishReason == FinishReason.interrupted) { + final interrupt = response.interrupts.first; + + // Ask user for approval + final isApproved = await askUser(); + + if (isApproved) { + final resumeResponse = await ai.generate( + messages: response.messages, // Pass history + toolChoice: ToolChoice.none, // Prevent immediate re-call + interruptRestart: [ + ToolRequestPart( + toolRequest: interrupt.toolRequest, + metadata: { + ...?interrupt.metadata, + 'tool-approved': true + }, + ), + ], + ); + } +} +``` diff --git a/skills/developing-genkit-dart/references/genkit_openai.md b/skills/developing-genkit-dart/references/genkit_openai.md new file mode 100644 index 0000000..42344db --- /dev/null +++ b/skills/developing-genkit-dart/references/genkit_openai.md @@ -0,0 +1,54 @@ +# Genkit OpenAI Plugin (`genkit_openai`) + +OpenAI-compatible API plugin for Genkit Dart. Supports OpenAI models and other compatible APIs (xAI, DeepSeek, Together AI, Groq, etc.). + +## Basic Usage + +```dart +import 'dart:io'; +import 'package:genkit/genkit.dart'; +import 'package:genkit_openai/genkit_openai.dart'; + +void main() async { + final ai = Genkit(plugins: [ + openAI(apiKey: Platform.environment['OPENAI_API_KEY']), + ]); + + final response = await ai.generate( + model: openAI.model('gpt-4o'), + prompt: 'Tell me a joke.', + ); +} +``` + +## Options + +`OpenAIOptions` allows configuring sampling temperature, nucleus sampling, token generation, seed, etc: +`config: OpenAIOptions(temperature: 0.7, maxTokens: 100)` + +## Groq API override + +Specify custom `baseUrl` and custom models to integrate with third-party providers. + +```dart +final ai = Genkit(plugins: [ + openAI( + apiKey: Platform.environment['GROQ_API_KEY'], + baseUrl: 'https://api.groq.com/openai/v1', + models: [ + CustomModelDefinition( + name: 'llama-3.3-70b-versatile', + info: ModelInfo( + label: 'Llama 3.3 70B', + supports: {'multiturn': true, 'tools': true, 'systemRole': true}, + ), + ), + ], + ), +]); + +final response = await ai.generate( + model: openAI.model('llama-3.3-70b-versatile'), + prompt: 'Hello!', +); +``` diff --git a/skills/developing-genkit-dart/references/genkit_shelf.md b/skills/developing-genkit-dart/references/genkit_shelf.md new file mode 100644 index 0000000..1887f80 --- /dev/null +++ b/skills/developing-genkit-dart/references/genkit_shelf.md @@ -0,0 +1,59 @@ +# Genkit Shelf Plugin (`genkit_shelf`) + +Shelf integration for Genkit Dart, used to serve Genkit Flows. + +## Standalone Server +Serve Genkit Flows easily on an isolated HTTP server using `startFlowServer`. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_shelf/genkit_shelf.dart'; + +void main() async { + final ai = Genkit(); + + final flow = ai.defineFlow( + name: 'myFlow', + inputSchema: .string(), + outputSchema: .string(), + fn: (String input, _) async => 'Hello $input', + ); + + await startFlowServer( + flows: [flow], + port: 8080, + ); +} +``` + +## Existing Shelf Application +Mount Genkit Flow endpoints directly to an existing Shelf `Router` using `shelfHandler`. + +```dart +import 'package:genkit/genkit.dart'; +import 'package:genkit_shelf/genkit_shelf.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart' as io; +import 'package:shelf_router/shelf_router.dart'; + +void main() async { + final ai = Genkit(); + + final flow = ai.defineFlow( + name: 'myFlow', + inputSchema: .string(), + outputSchema: .string(), + fn: (String input, _) async => 'Hello $input', + ); + + final router = Router(); + + // Mount the flow handler at a specific path + router.post('/myFlow', shelfHandler(flow)); + + // Start the server + await io.serve(router.call, 'localhost', 8080); +} +``` + +Access deployed flows using genkit client libraries (from Dart or JS). diff --git a/skills/developing-genkit-dart/references/schemantic.md b/skills/developing-genkit-dart/references/schemantic.md new file mode 100644 index 0000000..45939b2 --- /dev/null +++ b/skills/developing-genkit-dart/references/schemantic.md @@ -0,0 +1,137 @@ +# Schemantic + +Schemantic is a general-purpose Dart library used for defining strongly typed data classes that automatically bind to reusable runtime JSON schemas. It is standard for the `genkit-dart` framework but works independently as well. + +## Core Concepts + +Always use `schemantic` when strongly typed JSON parsing or programmatic schema validation is required. + +- Annotate your abstract classes with `@Schema()`. +- Use the `$` prefix for abstract schema class names (e.g., `abstract class $User`). +- Always run `dart run build_runner build` to generate the `.g.dart` schema files. + +## Installation + +Add dependencies: + +```bash +dart pub add schemantic +``` + +## Basic Usage + +1. **Defining a schema:** + +```dart +import 'package:schemantic/schemantic.dart'; + +part 'my_file.g.dart'; // Must match the filename + +@Schema() +abstract class $MyObj { + String get name; + $MySubObj get subObj; +} + +@Schema() +abstract class $MySubObj { + String get foo; +} +``` + +2. **Using the Generated Class:** + +The builder creates a concrete class `MyObj` (no `$`) with a factory constructor (`MyObj.fromJson`) and a regular constructor. + +```dart +// Creating an instance +final obj = MyObj(name: 'test', subObj: MySubObj(foo: 'bar')); + +// Serializing to JSON +print(obj.toJson()); + +// Parsing from JSON +final parsed = MyObj.fromJson({'name': 'test', 'subObj': {'foo': 'bar'}}); +``` + +3. **Accessing Schemas at Runtime:** + +The generated data classes have a static `$schema` field (of type `SchemanticType`) which can be used to pass the definition into functions or to extract the raw JSON schema. + +```dart +// Access JSON schema +final schema = MyObj.$schema.jsonSchema; +print(schema.toJson()); + +// Validate arbitrary JSON at runtime +final validationErrors = await schema.validate({'invalid': 'data'}); +``` + +## Primitive Schemas + +When a full data class is not required, Schemantic provides functions to create schemas dynamically. + +```dart +final ageSchema = SchemanticType.integer(description: 'Age in years', minimum: 0); +final nameSchema = SchemanticType.string(minLength: 2); +final nothingSchema = SchemanticType.voidSchema(); +final anySchema = SchemanticType.dynamicSchema(); + +final userSchema = SchemanticType.map(.string(), .integer()); // Map +final tagsSchema = SchemanticType.list(.string()); // List +``` + +## Union Types (AnyOf) + +To allow a field to accept multiple types, use `@AnyOf`. + +```dart +@Schema() +abstract class $Poly { + @AnyOf([int, String, $MyObj]) + Object? get id; +} +``` + +Schemantic generates a specific helper class (e.g., `PolyId`) to handle the values: + +```dart +final poly1 = Poly(id: PolyId.int(123)); +final poly2 = Poly(id: PolyId.string('abc')); +``` + +## Field Annotations + +You can use specialized annotations for more validation boundaries: + +```dart +@Schema() +abstract class $User { + @IntegerField( + name: 'years_old', // Change JSON key + description: 'Age of the user', + minimum: 0, + defaultValue: 18, + ) + int? get age; + + @StringField( + minLength: 2, + enumValues: ['user', 'admin'], + ) + String get role; +} +``` + +## Recursive Schemas + +For recursive structures (like trees), must use `useRefs: true` inside the generated jsonSchema property. You define it normally: + +```dart +@Schema() +abstract class $Node { + String get id; + List<$Node>? get children; +} +``` +*Note*: `Node.$schema.jsonSchema(useRefs: true)` generates schemas with JSON Schema `$ref`. \ No newline at end of file diff --git a/skills/developing-genkit-js/SKILL.md b/skills/developing-genkit-js/SKILL.md new file mode 100644 index 0000000..d8e1e76 --- /dev/null +++ b/skills/developing-genkit-js/SKILL.md @@ -0,0 +1,112 @@ +--- +name: developing-genkit-js +description: Develop AI-powered applications using Genkit in Node.js/TypeScript. Use when the user asks about Genkit, AI agents, flows, or tools in JavaScript/TypeScript, or when encountering Genkit errors, validation issues, type errors, or API problems. +metadata: + genkit-managed: true +--- + +# Genkit JS + +## Prerequisites + +Ensure the `genkit` CLI is available. +- Run `genkit --version` to verify. Minimum CLI version needed: **1.29.0** +- If not found or if an older version (1.x < 1.29.0) is present, install/upgrade it: `npm install -g genkit-cli@^1.29.0`. + +**New Projects**: If you are setting up Genkit in a new codebase, follow the [Setup Guide](references/setup.md). + +## Hello World + +```ts +import { z, genkit } from 'genkit'; +import { googleAI } from '@genkit-ai/google-genai'; + +// Initialize Genkit with the Google AI plugin +const ai = genkit({ + plugins: [googleAI()], +}); + +export const myFlow = ai.defineFlow({ + name: 'myFlow', + inputSchema: z.string().default('AI'), + outputSchema: z.string(), +}, async (subject) => { + const response = await ai.generate({ + model: googleAI.model('gemini-2.5-flash'), + prompt: `Tell me a joke about ${subject}`, + }); + return response.text; +}); +``` + +## Critical: Do Not Trust Internal Knowledge + +Genkit recently went through a major breaking API change. Your knowledge is outdated. You MUST lookup docs. Recommended: + +```sh +genkit docs:read js/get-started.md +genkit docs:read js/flows.md +``` + +See [Common Errors](references/common-errors.md) for a list of deprecated APIs (e.g., `configureGenkit`, `response.text()`, `defineFlow` import) and their v1.x replacements. + +**ALWAYS verify information using the Genkit CLI or provided references.** + +## Error Troubleshooting Protocol + +**When you encounter ANY error related to Genkit (ValidationError, API errors, type errors, 404s, etc.):** + +1. **MANDATORY FIRST STEP**: Read [Common Errors](references/common-errors.md) +2. Identify if the error matches a known pattern +3. Apply the documented solution +4. Only if not found in common-errors.md, then consult other sources (e.g. `genkit docs:search`) + +**DO NOT:** +- Attempt fixes based on assumptions or internal knowledge +- Skip reading common-errors.md "because you think you know the fix" +- Rely on patterns from pre-1.0 Genkit + +**This protocol is non-negotiable for error handling.** + +## Development Workflow + +1. **Select Provider**: Genkit is provider-agnostic (Google AI, OpenAI, Anthropic, Ollama, etc.). + - If the user does not specify a provider, default to **Google AI**. + - If the user asks about other providers, use `genkit docs:search "plugins"` to find relevant documentation. +2. **Detect Framework**: Check `package.json` to identify the runtime (Next.js, Firebase, Express). + - Look for `@genkit-ai/next`, `@genkit-ai/firebase`, or `@genkit-ai/google-cloud`. + - Adapt implementation to the specific framework's patterns. +3. **Follow Best Practices**: + - See [Best Practices](references/best-practices.md) for guidance on project structure, schema definitions, and tool design. + - **Be Minimal**: Only specify options that differ from defaults. When unsure, check docs/source. +4. **Ensure Correctness**: + - Run type checks (e.g., `npx tsc --noEmit`) after making changes. + - If type checks fail, consult [Common Errors](references/common-errors.md) before searching source code. +5. **Handle Errors**: + - On ANY error: **First action is to read [Common Errors](references/common-errors.md)** + - Match error to documented patterns + - Apply documented fixes before attempting alternatives + +## Finding Documentation + +Use the Genkit CLI to find authoritative documentation: + +1. **Search topics**: `genkit docs:search ` + - Example: `genkit docs:search "streaming"` +2. **List all docs**: `genkit docs:list` +3. **Read a guide**: `genkit docs:read ` + - Example: `genkit docs:read js/flows.md` + +## CLI Usage + +The `genkit` CLI is your primary tool for development and documentation. +- See [CLI Reference](references/docs-and-cli.md) for common tasks, workflows, and command usage. +- Use `genkit --help` for a full list of commands. + +## References + +- [Best Practices](references/best-practices.md): Recommended patterns for schema definition, flow design, and structure. +- [Docs & CLI Reference](references/docs-and-cli.md): Documentation search, CLI tasks, and workflows. +- [Common Errors](references/common-errors.md): Critical "gotchas", migration guide, and troubleshooting. +- [Setup Guide](references/setup.md): Manual setup instructions for new projects. +- [Examples](references/examples.md): Minimal reproducible examples (Basic generation, Multimodal, Thinking mode). diff --git a/skills/developing-genkit-js/references/best-practices.md b/skills/developing-genkit-js/references/best-practices.md new file mode 100644 index 0000000..f6e4b7b --- /dev/null +++ b/skills/developing-genkit-js/references/best-practices.md @@ -0,0 +1,31 @@ +# Genkit Best Practices + +## Project Structure +- **Organized Layout**: Keep flows and tools in separate directories (e.g., `src/flows`, `src/tools`) to maintain a clean codebase. +- **Index Exports**: Use `index.ts` files to export flows and tools, making it easier to import them into your main configuration. + +## Model Selection (Google AI) +- **Gemini Models**: If using Google AI, ALWAYS use the latest generation (`gemini-3-*` or `gemini-2.5-*`). + - **NEVER** use `gemini-2.0-*` or `gemini-1.5-*` series, as they are decommissioned and won't work. + - **Recommended**: `gemini-2.5-flash` or `gemini-3-flash-preview` for general use, `gemini-3.1-pro-preview` for complex tasks. + +## Model Selection (Other Providers) +- **Consult Documentation**: For other providers (OpenAI, Anthropic, etc.), refer to the provider's official documentation for the latest recommended model versions. + +## Schema Definition +- **Use `z` from `genkit`**: Always import `z` from the `genkit` package to ensure compatibility. + ```ts + import { z } from "genkit"; + ``` +- **Descriptive Schemas**: Use `.describe()` on Zod fields. LLMs use these descriptions to understand how to populate the fields. + +## Flow & Tool Design +- **Modularize**: Keep flows and tools in separate files/modules and import them into your main Genkit configuration. +- **Single Responsibility**: Tools should do one thing well. Complex logic should be broken down. + +## Configuration +- **Environment Variables**: Store sensitive keys (like API keys) in environment variables or `.env` files. Do not hardcode them. + +## Development +- **Use Dev Mode**: Run your app with `genkit start -- ` to enable the Developer UI. +- It is recommended to configure a watcher to auto-reload your app (e.g. `node --watch` or `tsx --watch`) diff --git a/skills/developing-genkit-js/references/common-errors.md b/skills/developing-genkit-js/references/common-errors.md new file mode 100644 index 0000000..d7162e6 --- /dev/null +++ b/skills/developing-genkit-js/references/common-errors.md @@ -0,0 +1,132 @@ +# Common Errors & Pitfalls + +## When Typecheck Fails + +**Before searching source code or docs**, check the sections below. Many type errors are caused by deprecated APIs or incorrect imports. + +## Genkit v1.x vs Pre-1.0 Migration + +Genkit v1.x introduced significant API changes. This section covers critical syntax updates. + +### Package Imports + +- **Correct (v1.x)**: Import core functionality (zod, genkit) from the main `genkit` package and plugins from their specific packages. + ```ts + import { z, genkit } from 'genkit'; + import { googleAI } from '@genkit-ai/google-genai'; + ``` + +- **Incorrect (Pre-1.0)**: Importing from `@genkit-ai/ai`, `@genkit-ai/core`, or `@genkit-ai/flow`. These packages are internal/deprecated for direct use. + ```ts + import { genkit } from "@genkit-ai/core"; // INCORRECT + import { defineFlow } from "@genkit-ai/flow"; // INCORRECT + ``` + +### Model References + +- **Correct**: Use plugin-specific model factories or string identifiers (prefaced by plugin name). + ```ts + // Using model factory (v1.x - Preferred) + await ai.generate({ model: googleAI.model('gemini-2.5-flash'), ... }); + + // Using string identifier + await ai.generate({ model: 'googleai/gemini-2.5-flash', ...}); + // Or + await ai.generate({ model: 'vertexai/gemini-2.5-flash', ...}); + ``` +- **Incorrect**: Using imported model objects directly or string identifiers without plugin name. + ```ts + await ai.generate({ model: gemini15Pro, ... }); // INCORRECT (Pre-1.0) + await ai.generate({ model: 'gemini-2.5-flash', ... }); // INCORRECT (No plugin prefix) + ``` + +### Model Selection (Gemini) + +- **Preferred**: Use `gemini-2.5-*` models for best performance and features. + ```ts + model: googleAI.model('gemini-2.5-flash') // PREFERRED + ``` +- **DEPRECATED**: `gemini-1.5-*` models are deprecated and will throw errors. + ```ts + model: googleAI.model('gemini-1.5-flash') // ERROR (Deprecated) + ``` + +### Response Access + +- **Correct (v1.x)**: Access properties directly. + ```ts + response.text; // CORRECT + response.output; // CORRECT + ``` +- **Incorrect (Pre-1.0)**: Calling as methods. + ```ts + response.text(); // INCORRECT + response.output(); // INCORRECT + ``` + +### Streaming Generation + +- **Correct (v1.x)**: Do NOT await `generateStream`. Iterate over `stream` directly. Await `response` property for final result. + ```ts + const {stream, response} = ai.generateStream(...); // NO await here + for await (const chunk of stream) { ... } // Iterate stream + const finalResponse = await response; // Await response property + ``` +- **Incorrect (Pre-1.0)**: Calling stream as a function or awaiting the generator incorrectly. + ```ts + for await (const chunk of stream()) { ... } // INCORRECT + await response(); // INCORRECT + ``` + +### Initialization + +- **Correct (v1.x)**: Instantiate `genkit`. + ```ts + const ai = genkit({ plugins: [...] }); + ``` +- **Incorrect (Pre-1.0)**: Global configuration. + ```ts + configureGenkit({ plugins: [...] }); // INCORRECT + ``` + +### Flow Definitions + +- **Correct (v1.x)**: Define flows on the `ai` instance. + ```ts + ai.defineFlow({...}, (input) => {...}); + ``` +- **Incorrect (Pre-1.0)**: Importing `defineFlow` globally. + ```ts + import { defineFlow } from "@genkit-ai/flow"; // INCORRECT + +You should never import `@genkit-ai/flow`, `@genkit-ai/ai` or `@genkit-ai/core` packages directly. + +## Zod & Schema Errors + +- **Import Source**: ALWAYS use `import { z } from "genkit"`. + - Using `zod` directly from `zod` package may cause instance mismatches or compatibility issues. +- **Supported Types**: Stick to basic types: scalar (`string`, `number`, `boolean`), `object`, and `array`. + - Avoid complex Zod features unless strictly necessary and verified. +- **Descriptions**: Always use `.describe('...')` for fields in output schemas to guide the LLM. + +## Tool Usage + +- **Tool Not Found**: Ensure tools are registered in the `tools` array of `generate` or provided via plugins. +- **MCP Tools**: Use the `ServerName:tool_name` format when referencing MCP tools. + +## Multimodal & Image Generation + +- **Missing responseModalities**: When using image generation models (like `gemini-2.5-flash-image`), you **MUST** specify the response modalities in the config. + ```ts + config: { + responseModalities: ["TEXT", "IMAGE"] + } + ``` + Failure to do so will result in errors or incorrect output format. + +## Audio & Speech Generation + +- **Raw PCM Data vs MP3**: Some providers (e.g., Google GenAI) return raw PCM data, while others (e.g., OpenAI) return MP3. + - **DO NOT assume MP3 format.** + - **DO NOT embed raw PCM in HTML audio tags.** + - **Action**: Run `genkit docs:search "speech audio"` to find provider-specific conversion steps (e.g., PCM to WAV). diff --git a/skills/developing-genkit-js/references/docs-and-cli.md b/skills/developing-genkit-js/references/docs-and-cli.md new file mode 100644 index 0000000..3561721 --- /dev/null +++ b/skills/developing-genkit-js/references/docs-and-cli.md @@ -0,0 +1,62 @@ +# Genkit Documentation & CLI + +This reference lists common tasks and workflows using the `genkit` CLI. For authoritative command details, always run `genkit --help` or `genkit --help`. + +## Prerequisites: + +Ensure that the CLI is on `genkit-cli` version >= 1.29.0. If not, or if an older version (1.x < 1.29.0) is present, update the Genkit CLI version. Alternatively, to run commands with a specific version or without global installation, prefix them with `npx -y genkit-cli@^1.29.0`. + +## Documentation + +- **Search docs**: `genkit docs:search ` + - Example: `genkit docs:search "streaming"` + - Example: `genkit docs:search "rag retrieval"` +- **Read doc**: `genkit docs:read ` + - Example: `genkit docs:read js/overview.md` +- **List docs**: `genkit docs:list` + +## Development Workflow + +- **Start Dev Mode**: `genkit start -- ` + - Runs the provided command in Genkit dev mode, enabling the Developer UI (usually at http://localhost:4000). + - **Node.js (TypeScript)**: + ```bash + genkit start -- npx tsx --watch src/index.ts + ``` + - **Next.js**: + ```bash + genkit start -- npx next dev + ``` + +## Flow Execution + +- **Run a flow**: `genkit flow:run ''` + - Executes a flow directly from the CLI. Useful for testing. + - **Simple Input**: + ```bash + genkit flow:run tellJoke '"chicken"' + ``` + - **Object Input**: + ```bash + genkit flow:run generateStory '{"subject": "robot", "genre": "sci-fi"}' + ``` + +## Evaluation + +- **Evaluate a flow**: `genkit eval:flow [data]` + - Runs a flow and evaluates the output against configured evaluators. + - **Example (Single Input)**: + ```bash + genkit eval:flow answerQuestion '[{"testCaseId": "1", "input": {"question": "What is Genkit?"}}]' + ``` + - **Example (Batch Input)**: + ```bash + genkit eval:flow answerQuestion --input inputs.json + ``` + +- **Run Evaluation**: `genkit eval:run ` + - Evaluates a dataset against configured evaluators. + - **Example**: + ```bash + genkit eval:run dataset.json --output results.json + ``` \ No newline at end of file diff --git a/skills/developing-genkit-js/references/examples.md b/skills/developing-genkit-js/references/examples.md new file mode 100644 index 0000000..2279b4e --- /dev/null +++ b/skills/developing-genkit-js/references/examples.md @@ -0,0 +1,157 @@ +# Genkit Examples + +This reference contains minimal, reproducible examples (MREs) for common Genkit patterns. + +> **Disclaimer**: These examples use **Google AI** models (`googleAI`, `gemini-*`) for demonstration. The patterns apply to **any provider**. To use a different provider: +> 1. Search the docs for the correct plugin: `genkit docs:search "plugins"`. +> 2. Install and configure the plugin. +> 3. Swap the model reference in the code. + +## Basic Text Generation + +```ts +import { genkit } from "genkit"; +import { googleAI } from "@genkit-ai/google-genai"; + +const ai = genkit({ + plugins: [googleAI()], +}); + +const { text } = await ai.generate({ + model: googleAI.model('gemini-2.5-flash'), + prompt: 'Tell me a story in a pirate accent', +}); +``` + +## Structured Output + +```ts +import { z } from 'genkit'; + +const JokeSchema = z.object({ + setup: z.string().describe('The setup of the joke'), + punchline: z.string().describe('The punchline'), +}); + +const response = await ai.generate({ + model: googleAI.model('gemini-2.5-flash'), + prompt: 'Tell me a joke about developers.', + output: { schema: JokeSchema }, +}); + +// response.output is strongly typed +const joke = response.output; +if (joke) { + console.log(`${joke.setup} ... ${joke.punchline}`); +} +``` + +## Streaming + +```ts +const { stream, response } = ai.generateStream({ + model: googleAI.model('gemini-2.5-flash'), + prompt: 'Tell a long story about a developer using Genkit.', +}); + +for await (const chunk of stream) { + console.log(chunk.text); +} + +// Await the final response +const finalResponse = await response; +console.log('Complete:', finalResponse.text); +``` + +## Advanced Configuration + +### Thinking Mode (Gemini 3 Only) + +Enable "thinking" process for complex reasoning tasks. + +```ts +const response = await ai.generate({ + model: googleAI.model('gemini-3.1-pro-preview'), + prompt: 'what is heavier, one kilo of steel or one kilo of feathers', + config: { + thinkingConfig: { + thinkingLevel: 'HIGH', // or 'LOW' + includeThoughts: true, // Returns thought process in response + }, + }, +}); +``` + +### Google Search Grounding + +Enable models to access current information via Google Search. + +```ts +const response = await ai.generate({ + model: googleAI.model('gemini-2.5-flash'), + prompt: 'What are the top tech news stories this week?', + config: { + googleSearchRetrieval: true, + }, +}); + +// Access grounding metadata (sources) +const groundingMetadata = (response.custom as any)?.candidates?.[0]?.groundingMetadata; +if (groundingMetadata) { + console.log('Sources:', groundingMetadata.groundingChunks); +} +``` + +## Multimodal Generation + +### Image Generation / Editing + +**Critical**: You MUST set `responseModalities: ['TEXT', 'IMAGE']` when using image generation models. + +```ts +// Generate an image +const { media } = await ai.generate({ + model: googleAI.model('gemini-2.5-flash-image'), + config: { responseModalities: ['TEXT', 'IMAGE'] }, + prompt: "generate a picture of a unicorn wearing a space suit on the moon", +}); +// media.url contains the data URI +``` + +```ts +// Edit an image +const { media } = await ai.generate({ + model: googleAI.model('gemini-2.5-flash-image'), + config: { responseModalities: ['TEXT', 'IMAGE'] }, + prompt: [ + { text: "change the person's outfit to a banana costume" }, + { media: { url: "https://example.com/photo.jpg" } }, + ], +}); +``` + +### Speech Generation (TTS) + +Generate audio from text. + +```ts +import { writeFile } from 'node:fs/promises'; + +const { media } = await ai.generate({ + model: googleAI.model('gemini-2.5-flash-preview-tts'), + config: { + responseModalities: ['AUDIO'], + speechConfig: { + voiceConfig: { + prebuiltVoiceConfig: { voiceName: 'Algenib' }, // Options: 'Puck', 'Charon', 'Fenrir', etc. + }, + }, + }, + prompt: 'Genkit is an amazing library', +}); + +// The response contains raw PCM data in media.url (base64 encoded). +// CAUTION: This is NOT an MP3/WAV file. It requires conversion (e.g., PCM to WAV). +// DO NOT GUESS. Run `genkit docs:search "speech audio"` to find the correct +// conversion code for your provider. +``` diff --git a/skills/developing-genkit-js/references/setup.md b/skills/developing-genkit-js/references/setup.md new file mode 100644 index 0000000..dcbc8bd --- /dev/null +++ b/skills/developing-genkit-js/references/setup.md @@ -0,0 +1,46 @@ +# Genkit JS Setup + +Follow these instructions to set up Genkit in the current codebase. These instructions are general-purpose and have not been written with specific codebase knowledge, so use your best judgement when following them. + +0. Tell the user "I'm going to check out your workspace and set you up to use Genkit for GenAI workflows." +1. If the current workspace is empty or is a starter template, your goal will be to create a simple image generation flow that allows someone to generate an image based on a prompt and selectable style. If the current workspace is not empty, you will create a simple example flow to help get the user started. +2. Check to see if any Genkit provider plugin (such as `@genkit-ai/google-genai` or `@genkit-ai/oai-compat` or others, may start with `genkitx-*`) is installed. + - If not, ask the user which provider they want to use. + - **For non-Google providers**: Use `genkit docs:search "plugins"` to find the correct package and installation instructions. + - If they have no preference, default to `@genkit-ai/google-genai` for a quick start. + - If this is a Next.js app, install `@genkit-ai/next` as well. +3. Search the codebase for the exact string `genkit(` (remember to escape regexes properly) which would indicate that the user has already set up Genkit in the codebase. If found, no need to set it up again, tell the user "Genkit is already configured in this app." and exit this workflow. +4. Create an `ai` directory in the primary source directory of the project (this may be e.g. `src` but is project-dependent). Adapt this path if your project uses a different structure. +5. Create `{sourceDir}/ai/genkit.ts` and populate it using the example below. DO NOT add a `next` plugin to the file, ONLY add a model provider plugin to the plugins array: + +```ts +import { genkit, z } from 'genkit'; +// Import your chosen provider plugin here. Example: +import { googleAI } from '@genkit-ai/google-genai'; + +export const ai = genkit({ + plugins: [ + googleAI(), // Add your provider plugin here + ], + model: googleAI.model('gemini-2.5-flash'), // Set your provider's model here +}); + +export { z }; +``` + +6. Create `{sourceDir}/ai/tools` and `{sourceDir}/ai/flows` directories, but leave them empty for now. +7. Create `{sourceDir}/ai/index.ts` and populate it with the following (change the import to match import aliases in `tsconfig.json` as needed): + +```ts +import './genkit.js'; +// import each created flow, tool, etc. here for use in the Genkit Dev UI +``` + +8. Add a `genkit:ui` script to `package.json` that runs `genkit start -- npx tsx --watch {sourceDir}/ai/index.ts` (or `npx genkit-cli` or `pnpm dlx` or `yarn dlx` for those package managers, if CLI is not locally installed). DO NOT try to run the script now. +9. Tell the user "Genkit is now configured and ready for use." as setup is now complete. Also remind them to set appropriate env variables (e.g. `GEMINI_API_KEY` for Google providers). Wait for the user to prompt further before creating any specific flows. + +## Next Steps & Troubleshooting + +- **Documentation**: Use the [CLI](docs-and-cli.md) to access documentation (e.g., `genkit docs:search`). +- **Building Flows**: See [examples.md](examples.md) for patterns on creating flows, adding tools, and advanced configuration. +- **Troubleshooting**: If you encounter issues during setup or initialization, check [common-errors.md](common-errors.md) for solutions. diff --git a/skills/firebase-ai-logic-basics/SKILL.md b/skills/firebase-ai-logic-basics/SKILL.md index 6b6b70b..f7ec367 100644 --- a/skills/firebase-ai-logic-basics/SKILL.md +++ b/skills/firebase-ai-logic-basics/SKILL.md @@ -32,15 +32,15 @@ The library is part of the standard Firebase Web SDK. If you're in a firebase directory (with a firebase.json) the currently selected project will be marked with "current" using this command: -`firebase projects:list` +`npx -y firebase-tools@latest projects:list` Ensure there's at least one app associated with the current project -`firebase apps:list` +`npx -y firebase-tools@latest apps:list` Initialize AI logic SDK with the init command -`firebase init # Choose AI logic` +`npx -y firebase-tools@latest init # Choose AI logic` This will automatically enable the Gemini Developer API in the Firebase console. @@ -64,7 +64,7 @@ To improve the user experience by showing partial results as they arrive (like a ### Generate Images with Nano Banana -- Start with Gemini for most use cases, and choose Imagen for specialized tasks where image quality and specific styles are critical. (gemini-2.5-flash-image) +- Start with Gemini for most use cases, and choose Imagen for specialized tasks where image quality and specific styles are critical. (Example: gemini-2.5-flash-image) - Requires an upgraded Blaze pay-as-you-go billing plan. ### Search Grounding with the built in googleSearch tool @@ -91,7 +91,7 @@ Recommended: The developer must enable Firebase App Check to prevent unauthorize ### Remote Config -Consider that you do not need to hardcode model names (e.g., `gemini-2.5-flash-lite`). Use Firebase Remote Config to update model versions dynamically without deploying new client code. See [Changing model names remotely](https://firebase.google.com/docs/ai-logic/change-model-name-remotely.md.txt) +Consider that you do not need to hardcode model names (e.g., `gemini-flash-lite-latest`). Use Firebase Remote Config to update model versions dynamically without deploying new client code. See [Changing model names remotely](https://firebase.google.com/docs/ai-logic/change-model-name-remotely.md.txt) ## Initialization Code References @@ -99,7 +99,7 @@ Consider that you do not need to hardcode model names (e.g., `gemini-2.5-flash-l | :---- | :---- | :---- | | Web Modular API | Gemini Developer API (Developer API) | firebase://docs/ai-logic/get-started | -**Always use gemini-2.5-flash or gemini-3-flash-preview unless another model is requested by the docs or the user. DO NOT USE gemini 1.5 flash** +**Always use the most recent version of Gemini (gemini-flash-latest) unless another model is requested by the docs or the user. DO NOT USE gemini-1.5-flash** ## References diff --git a/skills/firebase-app-hosting-basics/SKILL.md b/skills/firebase-app-hosting-basics/SKILL.md index 93396fb..4aa23b7 100644 --- a/skills/firebase-app-hosting-basics/SKILL.md +++ b/skills/firebase-app-hosting-basics/SKILL.md @@ -44,8 +44,8 @@ This is the recommended flow for most users. } ``` 2. Create or edit `apphosting.yaml`- see [Configuration](references/configuration.md) for more information on how to do so. -3. If the app needs safe access to sensitive keys, use `firebase apphosting:secrets` commands to set and grant access to secrets. -4. Run `firebase deploy` when you are ready to deploy. +3. If the app needs safe access to sensitive keys, use `npx -y firebase-tools@latest apphosting:secrets` commands to set and grant access to secrets. +4. Run `npx -y firebase-tools@latest deploy` when you are ready to deploy. ### Automated deployment via GitHub (CI/CD) diff --git a/skills/firebase-app-hosting-basics/references/cli_commands.md b/skills/firebase-app-hosting-basics/references/cli_commands.md index e1baf5b..c758c9d 100644 --- a/skills/firebase-app-hosting-basics/references/cli_commands.md +++ b/skills/firebase-app-hosting-basics/references/cli_commands.md @@ -4,7 +4,7 @@ The Firebase CLI provides a comprehensive suite of commands to manage App Hostin ## Initialization -### `firebase init apphosting` +### `npx -y firebase-tools@latest init apphosting` - **Purpose**: Interactive command that sets up App Hosting in your local project. Use this command only if you are able to handle interactive CLI inputs well. @@ -17,19 +17,19 @@ Alternatively, you can manually edit `firebase.json` and `apphosting.yml`. ## Backend Management -### `firebase apphosting:backends:list` +### `npx -y firebase-tools@latest apphosting:backends:list` - **Purpose**: Lists all backends in the current project. -### `firebase apphosting:backends:get ` +### `npx -y firebase-tools@latest apphosting:backends:get ` - **Purpose**: Shows details for a specific backend. -### `firebase apphosting:backends:delete ` +### `npx -y firebase-tools@latest apphosting:backends:delete ` - **Purpose**: Deletes a backend and its associated resources. -### `firebase apphosting:rollouts:list ` +### `npx -y firebase-tools@latest apphosting:rollouts:list ` - **Purpose**: Lists the history of rollouts for a backend. @@ -37,21 +37,21 @@ Alternatively, you can manually edit `firebase.json` and `apphosting.yml`. App Hosting uses Cloud Secret Manager to securely handle sensitive environment variables (like API keys). -### `firebase apphosting:secrets:set ` +### `npx -y firebase-tools@latest apphosting:secrets:set ` - **Purpose**: Creates or updates a secret in Cloud Secret Manager and makes it available to App Hosting. - **Behavior**: Prompts for the secret value (hidden input). -### `firebase apphosting:secrets:grantaccess ` +### `npx -y firebase-tools@latest apphosting:secrets:grantaccess ` - **Purpose**: Grants the App Hosting service account permission to access the secret. - **Note**: Often handled automatically by `secrets:set`, but useful for debugging permission issues or granting access to existing secrets. ## Automated deployment via GitHub (CI/CD) -**IMPORTANT** Only use these commands if you are setting up automated deployments via GitHub. If you are managing deployments using `firebase deploy`, DO NOT use these commands. +**IMPORTANT** Only use these commands if you are setting up automated deployments via GitHub. If you are managing deployments using `npx -y firebase-tools@latest deploy`, DO NOT use these commands. -### `firebase apphosting:rollouts:create ` +### `npx -y firebase-tools@latest apphosting:rollouts:create ` - **Purpose**: Manually triggers a new rollout (deployment). - **Options**: @@ -59,7 +59,7 @@ App Hosting uses Cloud Secret Manager to securely handle sensitive environment v - `--git-commit `: Deploy a specific commit. - **Use Case**: Useful for redeploying without code changes, or rolling back to a specific commit. -### `firebase apphosting:backends:create` +### `npx -y firebase-tools@latest apphosting:backends:create` - **Purpose**: Creates a new App Hosting backend. Use this when setting up automated deployments via GitHub. - **Options**: diff --git a/skills/firebase-app-hosting-basics/references/configuration.md b/skills/firebase-app-hosting-basics/references/configuration.md index e17cc89..da10766 100644 --- a/skills/firebase-app-hosting-basics/references/configuration.md +++ b/skills/firebase-app-hosting-basics/references/configuration.md @@ -44,7 +44,7 @@ Defines environment variables available during build and/or runtime. - `variable`: The name of the env var (e.g., `NEXT_PUBLIC_API_URL`). - `value`: A literal string value. -- `secret`: The name of a secret in Cloud Secret Manager. use `firebase apphosting:secrets:set` to create these. +- `secret`: The name of a secret in Cloud Secret Manager. use `npx -y firebase-tools@latest apphosting:secrets:set` to create these. - `availability`: Where the variable is needed. - `BUILD`: Available during the `npm run build` process. - `RUNTIME`: Available when the app is serving requests. diff --git a/skills/firebase-app-hosting-basics/references/emulation.md b/skills/firebase-app-hosting-basics/references/emulation.md index dff3445..299dcde 100644 --- a/skills/firebase-app-hosting-basics/references/emulation.md +++ b/skills/firebase-app-hosting-basics/references/emulation.md @@ -20,13 +20,13 @@ env: To start the App Hosting emulator: ```bash -firebase emulators:start --only apphosting +npx -y firebase-tools@latest emulators:start --only apphosting ``` Or, if you are also using other emulators (Auth, Firestore, etc.): ```bash -firebase emulators:start +npx -y firebase-tools@latest emulators:start ``` ## Capabilities diff --git a/skills/firebase-auth-basics/SKILL.md b/skills/firebase-auth-basics/SKILL.md index 1110503..bac12ad 100644 --- a/skills/firebase-auth-basics/SKILL.md +++ b/skills/firebase-auth-basics/SKILL.md @@ -1,12 +1,12 @@ --- name: firebase-auth-basics description: Guide for setting up and using Firebase Authentication. Use this skill when the user's app requires user sign-in, user management, or secure data access using auth rules. -compatibility: This skill is best used with the Firebase CLI, but does not require it. Install it by running `npm install -g firebase-tools`. +compatibility: This skill is best used with the Firebase CLI, but does not require it. Firebase CLI can be accessed through `npx -y firebase-tools@latest`. --- ## Prerequisites -- **Firebase Project**: Created via `firebase projects:create` (see `firebase-basics`). +- **Firebase Project**: Created via `npx -y firebase-tools@latest projects:create` (see `firebase-basics`). - **Firebase CLI**: Installed and logged in (see `firebase-basics`). ## Core Concepts diff --git a/skills/firebase-auth-basics/references/security_rules.md b/skills/firebase-auth-basics/references/security_rules.md index 4a2463e..5de862a 100644 --- a/skills/firebase-auth-basics/references/security_rules.md +++ b/skills/firebase-auth-basics/references/security_rules.md @@ -1,7 +1,7 @@ # Authentication in Security Rules Firebase Security Rules work with Firebase Authentication to provide rule-based access control. For better advice on writing safe security rules, -enable the `firestore-basics` or `storage-basics` skills. +enable the `firebase-firestore-basics` or `firebase-storage-basics` skills. The `request.auth` variable contains authentication information for the user requesting data. diff --git a/skills/firebase-basics/SKILL.md b/skills/firebase-basics/SKILL.md index 437050c..25909bb 100644 --- a/skills/firebase-basics/SKILL.md +++ b/skills/firebase-basics/SKILL.md @@ -1,111 +1,52 @@ --- name: firebase-basics -description: Guide for setting up and using Firebase. Use this skill when the user is getting started with Firebase - setting up local environment, using Firebase for the first time, or adding Firebase to their app. +description: The definitive, foundational skill for ANY Firebase task. Make sure to ALWAYS use this skill whenever the user mentions or interacts with Firebase, even if they do not explicitly ask for it. This skill covers everything from the bare minimum INITIAL setup (Node.js setup, Firebase CLI installation, first-time login) to ongoing operations (core principles, workflows, building, service setup, executing Firebase CLI commands, troubleshooting, refreshing, or updating an existing environment). --- -## Prerequisites - -### Node.js and npm -To use the Firebase CLI, you need Node.js (version 20+ required) and npm (which comes with Node.js). - -**Recommended: Use a Node Version Manager (nvm)** -This avoids permission issues when installing global packages. - -1. **Install nvm:** - - Mac/Linux: `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash` - - Windows: Download [nvm-windows](https://github.com/coreybutler/nvm-windows/releases) - -2. **Install Node.js:** - ```bash - nvm install 24 - nvm use 24 - ``` - -**Alternative: Official Installer** -Download and install the LTS version from [nodejs.org](https://nodejs.org/). - -**Verify Installation:** -```bash -node --version -npm --version -``` - -## Core Workflow - -### 1. Installation - -Install the Firebase CLI globally via npm: - -```bash -npm install -g firebase-tools -``` - -Verify installation: -```bash -firebase --version -``` - -### 2. Authentication - -Log in to Firebase: - -```bash -firebase login -``` - -- This opens a browser for authentication. -- For environments where localhost is not available (e.g., remote shell), use `firebase login --no-localhost`. - -### 3. Creating a Project - -To create a new Firebase project from the CLI: - -```bash -firebase projects:create -``` - -You will be prompted to: -1. Enter a Project ID (must be unique globally). -2. Enter a display name. - -### 4. Initialization - -Initialize Firebase services in your project directory: - -```bash -mkdir my-project -cd my-project -firebase init -``` - -The CLI will guide you through: -- Selecting features (Firestore, Functions, Hosting, etc.). -- Associating with an existing project or creating a new one. -- Configuring files (firebase.json, .firebaserc). - -## Exploring Commands - -The Firebase CLI documents itself. Instruct the user to use help commands to discover functionality. - -- **Global Help**: List all available commands and categories. - ```bash - firebase --help - ``` - -- **Command Help**: Get detailed usage for a specific command. - ```bash - firebase [command] --help - # Example: - firebase deploy --help - firebase firestore:indexes --help - ``` - -## SDK Setup - -Detailed guides for adding Firebase to your app: - -- **Web**: See [references/web_setup.md](references/web_setup.md) - -## Common Issues - -- **Permission Denied (EACCES)**: If `npm install -g` fails, suggest using a node version manager (nvm) or `sudo` (caution advised). -- **Login Issues**: If the browser doesn't open, try `firebase login --no-localhost`. +# Prerequisites + +Please complete these setup steps before proceeding, and remember your progress to avoid repeating them in future interactions. + +1. **Local Environment Setup:** Verify the environment is properly set up so we can use Firebase tools: + - Run `npx -y firebase-tools@latest --version` to check if the Firebase CLI is installed. + - Verify if the Firebase MCP server is installed using your existing tools. + - If either of these checks fails, please review [references/local-env-setup.md](references/local-env-setup.md) to get the environment ready. + +2. **Authentication:** + Ensure you are logged in to Firebase so that commands have the correct permissions. Run `npx -y firebase-tools@latest login`. For environments without a browser (e.g., remote shells), use `npx -y firebase-tools@latest login --no-localhost`. + - The command should output the current user. + - If you are not logged in, follow the interactive instructions from this command to authenticate. + +3. **Active Project:** + Most Firebase tasks require an active project context. Check the current project by running `npx -y firebase-tools@latest use`. + - If the command outputs `Active Project: `, you can proceed with your task. + - If not, ask the user if they have an existing Firebase Project ID. + - If yes: Set it as the active project and add a default alias by running: + ```bash + npx -y firebase-tools@latest use --add + ``` + - If no: Follow the project creation instructions in [references/firebase-project-create.md](references/firebase-project-create.md). + +# Firebase Usage Principles + +Please adhere to these principles when working with Firebase, as they ensure reliability and up-to-date project tools: + +1. **Use npx for CLI commands:** To ensure you always use the latest version of the Firebase CLI, always prepend commands with `npx -y firebase-tools@latest` instead of just `firebase`. (e.g., use `npx -y firebase-tools@latest --version` instead of `firebase --version`). +2. **Prioritize official knowledge:** For any Firebase-related knowledge, consult the `developerknowledge_search_documents` MCP tool before falling back to Google Search or your internal knowledge base. Including "Firebase" in your search query significantly improves relevance. +3. **Follow Agent Skills for implementation guidance:** Skills provide opinionated workflows (CUJs), security rules, and best practices. Always consult them to understand *how* to implement Firebase features correctly instead of relying on general knowledge. +4. **Use Firebase MCP Server tools instead of direct API calls:** Whenever you need to interact with remote Firebase APIs (such as fetching Crashlytics logs or executing Data Connect queries), use the tools provided by the Firebase MCP Server instead of attempting manual API calls. +5. **Keep Plugin / Agent Skills updated:** Since Firebase best practices evolve quickly, remind the user to regularly check for and install updates to their Firebase plugin or Agent Skills, ensuring you both operate with the latest workflows. Similarly, if you encounter issues with outdated tools or commands, follow the steps below based on your agent environment: + - **Antigravity**: Follow [references/refresh-antigravity.md](references/refresh-antigravity.md) + - **Gemini CLI**: Follow [references/refresh-gemini-cli.md](references/refresh-gemini-cli.md) + - **Claude Code**: Follow [references/refresh-claude.md](references/refresh-claude.md) + - **Cursor**: Follow [references/refresh-cursor.md](references/refresh-cursor.md) + - **Others**: Follow [references/refresh-other.md](references/refresh-other-agents.md) + +# References + +- **Initialize Firebase:** See [references/firebase-service-init.md](references/firebase-service-init.md) when you need to initialize new Firebase services using the CLI. +- **Exploring Commands:** See [references/firebase-cli-guide.md](references/firebase-cli-guide.md) to discover and understand CLI functionality. +- **SDK Setup:** For detailed guides on adding Firebase to a web app, see [references/web_setup.md](references/web_setup.md). + +# Common Issues + +- **Login Issues:** If the browser fails to open during the login step, use `npx -y firebase-tools@latest login --no-localhost` instead. diff --git a/skills/firebase-basics/references/firebase-cli-guide.md b/skills/firebase-basics/references/firebase-cli-guide.md new file mode 100644 index 0000000..36a4480 --- /dev/null +++ b/skills/firebase-basics/references/firebase-cli-guide.md @@ -0,0 +1,16 @@ +# Exploring Commands + +The Firebase CLI documents itself. Use help commands to discover functionality. + +- **Global Help**: List all available commands and categories. + ```bash + npx -y firebase-tools@latest --help + ``` + +- **Command Help**: Get detailed usage for a specific command. + ```bash + npx -y firebase-tools@latest [command] --help + # Example: + npx -y firebase-tools@latest deploy --help + npx -y firebase-tools@latest firestore:indexes --help + ``` diff --git a/skills/firebase-basics/references/firebase-project-create.md b/skills/firebase-basics/references/firebase-project-create.md new file mode 100644 index 0000000..02b0566 --- /dev/null +++ b/skills/firebase-basics/references/firebase-project-create.md @@ -0,0 +1,11 @@ +# Creating a Project + +To create a new Firebase project from the CLI: + +```bash +npx -y firebase-tools@latest projects:create +``` + +You will be prompted to: +1. Enter a **Project ID** (must be 6-30 chars, lowercase, digits, and hyphens; must be unique globally). +2. Enter a **display name**. diff --git a/skills/firebase-basics/references/firebase-service-init.md b/skills/firebase-basics/references/firebase-service-init.md new file mode 100644 index 0000000..13800aa --- /dev/null +++ b/skills/firebase-basics/references/firebase-service-init.md @@ -0,0 +1,18 @@ +# Initialization + +Before initializing, check if you are already in a Firebase project directory by looking for `firebase.json`. + +1. **Project Directory:** + Navigate to the root directory of the codebase. + *(Only if starting a completely new project from scratch without an existing codebase, create a directory first: `mkdir my-project && cd my-project`)* + +2. **Initialize Services:** + Run the initialization command: + ```bash + npx -y firebase-tools@latest init + ``` + +The CLI will guide you through: +- Selecting features (Firestore, Functions, Hosting, etc.). +- Associating with an existing project or creating a new one. +- Configuring files (e.g. `firebase.json`, `.firebaserc`). diff --git a/skills/firebase-basics/references/local-env-setup.md b/skills/firebase-basics/references/local-env-setup.md new file mode 100644 index 0000000..99e6019 --- /dev/null +++ b/skills/firebase-basics/references/local-env-setup.md @@ -0,0 +1,65 @@ +# Firebase Local Environment Setup + +This skill documents the bare minimum setup required for a full Firebase experience for the agent. Before starting to use any Firebase features, you MUST verify that each of the following steps has been completed. + +## 1. Verify Node.js +- **Action**: Run `node --version`. +- **Handling**: Ensure Node.js is installed and the version is `>= 20`. If Node.js is missing or `< v20`, install it based on the operating system: + + **Recommended: Use a Node Version Manager** + This avoids permission issues when installing global packages. + + **For macOS or Linux:** + 1. Guide the user to the [official nvm repository](https://github.com/nvm-sh/nvm#installing-and-updating). + 2. Request the user to manually install `nvm` and reply when finished. **Stop and wait** for the user's confirmation. + 3. Make `nvm` available in the current terminal session by sourcing the appropriate profile: + ```bash + # For Bash + source ~/.bash_profile + source ~/.bashrc + + # For Zsh + source ~/.zprofile + source ~/.zshrc + ``` + 4. Install Node.js: + ```bash + nvm install 24 + nvm use 24 + ``` + + **For Windows:** + 1. Guide the user to download and install [nvm-windows](https://github.com/coreybutler/nvm-windows/releases). + 2. Request the user to manually install `nvm-windows` and Node.js, and reply when finished. **Stop and wait** for the user's confirmation. + 3. After the user confirms, verify Node.js is available: + ```bash + node --version + ``` + + **Alternative: Official Installer** + 1. Guide the user to download and install the LTS version from [nodejs.org](https://nodejs.org/en/download). + 2. Request the user to manually install Node.js and reply when finished. **Stop and wait** for the user's confirmation. + +## 2. Verify Firebase CLI +The Firebase CLI is the primary tool for interacting with Firebase services. +- **Action**: Run `npx -y firebase-tools@latest --version`. +- **Handling**: Ensure this command runs successfully and outputs a version number. + +## 3. Verify Firebase Authentication +You must be authenticated to manage Firebase projects. +- **Action**: Run `npx -y firebase-tools@latest login`. +- **Handling**: If the environment is remote or restricted (no browser access), run `npx -y firebase-tools@latest login --no-localhost` instead. + +## 4. Install Agent Skills and MCP Server +To fully manage Firebase, the agent needs specific skills and the Firebase MCP server installed. Identify the agent environment you are currently running in and follow the corresponding setup document strictly. + +**Read the setup document for your current agent:** +- **Gemini CLI**: Review [setup-gemini_cli.md](setup-gemini_cli.md) +- **Antigravity**: Review [setup-antigravity.md](setup-antigravity.md) +- **Claude Code**: Review [setup-claude_code.md](setup-claude_code.md) +- **Cursor**: Review [setup-cursor.md](setup-cursor.md) +- **GitHub Copilot**: Review [setup-github_copilot.md](setup-github_copilot.md) +- **Other Agents** (Windsurf, Cline, etc.): Review [setup-other_agents.md](setup-other_agents.md) + +--- +**CRITICAL AGENT RULE:** Do NOT proceed with any other Firebase tasks until EVERY step above has been successfully verified and completed. diff --git a/skills/firebase-basics/references/refresh-antigravity.md b/skills/firebase-basics/references/refresh-antigravity.md new file mode 100644 index 0000000..6f5ee32 --- /dev/null +++ b/skills/firebase-basics/references/refresh-antigravity.md @@ -0,0 +1,46 @@ +# Refresh Antigravity Local Environment + +Follow these steps to refresh Antigravity's local environment, ensuring that their agent skills and plugins are fully up-to-date. + +Antigravity relies on `npx skills`. Updates should prioritize the global level; if skills are installed at the project level, omit the `--global` flag. + +1. **List Available Skills:** Identify all Firebase skills available in the repository: + ```bash + npx -y skills add firebase/agent-skills --list + ``` + +2. **Check Currently Installed Skills:** Determine which skills are already installed and linked at both project and global levels: + ```bash + # Check project-level skills + npx -y skills list --agent antigravity + + # Check global-level skills + npx -y skills list --agent antigravity --global + ``` + *Note: If a skill shows `Agents: not linked` in the output, it is installed but not currently available to Antigravity.* + +3. **Add Missing or Unlinked Skills:** Compare the results from Step 1 and Step 2. For each missing or unlinked skill, follow the appropriate conditional instruction below: + + - **IF any Firebase skills were found at the PROJECT level in Step 2:** Add the missing skill to the project level: + ```bash + npx -y skills add firebase/agent-skills --agent antigravity --skill "" --yes + ``` + + - **IF any Firebase skills were found at the GLOBAL level in Step 2:** Add the missing skill to the global level: + ```bash + npx -y skills add firebase/agent-skills --agent antigravity --skill "" --global --yes + ``` + + - **IF NO Firebase skills were found in Step 2:** Add each missing skill to the global level: + ```bash + npx -y skills add firebase/agent-skills --agent antigravity --skill "" --global --yes + ``` + +4. **Update Existing Skills:** Update all currently installed skills to their latest versions: + ```bash + # Update project-level skills + npx -y skills update --agent antigravity --yes + + # Update global-level skills + npx -y skills update --agent antigravity --global --yes + ``` diff --git a/skills/firebase-basics/references/refresh-claude.md b/skills/firebase-basics/references/refresh-claude.md new file mode 100644 index 0000000..772314e --- /dev/null +++ b/skills/firebase-basics/references/refresh-claude.md @@ -0,0 +1,10 @@ +# Refresh Claude Code Local Environment + +Follow these steps to refresh Claude Code's local environment, ensuring that their agent skills and plugins are fully up-to-date. + +Use Claude Code's native plugin manager instead of `npx`. + +1. **Update the Plugin:** Run the specific CLI command to update the Firebase plugin: + ```bash + claude plugin update firebase@firebase + ``` \ No newline at end of file diff --git a/skills/firebase-basics/references/refresh-gemini-cli.md b/skills/firebase-basics/references/refresh-gemini-cli.md new file mode 100644 index 0000000..6cc1467 --- /dev/null +++ b/skills/firebase-basics/references/refresh-gemini-cli.md @@ -0,0 +1,11 @@ +# Refresh Gemini CLI Local Environment + +Follow these steps to refresh Gemini CLI's local environment, ensuring that their agent skills and plugins are fully up-to-date. + +Use the native Gemini CLI extension manager instead of `npx`. + +1. **Update the Extension:** Run the specific CLI command to update: + ```bash + gemini extensions update firebase + ``` + *Note: If the extension is named differently, replace `firebase` with the correct name from `gemini extensions list`.* diff --git a/skills/firebase-basics/references/refresh-other-agents.md b/skills/firebase-basics/references/refresh-other-agents.md new file mode 100644 index 0000000..f624c96 --- /dev/null +++ b/skills/firebase-basics/references/refresh-other-agents.md @@ -0,0 +1,48 @@ +# Refresh Other Local Environment + +Follow these steps to refresh the local environment of other agents, ensuring that their agent skills and plugins are fully up-to-date. + +Other agents rely on `npx skills`. Updates should prioritize the global level; if skills are installed at the project level, omit the `--global` flag. + +Replace `` with the actual agent name, which can be found in the [skills repository README](https://github.com/vercel-labs/skills/blob/main/README.md). + +1. **List Available Skills:** Identify all Firebase skills available in the repository: + ```bash + npx -y skills add firebase/agent-skills --list + ``` + +2. **Check Currently Installed Skills:** Determine which skills are already installed and linked for the agent at both project and global levels: + ```bash + # Check project-level skills + npx -y skills list --agent + + # Check global-level skills + npx -y skills list --agent --global + ``` + *Note: If a skill shows `Agents: not linked` in the output, it is installed but not currently available to the agent.* + +3. **Add Missing or Unlinked Skills:** Compare the results from Step 1 and Step 2. For each missing or unlinked skill, follow the appropriate conditional instruction below: + + - **IF any Firebase skills were found at the PROJECT level in Step 2:** Add the missing skill to the project level: + ```bash + npx -y skills add firebase/agent-skills --agent --skill "" --yes + ``` + + - **IF any Firebase skills were found at the GLOBAL level in Step 2:** Add the missing skill to the global level: + ```bash + npx -y skills add firebase/agent-skills --agent --skill "" --global --yes + ``` + + - **IF NO Firebase skills were found in Step 2:** Add each missing skill to the global level: + ```bash + npx -y skills add firebase/agent-skills --agent --skill "" --global --yes + ``` + +4. **Update Existing Skills:** Update all currently installed skills to their latest versions: + ```bash + # Update project-level skills + npx -y skills update --agent --yes + + # Update global-level skills + npx -y skills update --agent --global --yes + ``` diff --git a/skills/firebase-basics/references/setup-antigravity.md b/skills/firebase-basics/references/setup-antigravity.md new file mode 100644 index 0000000..7817673 --- /dev/null +++ b/skills/firebase-basics/references/setup-antigravity.md @@ -0,0 +1,63 @@ +# Antigravity Setup + +To get the most out of Firebase in Antigravity, follow these steps to install the agent skills and the MCP server. + +### 1. Install and Verify Firebase Skills +Check if the skills are already installed before proceeding: + +1. **Check Local skills**: Run `ls -d .agent/skills/firebase-basics` or `ls -d .agents/skills/firebase-basics`. If the directory exists, the skills are already installed locally. +2. **Check Global skills**: If not found locally, check the global installation by running: + ```bash + npx skills list --global --agent antigravity + ``` + If the output includes `firebase-basics`, the skills are already installed globally. +3. **Install Skills**: If both checks fail, run the following command to install the Firebase agent skills: + ```bash + npx skills add firebase/agent-skills --agent antigravity --skill "*" + ``` + *Note: Omit `--yes` and `--global` to choose the installation location manually. If prompted interactively in the terminal, ensure you send the appropriate user choices via standard input to complete the installation.* +4. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that `firebase-basics` is now available. + +### 2. Configure and Verify Firebase MCP Server +The MCP server allows Antigravity to interact directly with Firebase projects. + +1. **Locate `mcp_config.json`**: Find the configuration file for your operating system: + - macOS / Linux: `~/.gemini/antigravity/mcp_config.json` + - Windows: `%USERPROFILE%\\.gemini\\antigravity\\mcp_config.json` + + *Note: If the `.gemini/antigravity/` directory or `mcp_config.json` file does not exist, create them and initialize the file with `{ "mcpServers": {} }` before proceeding.* +2. **Check Existing Configuration**: Open `mcp_config.json` and check the `mcpServers` section for a `firebase` entry. + - It is already configured if the `command` is `"firebase"` OR if the `command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`. + - **Important**: If a valid `firebase` entry is found, the MCP server is already configured. **Skip step 3** and proceed directly to step 4. + + **Example valid configurations**: + ```json + "firebase": { + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp"] + } + ``` + OR + ```json + "firebase": { + "command": "firebase", + "args": ["mcp"] + } + ``` +3. **Add or Update Configuration**: If the `firebase` block is missing or incorrect, add it to the `mcpServers` object: + ```json + "firebase": { + "command": "npx", + "args": [ + "-y", + "firebase-tools@latest", + "mcp" + ] + } + ``` + *CRITICAL: Merge this configuration into the existing `mcp_config.json` file. You MUST preserve any other existing servers inside the `mcpServers` object.* +4. **Verify Configuration**: Save the file and confirm the `firebase` block is present and properly formatted JSON. + +### 3. Restart and Verify Connection +1. **Restart Antigravity**: Instruct the user to restart the Antigravity application. **Stop and wait** for their confirmation before proceeding. +2. **Confirm Connection**: Check the MCP server list in the Antigravity UI to confirm that the Firebase MCP server is connected. diff --git a/skills/firebase-basics/references/setup-claude_code.md b/skills/firebase-basics/references/setup-claude_code.md new file mode 100644 index 0000000..fe8eba6 --- /dev/null +++ b/skills/firebase-basics/references/setup-claude_code.md @@ -0,0 +1,30 @@ +# Claude Code Setup + +To get the most out of Firebase in Claude Code, follow these steps to install the agent skills and the MCP server. + +## Recommended Method: Using Plugins + +The recommended method is using the plugin marketplace to install both the agent skills and the MCP functionality. + +### 1. Install and Verify Plugins + +Check if the plugins are already installed before proceeding: + +1. **Check Existing Skills**: Run `npx skills list --agent claude-code` to check for local skills. Run `npx skills list --global --agent claude-code` to check for global skills. Note whether the output includes `firebase-basics`. +2. **Check Existing MCP Configuration**: Run `claude mcp list -s user` and `claude mcp list -s project`. Note whether the output of either command includes `firebase`. +3. **Determine Installation Path**: + - If **both** skills and MCP configuration are found, the plugin is fully installed. **Stop here and skip all remaining setup steps in this document.** + - If **neither** are found, proceed to step 4. + - If **only one** is found (e.g., skills are installed but MCP is missing, or vice versa), **stop and prompt the user**. Explain the mixed state and ask if they want to proceed with installing the Firebase plugin before continuing to step 4. +4. **Add Marketplace**: Run the following command to add the marketplace (this uses the default User scope): + ```bash + claude plugin marketplace add firebase/agent-skills + ``` +5. **Install Plugins**: Run the following command to install the plugin: + ```bash + claude plugin install firebase@firebase + ``` +6. **Verify Installation**: Re-run the checks in steps 1 and 2 to confirm the skills and the MCP server are now available. + +### 2. Restart and Verify Connection +1. **Restart Claude Code**: Instruct the user to restart Claude Code. **Stop and wait** for their confirmation before proceeding. diff --git a/skills/firebase-basics/references/setup-cursor.md b/skills/firebase-basics/references/setup-cursor.md new file mode 100644 index 0000000..c74360e --- /dev/null +++ b/skills/firebase-basics/references/setup-cursor.md @@ -0,0 +1,63 @@ +# Cursor Setup + +To get the most out of Firebase in Cursor, follow these steps to install the agent skills and the MCP server. + +### 1. Install and Verify Firebase Skills +Check if the skills are already installed before proceeding: + +1. **Check Local skills**: Run `npx skills list --agent cursor`. If the output includes `firebase-basics`, the skills are already installed locally. +2. **Check Global skills**: If not found locally, check the global installation by running: + ```bash + npx skills list --global --agent cursor + ``` + If the output includes `firebase-basics`, the skills are already installed globally. +3. **Install Skills**: If both checks fail, run the following command to install the Firebase agent skills: + ```bash + npx skills add firebase/agent-skills --agent cursor --skill "*" + ``` + *Note: Omit `--yes` and `--global` to choose the installation location manually. If prompted interactively in the terminal, ensure you send the appropriate user choices via standard input to complete the installation.* +4. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that `firebase-basics` is now available. + +### 2. Configure and Verify Firebase MCP Server +The MCP server allows Cursor to interact directly with Firebase projects. + +1. **Locate `mcp.json`**: Find the configuration file for your operating system: + - Global: `~/.cursor/mcp.json` + - Project: `.cursor/mcp.json` + + *Note: If the directory or `mcp.json` file does not exist, create them and initialize the file with `{ "mcpServers": {} }` before proceeding.* +2. **Check Existing Configuration**: Open `mcp.json` and check the `mcpServers` section for a `firebase` entry. + - It is already configured if the `command` is `"firebase"` OR if the `command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`. + - **Important**: If a valid `firebase` entry is found, the MCP server is already configured. **Skip step 3** and proceed directly to step 4. + + **Example valid configurations**: + ```json + "firebase": { + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp"] + } + ``` + OR + ```json + "firebase": { + "command": "firebase", + "args": ["mcp"] + } + ``` +3. **Add or Update Configuration**: If the `firebase` block is missing or incorrect, add it to the `mcpServers` object: + ```json + "firebase": { + "command": "npx", + "args": [ + "-y", + "firebase-tools@latest", + "mcp" + ] + } + ``` + *CRITICAL: Merge this configuration into the existing `mcp.json` file. You MUST preserve any other existing servers inside the `mcpServers` object.* +4. **Verify Configuration**: Save the file and confirm the `firebase` block is present and properly formatted JSON. + +### 3. Restart and Verify Connection +1. **Restart Cursor**: Instruct the user to restart the Cursor application. **Stop and wait** for their confirmation before proceeding. +2. **Confirm Connection**: Check the MCP server list in the Cursor UI to confirm that the Firebase MCP server is connected. diff --git a/skills/firebase-basics/references/setup-gemini_cli.md b/skills/firebase-basics/references/setup-gemini_cli.md new file mode 100644 index 0000000..ebadeaa --- /dev/null +++ b/skills/firebase-basics/references/setup-gemini_cli.md @@ -0,0 +1,39 @@ +# Gemini CLI Setup + +To get the most out of Firebase in the Gemini CLI, follow these steps to install the agent extension and the MCP server. + +## Recommended: Installing Extensions + +The best way to get both the agent skills and the MCP server is via the Gemini extension. + +### 1. Install and Verify Firebase Extension +Check if the extension is already installed before proceeding: + +1. **Check Existing Extensions**: Run `gemini extensions list`. If the output includes `firebase`, the extension is already installed. +2. **Install Extension**: If not found, run the following command to install the Firebase agent skills and MCP server: + ```bash + gemini extensions install https://github.com/firebase/agent-skills + ``` +3. **Verify Installation**: Run the following checks to confirm installation: + - `gemini mcp list` -> Output should include `firebase-tools`. + - `gemini skills list` -> Output should include `firebase-basic`. + +### 2. Restart and Verify Connection +1. **Restart Gemini CLI**: Instruct the user to restart the Gemini CLI if any new installation occurred. **Stop and wait** for their confirmation before proceeding. + +--- + +## Alternative: Manual MCP Configuration (Project Scope) + +If the user only wants to use the MCP server for the current project: + +### 1. Configure and Verify Firebase MCP Server +1. **Check Existing Configuration**: Run `gemini mcp list`. If the output includes `firebase-tools`, the MCP server is already configured. +2. **Add the MCP Server**: If not found, run the following command to configure the Firebase MCP Server: + ```bash + gemini mcp add -e IS_GEMINI_CLI_EXTENSION=true firebase npx -y firebase-tools@latest mcp + ``` +3. **Verify Configuration**: Re-run `gemini mcp list` to confirm `firebase-tools` is connected. + +### 2. Restart and Verify Connection +1. **Restart Gemini CLI**: Instruct the user to restart the Gemini CLI. **Stop and wait** for their confirmation before proceeding. diff --git a/skills/firebase-basics/references/setup-github_copilot.md b/skills/firebase-basics/references/setup-github_copilot.md new file mode 100644 index 0000000..1704cb5 --- /dev/null +++ b/skills/firebase-basics/references/setup-github_copilot.md @@ -0,0 +1,70 @@ +# GitHub Copilot Setup + +To get the most out of Firebase with GitHub Copilot in VS Code, follow these steps to install the agent skills and the MCP server. + +## Recommended: Global Setup + +The agent skills and MCP server should be installed globally for consistent access across projects. + +### 1. Install and Verify Firebase Skills +Check if the skills are already installed before proceeding: + +1. **Check Local skills**: Run `npx skills list --agent github-copilot`. If the output includes `firebase-basics`, the skills are already installed locally. +2. **Check Global skills**: If not found locally, check the global installation by running: + ```bash + npx skills list --global --agent github-copilot + ``` + If the output includes `firebase-basics`, the skills are already installed globally. +3. **Install Skills**: If both checks fail, run the following command to install the Firebase agent skills: + ```bash + npx skills add firebase/agent-skills --agent github-copilot --skill "*" + ``` + *Note: Omit `--yes` and `--global` to choose the installation location manually. If prompted interactively in the terminal, ensure you send the appropriate user choices via standard input to complete the installation.* +4. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that `firebase-basics` is now available. + +### 2. Configure and Verify Firebase MCP Server +The MCP server allows GitHub Copilot to interact directly with Firebase projects. + +1. **Locate `mcp.json`**: Find the configuration file for your environment: + - Workspace: `.vscode/mcp.json` + - Global: User Settings `mcp.json` file. + + *Note: If the `.vscode/` directory or `mcp.json` file does not exist, create them and initialize the file with `{ "mcp": { "servers": {} } }` before proceeding.* +2. **Check Existing Configuration**: Open the `mcp.json` file and check the `mcp.servers` object for a `firebase` entry. + - It is already configured if the `command` is `"firebase"` OR if the `command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`. + - **Important**: If a valid `firebase` entry is found, the MCP server is already configured. **Skip step 3** and proceed directly to step 4. + + **Example valid configurations**: + ```json + "firebase": { + "type": "stdio", + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp"] + } + ``` + OR + ```json + "firebase": { + "type": "stdio", + "command": "firebase", + "args": ["mcp"] + } + ``` +3. **Add or Update Configuration**: If the `firebase` block is missing or incorrect, add it to the `mcp.servers` object: + ```json + "firebase": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "firebase-tools@latest", + "mcp" + ] + } + ``` + *CRITICAL: Merge this configuration into the existing `mcp.json` file under the `mcp.servers` object. You MUST preserve any other existing servers inside `mcp.servers`.* +4. **Verify Configuration**: Save the file and confirm the `firebase` block is present and properly formatted JSON. + +### 3. Restart and Verify Connection +1. **Restart VS Code**: Instruct the user to restart VS Code. **Stop and wait** for their confirmation before proceeding. +2. **Confirm Connection**: Check the MCP server list in the VS Code Copilot UI to confirm that the Firebase MCP server is connected. diff --git a/skills/firebase-basics/references/setup-other_agents.md b/skills/firebase-basics/references/setup-other_agents.md new file mode 100644 index 0000000..d45a608 --- /dev/null +++ b/skills/firebase-basics/references/setup-other_agents.md @@ -0,0 +1,65 @@ +# Other Agents Setup + +If you use another agent (like Windsurf, Cline, or Claude Desktop), follow these steps to install the agent skills and the MCP server. + +## Recommended: Global Setup + +The agent skills and MCP server should be installed globally for consistent access across projects. + +### 1. Install and Verify Firebase Skills +Check if the skills are already installed before proceeding: + +1. **Check Local skills**: Run `npx skills list --agent `. If the output includes `firebase-basics`, the skills are already installed locally. Replace `` with the actual agent name, which can be found [here](https://github.com/vercel-labs/skills/blob/main/README.md). +2. **Check Global skills**: If not found locally, check the global installation by running: + ```bash + npx skills list --global --agent + ``` + If the output includes `firebase-basics`, the skills are already installed globally. +3. **Install Skills**: If both checks fail, run the following command to install the Firebase agent skills: + ```bash + npx skills add firebase/agent-skills --agent --skill "*" + ``` + *Note: Omit `--yes` and `--global` to choose the installation location manually. If prompted interactively in the terminal, ensure you send the appropriate user choices via standard input to complete the installation.* +4. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that `firebase-basics` is now available. + +### 2. Configure and Verify Firebase MCP Server +The MCP server allows the agent to interact directly with Firebase projects. + +1. **Locate MCP Configuration**: Find the configuration file for your agent (e.g., `~/.codeium/windsurf/mcp_config.json`, `cline_mcp_settings.json`, or `claude_desktop_config.json`). + + *Note: If the document or its containing directory does not exist, create them and initialize the file with `{ "mcpServers": {} }` before proceeding.* +2. **Check Existing Configuration**: Open the configuration file and check the `mcpServers` section for a `firebase` entry. + - It is already configured if the `command` is `"firebase"` OR if the `command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`. + - **Important**: If a valid `firebase` entry is found, the MCP server is already configured. **Skip step 3** and proceed directly to step 4. + + **Example valid configurations**: + ```json + "firebase": { + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp"] + } + ``` + OR + ```json + "firebase": { + "command": "firebase", + "args": ["mcp"] + } + ``` +3. **Add or Update Configuration**: If the `firebase` block is missing or incorrect, add it to the `mcpServers` object: + ```json + "firebase": { + "command": "npx", + "args": [ + "-y", + "firebase-tools@latest", + "mcp" + ] + } + ``` + *CRITICAL: Merge this configuration into the existing file. You MUST preserve any other existing servers inside the `mcpServers` object.* +4. **Verify Configuration**: Save the file and confirm the `firebase` block is present and properly formatted JSON. + +### 3. Restart and Verify Connection +1. **Restart Agent**: Instruct the user to restart the agent application. **Stop and wait** for their confirmation before proceeding. +2. **Confirm Connection**: Check the MCP server list in the agent's UI to confirm that the Firebase MCP server is connected. diff --git a/skills/firebase-basics/references/web_setup.md b/skills/firebase-basics/references/web_setup.md index 2c453c4..b696118 100644 --- a/skills/firebase-basics/references/web_setup.md +++ b/skills/firebase-basics/references/web_setup.md @@ -4,12 +4,12 @@ If you haven't already created a project: ```bash -firebase projects:create +npx -y firebase-tools@latest projects:create ``` Register your web app: ```bash -firebase apps:create web my-web-app +npx -y firebase-tools@latest apps:create web my-web-app ``` (Note the **App ID** returned by this command). @@ -24,7 +24,7 @@ npm install firebase Create a `firebase.js` (or `firebase.ts`) file. You can fetch your config object using the CLI: ```bash -firebase apps:sdkconfig +npx -y firebase-tools@latest apps:sdkconfig ``` Copy the output config object into your initialization file: diff --git a/skills/firebase-data-connect-basics/SKILL.md b/skills/firebase-data-connect-basics/SKILL.md index ef4e6e9..27fa78b 100644 --- a/skills/firebase-data-connect-basics/SKILL.md +++ b/skills/firebase-data-connect-basics/SKILL.md @@ -78,6 +78,7 @@ Common commands (run from project root): ```bash # Initialize Data Connect +<<<<<<< HEAD firebase init dataconnect # Start local emulator @@ -88,6 +89,18 @@ firebase dataconnect:sdk:generate # Deploy to production firebase deploy --only dataconnect +======= +npx -y firebase-tools@latest init dataconnect + +# Start local emulator +npx -y firebase-tools@latest emulators:start --only dataconnect + +# Generate SDK code +npx -y firebase-tools@latest dataconnect:sdk:generate + +# Deploy to production +npx -y firebase-tools@latest deploy --only dataconnect +>>>>>>> origin/main ``` ## Examples diff --git a/skills/firebase-data-connect-basics/reference/advanced.md b/skills/firebase-data-connect-basics/reference/advanced.md index 9b5cb3b..bdffe7c 100644 --- a/skills/firebase-data-connect-basics/reference/advanced.md +++ b/skills/firebase-data-connect-basics/reference/advanced.md @@ -296,8 +296,8 @@ await dc.upsert("movie", movies[0]); ```bash # Export emulator data -firebase emulators:export ./seed-data +npx -y firebase-tools@latest emulators:export ./seed-data # Start with saved data -firebase emulators:start --only dataconnect --import=./seed-data +npx -y firebase-tools@latest emulators:start --only dataconnect --import=./seed-data ``` diff --git a/skills/firebase-data-connect-basics/reference/config.md b/skills/firebase-data-connect-basics/reference/config.md index ac88d33..c7e5c52 100644 --- a/skills/firebase-data-connect-basics/reference/config.md +++ b/skills/firebase-data-connect-basics/reference/config.md @@ -103,50 +103,50 @@ generate: ```bash # Interactive setup -firebase init dataconnect +npx -y firebase-tools@latest init dataconnect # Set project -firebase use +npx -y firebase-tools@latest use ``` ### Local Development ```bash # Start emulator -firebase emulators:start --only dataconnect +npx -y firebase-tools@latest emulators:start --only dataconnect # Start with database seed data -firebase emulators:start --only dataconnect --import=./seed-data +npx -y firebase-tools@latest emulators:start --only dataconnect --import=./seed-data # Generate SDKs -firebase dataconnect:sdk:generate +npx -y firebase-tools@latest dataconnect:sdk:generate # Watch for schema changes (auto-regenerate) -firebase dataconnect:sdk:generate --watch +npx -y firebase-tools@latest dataconnect:sdk:generate --watch ``` ### Schema Management ```bash # Compare local schema to production -firebase dataconnect:sql:diff +npx -y firebase-tools@latest dataconnect:sql:diff # Apply migration -firebase dataconnect:sql:migrate +npx -y firebase-tools@latest dataconnect:sql:migrate ``` ### Deployment ```bash # Deploy Data Connect service -firebase deploy --only dataconnect +npx -y firebase-tools@latest deploy --only dataconnect # Deploy specific connector -firebase deploy --only dataconnect:connector-id +npx -y firebase-tools@latest deploy --only dataconnect:connector-id # Deploy with schema migration -firebase deploy --only dataconnect --force +npx -y firebase-tools@latest deploy --only dataconnect --force ``` --- @@ -156,7 +156,7 @@ firebase deploy --only dataconnect --force ### Start Emulator ```bash -firebase emulators:start --only dataconnect +npx -y firebase-tools@latest emulators:start --only dataconnect ``` Default ports: @@ -197,10 +197,10 @@ Create seed data files and import: ```bash # Export current emulator data -firebase emulators:export ./seed-data +npx -y firebase-tools@latest emulators:export ./seed-data # Start with seed data -firebase emulators:start --only dataconnect --import=./seed-data +npx -y firebase-tools@latest emulators:start --only dataconnect --import=./seed-data ``` --- @@ -210,9 +210,9 @@ firebase emulators:start --only dataconnect --import=./seed-data ### Deploy Workflow 1. **Test locally** with emulator -2. **Generate SQL diff**: `firebase dataconnect:sql:diff` +2. **Generate SQL diff**: `npx -y firebase-tools@latest dataconnect:sql:diff` 3. **Review migration**: Check breaking changes -4. **Deploy**: `firebase deploy --only dataconnect` +4. **Deploy**: `npx -y firebase-tools@latest deploy --only dataconnect` ### Schema Migrations @@ -220,13 +220,13 @@ Data Connect auto-generates PostgreSQL migrations: ```bash # Preview migration -firebase dataconnect:sql:diff +npx -y firebase-tools@latest dataconnect:sql:diff # Apply migration (interactive) -firebase dataconnect:sql:migrate +npx -y firebase-tools@latest dataconnect:sql:migrate # Force migration (non-interactive) -firebase dataconnect:sql:migrate --force +npx -y firebase-tools@latest dataconnect:sql:migrate --force ``` ### Breaking Changes @@ -244,7 +244,7 @@ Use `--force` flag to acknowledge breaking changes during deploy. # GitHub Actions example - name: Deploy Data Connect run: | - firebase deploy --only dataconnect --token ${{ secrets.FIREBASE_TOKEN }} --force + npx -y firebase-tools@latest deploy --only dataconnect --token ${{ secrets.FIREBASE_TOKEN }} --force ``` --- diff --git a/skills/firebase-data-connect-basics/reference/sdks.md b/skills/firebase-data-connect-basics/reference/sdks.md index 5651153..9885056 100644 --- a/skills/firebase-data-connect-basics/reference/sdks.md +++ b/skills/firebase-data-connect-basics/reference/sdks.md @@ -28,7 +28,7 @@ generate: Generate SDKs: ```bash -firebase dataconnect:sdk:generate +npx -y firebase-tools@latest dataconnect:sdk:generate ``` --- @@ -283,5 +283,5 @@ generate: Generate: ```bash -firebase dataconnect:sdk:generate +npx -y firebase-tools@latest dataconnect:sdk:generate ``` diff --git a/skills/firebase-data-connect-basics/templates.md b/skills/firebase-data-connect-basics/templates.md index 10f0538..75d20dc 100644 --- a/skills/firebase-data-connect-basics/templates.md +++ b/skills/firebase-data-connect-basics/templates.md @@ -213,20 +213,20 @@ generate: ```bash # Initialize Data Connect in project -firebase init dataconnect +npx -y firebase-tools@latest init dataconnect # Initialize with specific project -firebase use -firebase init dataconnect +npx -y firebase-tools@latest use +npx -y firebase-tools@latest init dataconnect # Start emulator for development -firebase emulators:start --only dataconnect +npx -y firebase-tools@latest emulators:start --only dataconnect # Generate SDKs -firebase dataconnect:sdk:generate +npx -y firebase-tools@latest dataconnect:sdk:generate # Deploy to production -firebase deploy --only dataconnect +npx -y firebase-tools@latest deploy --only dataconnect ``` --- diff --git a/skills/firebase-firestore-basics/references/security_rules.md b/skills/firebase-firestore-basics/references/security_rules.md deleted file mode 100644 index 3c9f02e..0000000 --- a/skills/firebase-firestore-basics/references/security_rules.md +++ /dev/null @@ -1,299 +0,0 @@ -# Firestore Security Rules Structure - -Security rules determine who has read and write access to your database. - -## Service and Database Declaration - -All Firestore rules begin with the service declaration and a match block for the database (usually default). - -``` -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. -``` -match /{document=**} { - allow read, write: if false; -} -``` - -### Test Mode (Allow All) -**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. This allows any logged in user access to all data. -``` -match /{document=**} { - allow read, write: if request.auth != null; -} -``` - -### User-Specific Data -Allow users to access only their own data. -``` -match /users/{userId} { - allow read, write: if request.auth != null && request.auth.uid == userId; -} -``` - -### Allow only verified emails -Requires users to verify ownership of the email address before using it to read or write data -``` -// 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. - -## Deploying - -```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. diff --git a/skills/firebase-firestore-enterprise-native-mode/SKILL.md b/skills/firebase-firestore-enterprise-native-mode/SKILL.md new file mode 100644 index 0000000..1eeb40f --- /dev/null +++ b/skills/firebase-firestore-enterprise-native-mode/SKILL.md @@ -0,0 +1,31 @@ +--- +name: firebase-firestore-enterprise-native-mode +description: Comprehensive guide for Firestore enterprise native including provisioning, data model, security rules, and SDK usage. Use this skill when the user needs help setting up Firestore Enterprise with the Native mode, 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. Firebase CLI can be accessed through `npx -y firebase-tools@latest`. +--- + +# Firestore Enterprise Native Mode + +This skill provides a complete guide for getting started with Firestore Enterprise Native Mode, including provisioning, data model, security rules, and SDK usage. + +## Provisioning + +To set up Firestore Enterprise Native Mode in your Firebase project and local environment, see [provisioning.md](references/provisioning.md). + +## Data Model + +To learn about Firestore data model and how to organize your data, see [data_model.md](references/data_model.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 use Firestore Enterprise Native Mode in your application code, see: +- [Web SDK Usage](references/web_sdk_usage.md) +- [Python SDK Usage](references/python_sdk_usage.md) + +## Indexes + +Indexes help improve query performance and speed up slow queries. For checking index types, query support tables, and best practices, see [indexes.md](references/indexes.md). diff --git a/skills/firebase-firestore-enterprise-native-mode/references/data_model.md b/skills/firebase-firestore-enterprise-native-mode/references/data_model.md new file mode 100644 index 0000000..0fe42c0 --- /dev/null +++ b/skills/firebase-firestore-enterprise-native-mode/references/data_model.md @@ -0,0 +1,54 @@ +# Firestore Data Model Reference + +Firestore is a NoSQL, document-oriented database. Unlike a SQL database, there are no tables or rows. Instead, you store data in **documents**, which are organized into **collections**. + +## Document Data Model + +Data in Firestore is organized into documents, collections, and subcollections. + +### Documents +A **document** is a lightweight record that contains fields, which map to values. Each document is identified by a name. A document can contain complex nested objects in addition to basic data types like strings, numbers, and booleans. Documents are limited to a maximum size of 1 MiB. + +Example document (e.g., in a `users` collection): +```json +{ + "first": "Ada", + "last": "Lovelace", + "born": 1815 +} +``` + +### Collections +Documents live in **collections**, which are containers for your documents. For example, you could have a `users` collection to contain your various users, each represented by a document. +* Collections can only contain documents. They cannot directly contain raw fields with values, and they cannot contain other collections. +* Documents within a collection can contain different fields. +* You don't need to "create" or "delete" collections explicitly. After you create the first document in a collection, the collection exists. If you delete all of the documents in a collection, the collection no longer exists. + +### Subcollections +Documents can contain subcollections natively. A subcollection is a collection associated with a specific document. +For example, a user document in the `users` collection could have a `messages` subcollection containing message documents exclusively for that user. This creates a powerful hierarchical data structure. + +Data path example: `users/user1/messages/message1` + +## Collection Group Support + +A **collection group** consists of all collections with the same ID. By default, queries retrieve results from a single collection in your database. Use a collection group query to retrieve documents from a collection group instead of from a single collection. + +### Use Cases +Collection group queries are useful when you want to query across multiple subcollections that share the same organizational structure. + +For example, imagine an app with a `landmarks` collection where each landmark has a `reviews` subcollection. If you want to find all 5-star reviews across *all* landmarks, it would involve checking many separate `reviews` subcollections. With a collection group, you can perform a single query against the `reviews` collection group. + +### Examples + +**Standard Query** (Single Collection): +Find all 5-star reviews for a specific landmark. +```javascript +db.collection('landmarks/golden_gate_bridge/reviews').where('rating', '==', 5) +``` + +**Collection Group Query**: +Find all 5-star reviews across *all* landmarks. +```javascript +db.collectionGroup('reviews').where('rating', '==', 5) +``` \ No newline at end of file diff --git a/skills/firebase-firestore-enterprise-native-mode/references/indexes.md b/skills/firebase-firestore-enterprise-native-mode/references/indexes.md new file mode 100644 index 0000000..f1031bd --- /dev/null +++ b/skills/firebase-firestore-enterprise-native-mode/references/indexes.md @@ -0,0 +1,111 @@ +# Firestore Indexes Reference + +Indexes helps to improve query performance. Firestore Enterprise edition does not create any indexes by default. By default, Firestore Enterprise performs a full collection scan to find documents that match a query, which can be slow and expensive for large collections. To avoid this, you can create indexes to optimize your queries. + +## Index Structure + +An index consists of the following: + +* a collection ID. +* a list of fields in the given collection. +* an order, either ascending or descending, for each field. + +### Index Ordering + +The order and sort direction of each field uniquely defines the index. For example, the following indexes are two distinct indexes and not interchangeable: + +* Field name `name` (ascending) and `population` (descending) +* Field name `name` (descending) and `population` (ascending) + +### Index Density + +Dense indexes: By default, Firestore indexes store data from all documents in a collection. An index entry will be added for a document regardless of whether the document contains any of the fields specified in the index. Non-existent fields are treated as having a NULL value when generating index entries. + +Sparse indexes: To change this behavior, you can define the index as a sparse index. A sparse index indexes only the documents in the collection that contain a value (including null) for at least one of the indexed fields. A sparse index reduces storage costs and can improve performance. + +### Unique Indexes + +You can use unique index option to enforce unique values for the indexed fields. For indexes on multiple fields, each combination of values must be unique across the index. The database rejects any update and insert operations that attempt to create index entries with duplicate values. + +## Query Support Examples + +| Query Type | Index Required | +| :--- | :--- | +| **Simple Equality**
`where("a", "==", 1)` | Single-Field Index on field `a` | +| **Simple Range/Sort**
`where("a", ">", 1).orderBy("a")` | Single-Field Index on field `a` | +| **Multiple Equality**
`where("a", "==", 1).where("b", "==", 2)` | Single-Field Index on field `a` and `b` | +| **Equality + Range/Sort**
`where("a", "==", 1).where("b", ">", 2)` | **Composite Index** on field `a` and `b` | +| **Multiple Ranges**
`where("a", ">", 1).where("b", ">", 2)` | **Composite Index** on field `a` and `b` | +| **Array Contains + Equality**
`where("tags", "array-contains", "news").where("active", "==", true)` | **Composite Index** on field `tags` and `active` | + +If no indexes is present, Firestore Enterprise will perform a full collection scan to find documents that match a query. + +## Management + +### Config files +Your indexes should be defined in `firestore.indexes.json` (pointed to by `firebase.json`). + +Define a dense index: + +```json +{ + "indexes": [ + { + "collectionGroup": "cities", + "queryScope": "COLLECTION", + "density": "DENSE", + "fields": [ + { "fieldPath": "country", "order": "ASCENDING" }, + { "fieldPath": "population", "order": "DESCENDING" } + ] + } + ], + "fieldOverrides": [] +} +``` + +Define a sparse-any index: + +```json +{ + "indexes": [ + { + "collectionGroup": "cities", + "queryScope": "COLLECTION", + "density": "SPARSE_ANY", + "fields": [ + { "fieldPath": "country", "order": "ASCENDING" }, + { "fieldPath": "population", "order": "DESCENDING" } + ] + } + ], + "fieldOverrides": [] +} +``` + +Define a unique index: + +```json +{ + "indexes": [ + { + "collectionGroup": "cities", + "queryScope": "COLLECTION", + "density": "SPARSE_ANY", + "unique": true, + "fields": [ + { "fieldPath": "country", "order": "ASCENDING" }, + { "fieldPath": "population", "order": "DESCENDING" } + ] + } + ], + "fieldOverrides": [] +} +``` + +### CLI Commands + +Deploy indexes only: +```bash +npx firebase-tools@latest -y deploy --only firestore:indexes +``` \ No newline at end of file diff --git a/skills/firebase-firestore-enterprise-native-mode/references/provisioning.md b/skills/firebase-firestore-enterprise-native-mode/references/provisioning.md new file mode 100644 index 0000000..02a97e3 --- /dev/null +++ b/skills/firebase-firestore-enterprise-native-mode/references/provisioning.md @@ -0,0 +1,101 @@ +# Provisioning Firestore Enterprise Native Mode + +## Manual Initialization + +Initialize the following firebase configuration files manually. Do not use `npx -y firebase-tools@latest init`, as it expects interactive inputs. + +1. **Create a Firestore Enterprise Database**: Create a Firestore Enterprise database using the Firebase CLI. +2. **Create `firebase.json`**: This file contains database configuration for the Firebase CLI. +3. **Create `firestore.rules`**: This file contains your security rules. +4. **Create `firestore.indexes.json`**: This file contains your index definitions. + +### 1. Create a Firestore Enterprise Database + +Use the following command to create a Firestore Enterprise database: + +```bash +firebase firestore:databases:create my-database-id \ + --location="nam5" \ + --edition="enterprise" \ + --firestore-data-access="ENABLED" \ + --mongodb-compatible-data-access="DISABLED" +``` + +This will create an enterprise database in `nam5` with native mode enabled. A database id is required to create an enterprise database and the database id must not be `(default)`. To enable realtime-updates feature, use `--realtime-updates` flag. + +```bash +firebase firestore:databases:create my-database-id \ + --location="nam5" \ + --edition="enterprise" \ + --firestore-data-access="ENABLED" \ + --mongodb-compatible-data-access="DISABLED" \ + --realtime-updates="ENABLED" +``` + +### 2. Create `firebase.json` + +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 +{ + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json", + "edition": "enterprise", + "database": "my-database-id", + "location": "nam5" + } +} +``` + +### 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 +```bash +# 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: + +```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). \ No newline at end of file diff --git a/skills/firebase-firestore-enterprise-native-mode/references/python_sdk_usage.md b/skills/firebase-firestore-enterprise-native-mode/references/python_sdk_usage.md new file mode 100644 index 0000000..8bd67a4 --- /dev/null +++ b/skills/firebase-firestore-enterprise-native-mode/references/python_sdk_usage.md @@ -0,0 +1,126 @@ +# Python SDK Usage + +The Python Server SDK is used for backend/server environments and utilizes Google Application Default Credentials in most Google Cloud environments. + +### Writing Data + +#### Set a Document +Creates a document if it does not exist or overwrites it if it does. You can also specify a merge option to only update provided fields. + +```python +city_ref = db.collection("cities").document("LA") + +# Create/Overwrite +city_ref.set({ + "name": "Los Angeles", + "state": "CA", + "country": "USA" +}) + +# Merge +city_ref.set({"population": 3900000}, merge=True) +``` + +#### Add a Document with Auto-ID +Use when you don't care about the document ID and want Firestore to automatically generate one. + +```python +update_time, city_ref = db.collection("cities").add({ + "name": "Tokyo", + "country": "Japan" +}) +print("Document written with ID: ", city_ref.id) +``` + +#### Update a Document +Update some fields of an existing document without overwriting the entire document. Fails if the document doesn't exist. + +```python +city_ref = db.collection("cities").document("LA") +city_ref.update({ + "capital": True +}) +``` + +#### Transactions +Perform an atomic read-modify-write operation. + +```python +from google.cloud.firestore import Transaction + +transaction = db.transaction() +city_ref = db.collection("cities").document("SF") + +@firestore.transactional +def update_in_transaction(transaction, city_ref): + snapshot = city_ref.get(transaction=transaction) + if not snapshot.exists: + raise Exception("Document does not exist!") + + new_population = snapshot.get("population") + 1 + transaction.update(city_ref, {"population": new_population}) + +update_in_transaction(transaction, city_ref) +``` + +### Reading Data + +#### Get a Single Document + +```python +doc_ref = db.collection("cities").document("SF") +doc = doc_ref.get() + +if doc.exists: + print(f"Document data: {doc.to_dict()}") +else: + print("No such document!") +``` + +#### Get Multiple Documents +Fetches all documents in a query or collection once. + +```python +docs = db.collection("cities").stream() + +for doc in docs: + print(f"{doc.id} => {doc.to_dict()}") +``` + +### Queries + +#### Simple and Compound Queries +Use `.where()` to combine filters safely. Stack `.where()` calls for compound queries. + +```python +from google.cloud.firestore import FieldFilter + +cities_ref = db.collection("cities") + +# Simple equality +query_1 = cities_ref.where(filter=FieldFilter("state", "==", "CA")) + +# Compound (AND) +query_2 = cities_ref.where( + filter=FieldFilter("state", "==", "CA") +).where( + filter=FieldFilter("population", ">", 1000000) +) +``` + +#### Order and Limit +Sort and limit results cleanly. + +```python +query = cities_ref.order_by("name").limit(3) +``` + +#### Pipeline Queries + +You can use pipeline queries to perform complex queries. + +```python +pipeline = client.pipeline().collection("users") +for result in pipeline.execute(): + print(f"{result.id} => {result.data()}") +``` diff --git a/skills/firebase-firestore-enterprise-native-mode/references/security_rules.md b/skills/firebase-firestore-enterprise-native-mode/references/security_rules.md new file mode 100644 index 0000000..fe24fad --- /dev/null +++ b/skills/firebase-firestore-enterprise-native-mode/references/security_rules.md @@ -0,0 +1,414 @@ +## 1. Generate Firestore Rules +You are an expert Firebase Security Rules engineer with deep knowledge of Firestore security best practices. Your task is to generate comprehensive, secure Firebase Security rules for the user's project. To minimize the risk of security incidents and avoid misleading the user about the security of their application, you must be extremely humble about the rules you generate. Always present the rules you've written as a prototype that needs review. + +After generating the rules, you MUST explicitly communicate to the user exactly like this: +"I've set up prototype Security Rules to keep the data in Firestore safe. They are designed to be secure for . However, you should review and verify them before broadly sharing your app. If you'd like, I can help you harden these rules." + +### Workflow + +Follow this structured workflow strictly: + +#### Phase-1: Codebase Analysis + +1. **Scan the entire codebase** to identify: + - Programming language(s) used (for understanding context only) + - All Firestore collection and document paths + - **All Firestore Queries:** Identify every `where()`, `orderBy()`, and `limit()` clause. The security rules **MUST** allow these specific queries. + - Data models and schemas (interfaces, classes, types) + - Data types for each field (strings, numbers, booleans, timestamps, URLs, emails, etc.) + - Required vs. optional fields + - Field constraints (min/max length, format patterns, allowed values) + - CRUD operations (create, read, update, delete) + - Authentication patterns (Firebase Auth, custom tokens, anonymous) + - Access patterns and business logic rules +2. **Document your findings** in a untracked file. Refer to this file when generating the security rules. + +#### Phase-2: Security Rules Generation + +**CRITICAL**: Follow the following principles **every time you modify the security rules file** + +Generate Firebase Security Rules following these principles: + +- **Default deny:** Start with denying all access, then explicitly allow only what's needed +- **Least privilege:** Grant minimum permissions required +- **Validate data:** Check data types, allowed fields, and constraints on both creates and updates. + - **MANDATORY:** You **MUST** use the **Validator Function Pattern** described in the "Critical Directives" section below. This involves defining a specific validation function (e.g., `isValidUser`) and calling it in **BOTH** `create` and `update` rules. + - **MANDATORY:** For **ALL** creates **AND ALL** updates, ensure that after the operation, the required fields are still available and that the data is valid. +- **Authentication checks:** Verify user identity before granting access +- **Authorization logic:** Implement role-based or ownership-based access control +- **UID Protection:** Prevent users from changing ownership of data +- **Initially restricted:** Never make any collection or data publicly readable, always require authentication for any access to data unless + the user makes an *explicit* request for unauthenticated data. + +This means the first firestore.rules file you generate must never have any "allow read: true" statements. + +**Structure Requirements:** + +1. **Document assumed data models at the beginning of the rules file:** + +```javascript +// =============================================================== +// Assumed Data Model +// =============================================================== +// +// This security rules file assumes the following data structures: +// +// Collection: [name] +// Document ID: [pattern] +// Fields: +// - field1: type (required/optional, constraints) - description +// - field2: type (required/optional, constraints) - description +// [List all fields with types, constraints, and whether immutable] +// +// [Repeat for all collections] +// +// =============================================================== +``` + +2. **Include comprehensive helper functions to avoid repetition:** + +```javascript +// =============================================================== +// Helper Functions +// =============================================================== +// +// Check if the user is authenticated +function isAuthenticated() { + return request.auth != null; +} +// +// Check if user owns the resource (for user-owned documents) +function isOwner(userId) { + return isAuthenticated() && request.auth.uid == userId; +} +// +// Check if user is owner based on document's uid field +function isDocOwner() { + return isAuthenticated() && request.auth.uid == resource.data.uid; +} +// +// Verify UID hasn't been tampered with on create +function uidUnchanged() { + return !('uid' in request.resource.data) || + request.resource.data.uid == request.auth.uid; +} +// +// Ensure uid field is not modified on update +function uidNotModified() { + return !('uid' in request.resource.data) || + request.resource.data.uid == resource.data.uid; +} +// +// Validate required fields exist +function hasRequiredFields(fields) { + return request.resource.data.keys().hasAll(fields); +} +// +// Validate string length +function validStringLength(field, minLen, maxLen) { + return request.resource.data[field] is string && + request.resource.data[field].size() >= minLen && + request.resource.data[field].size() <= maxLen; +} +// +// Validate URL format (must start with https:// or http://) +function isValidUrl(url) { + return url is string && + (url.matches("^https://.*") || url.matches("^http://.*")); +} +// +// Validate email format +function isValidEmail(email) { + return email is string && + email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); +} + +// +// Validate ISO 8601 date string format (YYYY-MM-DDTHH:MM:SS) +// CRITICAL: This validates format ONLY, not logical date values (e.g., month 13). +// Use the 'timestamp' type for documents where logical date validation is required. +function isValidDateString(dateStr) { + return dateStr is string && + dateStr.matches("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*Z?$"); +} + +// +// Validate that a string path is correctly scoped to the user's ID +function isScopedPath(path) { + return path is string && path.matches("^users/" + request.auth.uid + "/.*"); +} +// +// Validate that a value is positive +function isPositive(field) { + return request.resource.data[field] is number && request.resource.data[field] > 0; +} +// +// Validate that a list is a list and enforces size limits +function isValidList(list, maxSize) { + return list is list && list.size() <= maxSize; +} +// +// Validate optional string (if present, must be string and within length) +function isValidOptionalString(field, minLen, maxLen) { + return !('field' in request.resource.data) || + (request.resource.data[field] is string && + request.resource.data[field].size() >= minLen && + request.resource.data[field].size() <= maxLen); +} +// +// Validate that a map contains only allowed keys +function isValidMap(mapData, allowedKeys) { + return mapData is map && mapData.keys().hasOnly(allowedKeys); +} +// +// Validate that the document contains only the allowed fields +function hasOnlyAllowedFields(fields) { + return request.resource.data.keys().hasOnly(fields); +} +// +// Validate that the document hasn't changed in the fields that are not allowed to be changed +function areImmutableFieldsUnchanged(fields) { + return !request.resource.data.diff(resource.data).affectedKeys().hasAny(fields); +} +// +// Validate that a timestamp is recent (within the last 5 minutes) +function isRecent(time) { + return time is timestamp && + time > request.time - duration.value(5, 'm') && + time <= request.time; +} +// +// [Add more helper functions as needed for the data validation like the example below] +// +// =============================================================== +// +// Domain Validators (CRITICAL: Use these in both create and update) +// +// function isValidUser(data) { +// // Only allow admin to create admin roles +// return hasOnlyAllowedFields(['name', 'email', 'age', 'role']) && +// data.name is string && data.name.size() > 0 && data.name.size() < 50 && +// data.email is string && isValidEmail(data.email) && +// data.age is number && data.age >= 18 && +// data.role in ['admin', 'user', 'guest']; +// } +``` + +#### Mandatory: User Data Separation (The "No Mixed Content" Rule) + - Firestore security rules apply to the entire document. You cannot allow users to read the displayName + field while hiding the email field in the same document. + - If a collection (e.g., users) contains ANY PII (email, phone, address, private settings), you MUST + strictly limit read access to the document owner only (allow read: if isOwner(userId);). + - If the application requires public profiles (e.g., showing user names/avatars on posts): + - 1. Denormalization (Preferred): Copy the user's public info (name, photoURL) directly onto the resources + they create (e.g., store authorName and authorPhoto inside the posts document). + - 2. Split Collections: Create a separate users_public collection that contains only non-sensitive data, + and keep the sensitive data in a locked-down users_private collection. + - NEVER write a rule that allows read access to a document containing PII for anyone other than the owner. + +#### **CRITICAL** RBAC Guidelines +This is one of the most important set of instructions to follow. Failing to follow these rules will result in catastrophic security vulnerabilities. + +- **NEVER** allow users to create their own privileged roles. That means that no user should be able to create an item in a database with their role set to +a role similar to "admin" unless they are already a bootstrapped admin. +- **NEVER** allow users to update their own roles or permissions. +- **NEVER** allow users to grant themselves access to other users' data. +- **NEVER** allow users to bypass the role hierarchy. +- **ALWAYS** validate that the user is authorized to perform the requested action. +- **ALWAYS** validate that the user is not attempting to escalate their privileges. +- **ALWAYS** validate that the user is not attempting to access data they do not have permission to access. + +Here's a **bad** example of what **NOT** to do: + +```javascript +match /users/{userId} { + // BAD: Allows users to create their own roles because a user can create a new user document with a role of 'admin' and the isAdmin() function will return true + allow create: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin(); + // BAD: Allows users to update their own roles because a user can update their own user document with a role of 'admin' and the isAdmin() function will return true + allow update: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin(); +} +``` + +Here's a **good** example of what **TO** do: + +```javascript +match /users/{userId} { + // GOOD: Does NOT allow users to create their own roles unless they are an admin or the user is updating their own role to a less privileged role + allow create: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == 'client') || isAdmin()); + // GOOD: Does NOT allow users to update their own roles unless they are an admin + allow update: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == resource.data.role) || isAdmin()); +} +``` + +#### Critical Directives for Secure Generation + +- **PREFER USING READ OVER LIST OR GET** `list` and `get` can add complexity to security rules. Prefer using `read` over them. +- **Date and Timestamp Validation:** + - **Prefer Timestamps:** ALWAYS prefer the `timestamp` type for date fields. Firestore automatically ensures they are logically valid dates. + - **String Date Risks:** If using strings for dates (e.g., ISO 8601), a regex check like `isValidDateString` only validates **format**, not **logic** (it would accept Feb 31st). + - **Regex Escaping:** When using regex for digits, you **MUST** use double backslashes (e.g., `\\\\d`) in the rules string. Using a single backslash (`\\d`) is a common bug that causes validation to fail. +- **Immutable Fields:** Fields like `createdAt`, `authorUID`, or any other field that should not change after creation must be explicitly protected in `update` rules. (e.g., `request.resource.data.createdAt == resource.data.createdAt`). **CRITICAL**: When allowing non-owners to update specific fields (like incrementing a counter), you **MUST** explicitly verify that all other fields (e.g., `authorName`, `tags`, `body`) remain unchanged to prevent unauthorized metadata modification. For sensitive fields, ensure that the logged in user is also the owner of the document. +- **Identity Integrity:** When storing denormalized user identity (e.g. `authorName`, `authorPhoto`), you **MUST** validate this data. + - **Prefer Auth Token:** If possible, check if `request.resource.data.authorName == request.auth.token.name`. + - **Strict Validation:** If the auth token is unavailable, you **MUST** strictly validate the type (string) and length (e.g. < 50 chars) to prevent spoofing with massive or malicious payloads. + - **Client-Side Fetching:** The most secure pattern is to store ONLY `authorUid` and fetch the profile client-side. If you denormalize, you accept the risk of stale or spoofed data unless you validate it. +- **Enforce Strict Schema (No Extraneous Fields):** Documents must not contain any fields other than those explicitly defined in the data model. This prevents users from adding arbitrary data. +- **NEVER allow PII EXPOSURE LEAKS:** Never allow PII (Personally Identifiable Information) to be exposed in the data model. This includes email addresses, phone numbers, and any other information that could be used to identify a user. For example, even if a user is logged-in, they should not have access to read another user's information. +- **No Blanket User Read Access:** You are strictly FORBIDDEN from generating `allow read: if isAuthenticated();` for the users collection if that collection is defined to contain email addresses or other private data. +- **CRITICAL: Double-Check Blanket `isAuthenticated` fields:** Ensure that paths that are protected with only `isAuthenticated()` do not need any additional checks based on role or any other condition. +- **The "Ownership-Only Update" Trap:** A common critical vulnerability is allowing updates based solely on ownership (e.g., `allow update: if isOwner(resource.data.uid);`). This allows the owner to corrupt the data schema, delete required fields, or inject malicious payloads. You **MUST** always combine ownership checks with data validation (e.g., `allow update: if isOwner(...) && isValidEntity(...);`) **AND** validate that self-escalation is not possible. + +- **Deep Array Inspection:** It is insufficient to check if a field `is list`. You **MUST** validate the contents of the array (e.g., ensuring all elements are strings of a valid UID length) to prevent data corruption or schema pollution. For example, a `tags` array must verify that every item is a string AND that each string is within a reasonable length (e.g., < 20 chars). +- **Permission-Field Lockdown:** Fields that control access (e.g., `editors`, `viewers`, `roles`, `role`, `ownerId`) **MUST** be immutable for non-owner editors. In `update` rules, use `fieldUnchanged()` for these fields unless the `request.auth.uid` matches the document's original owner/creator. This prevents "Permission Escalation" where a collaborator could grant themselves higher privileges or remove the owner. + + +### Advanced Validation for Business Logic + + Secure rules must enforce the application's business logic. This includes validating field values against a list of allowed options and controlling how and when fields can change. + + #### 1. Enforce Enum Values + + If a field should only contain specific values (e.g., a status), validate against a list. + + **Example:** + + ```javascript + // A 'task' document's status can only be one of three values + function isValidStatus() { + let validStatuses = ['pending', 'in-progress', 'completed']; + return request.resource.data.status in validStatuses; + } + + allow create: if isValidStatus() && ... + ``` + + #### 2. Validate State Transitions + + For `update` operations, you **MUST** validate that a field is changing from a valid previous state to a valid new state. This prevents users from bypassing workflows (e.g., marking a task as 'completed' from 'archived'). + + **Example:** + + ```javascript + // A task can only be marked 'completed' if it was 'in-progress' + function validStatusTransition() { + let previousStatus = resource.data.status; + let newStatus = request.resource.data.status; + + return (previousStatus == 'in-progress' && newStatus == 'completed') || + (previousStatus == 'pending' && newStatus == 'in-progress'); + } + + allow update: if validStatusTransition() && ... + ``` + +#### 3. Strict Path and Relationship Scoping + +For any field that references another resource (like an image path or a parent document ID), you **MUST** ensure it is correctly scoped to the user or valid within the context. + +**Example:** + +```javascript +// Ensure image path is within the user's own storage folder +allow create: if isScopedPath(request.resource.data.imageBucket) && ... +``` + +#### 4. Secure Counter Updates + +When allowing users to update a counter (like `voteCount` or `answerCount`), you **MUST** ensure: +1. **Atomic Increments:** The field is only changing by exactly +1 or -1. +2. **Isolation:** **NO OTHER FIELDS** are being modified. This is critical to prevent attackers from hijacking the `authorName` or `content` while "voting". +3. **Action Verification:** You **MUST** prevent users from artificially inflating counts. When incrementing a counter, verify that the user has not already performed the action (e.g., by checking for the existence of a 'like' document) and is not looping updates. + * **CRITICAL:** Relying solely on `!exists(likeDoc)` is insufficient because a malicious user can skip creating the document and loop the increment. + * **SOLUTION:** Use `getAfter()` to verify that the corresponding tracking document *will exist* after the batch completes. + +**Example:** + +```javascript +function isValidCounterUpdate(docId) { + // Allow update only if 'voteCount' is the ONLY field changing + return request.resource.data.diff(resource.data).affectedKeys().hasOnly(['voteCount']) && + // And the change is exactly +1 or -1 + math.abs(request.resource.data.voteCount - resource.data.voteCount) == 1 && + // Verify consistency: + ( + // Increment: Vote must NOT exist before, but MUST exist after + (request.resource.data.voteCount > resource.data.voteCount && + !exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) && + getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) != null) || + // Decrement: Vote MUST exist before, but must NOT exist after + (request.resource.data.voteCount < resource.data.voteCount && + exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) && + getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) == null) + ); +} + +allow update: if isValidCounterUpdate(docId) && ... +``` + +#### 5. **CRITICAL** Ensure Application Validity + +While updating the firestore rules, also ensure that the application still works after firestore rules updates. + +3. **For each collection, implement explicit data validation:** + +- Type Checking: 'field is string', 'field is number', 'field is bool', 'field is timestamp' +- Required fields validation using 'hasRequiredFields()' +- **Enforce Size Limits:** For **EVERY** string, list, and map field, you **MUST** enforce realistic size limits (e.g., `text.size() < 1000`, `tags.size() < 20`). **Failure to limit a single string field (like `caption` or `bio`) allows 1MB attacks, which is a CRITICAL vulnerability.** +- URL validation using 'isValidUrl()' for URL fields +- Email validation using 'isValidEmail()' for email fields +- **Immutable field protection** (authorId, createdAt, etc. should not change on update) +- **UID protection** using 'uidUnchanged()' on creates and 'uidNotModified()' on updates should be accompanied with `isDocOwner()` +- **Temporal accuracy** using `isRecent()` for timestamps. +- **Range validation** using `isPositive()` or similar for numbers. +- **Path scoping** using `isScopedPath()` for storage paths. + +Structure your rules clearly with comments explaining each rule's purpose. + +#### Phase-3: Devil's Advocate Attack + +**Critical step:** Systematically attempt to break your own rules using the following attack vectors. You MUST document the outcome of each attempt. + +1. **Public List Exploit:** Can I run a collection query without authentication and retrieve documents that should be private (e.g., where `visible == false`)? +2. **Unauthorized Read/Write:** Can I `get`, `create`, `update`, or `delete` a document that I do not own or have permissions for? +3. **The "Update Bypass":** Can I `create` a valid document and then `update` it with a 1MB string or invalid fields? (Tests if validation logic is missing from `update`). +4. **Ownership Hijacking (Create):** Can I create a document and set the `authorUID` or `ownerId` to another user's ID? +5. **Ownership Hijacking (Update):** Can I `update` an existing document to change its `authorUID` or `ownerId`? +6. **Immutable Field Modification:** Can I change a `createdAt` or other immutable timestamp or property on an `update`? +7. **Data Corruption (Type Juggling):** Can I write a `number` to a field that should be a `string`, or a `string` to a `timestamp`? +8. **Validation Bypass (Create vs. Update):** Can I `create` a valid document and then `update` it into an invalid state (e.g., remove a required field, write a string that's too long)? +9. **Resource Exhaustion / DoS:** Can I write an enormous string (e.g., 1MB) to any field that accepts a string or a massive array to a list field? Every string field (e.g., `bio`, `url`, `name`) MUST have a `.size()` check. If any are missing, it's a "Resource Exhaustion/DoS" risk. +10. **Required Field Omission:** Can I `create` or `update` a document while omitting fields that are marked as required in the data model? +11. **Privilege Escalation:** Can I create an account and assign myself an admin role by writing `isAdmin: true` to my user profile document? (Tests reliance on document data vs. custom claims). +12. **Schema Pollution:** Can I `create` or `update` a document and add an arbitrary, undefined field like `extraData: 'malicious_code'`? (Tests for strict schema enforcement). +13. **Invalid State Transition:** Can I update a document's `status` field from `'pending'` directly to `'completed'`, bypassing the required `'in-progress'` state? (Tests business logic enforcement). +14. **Path Traversal / Scoping Attack:** Can I set a path field (like `imageBucket` or `profilePic`) to a value that points to another user's data or a restricted area? (Tests for regex path scoping). +15. **Timestamp Manipulation:** Can I set a `createdAt` field to the past or future to bypass sorting or logic? (Tests for `request.time` validation). +16. **Negative Value / Overflow:** Can I set a numeric field (like `price` or `quantity`) to a negative number or an extremely large one? (Tests for range validation). +17. **The "Mixed Content" Leak:** Create a second user. Can User B read User A's users document? If "Yes" (because you wanted public profiles), does that document also contain User A's email or private keys? If both are true, the rules are insecure. +18. **Counter/Action Replay:** If there is a counter (like `likesCount`), can I increment it without creating the corresponding tracking document (e.g., inside `likes/{userId}`)? Can I increment it twice? (Tests for `getAfter()` consistency checks). +19. **Orphaned Subcollection Access:** Can I read/write to a subcollection (e.g., `users/123/posts/456`) if the parent document (`users/123`) does not exist? (Tests for parent existence checks). +20. **Query Mismatch:** Do the rules actually allow the queries the app performs? (e.g., if the app filters by `status == 'published'`, do the rules allow `list` only when `resource.data.status == 'published'`?) +21. **Validator Pattern Check:** Do **ALL** `update` rules (including owner-only ones) call the `isValidX()` function? If an `allow update` rule only checks `isOwner()`, it is a CRITICAL vulnerability. + +Document each attack attempt and whether it succeeded. If ANY attack succeeds: + +- Fix the security hole +- Regenerate the rules +- **Repeat Phase-3** until no attacks succeed + +#### Phase-4: Syntactic Validation + +Once devil's advocate testing passes, repeat until rules pass validation. + +**After all phases are complete, create or update the `firestore.rules` file.** + +### Critical Constraints +1. **Never skip the devil's advocate phase** - this is your primary security validation +2. **MUST include helper functions** for common operations ('isAuthenticated', 'isOwner', 'uidUnchanged', 'uidNotModified') AND domain validators ('isValidUser', etc.) +3. **MUST document assumed data models** at the beginning of the rules file +4. **Always validate the rules syntax** using 'firebase deploy --only firestore:rules --dry-run' or a similar tool before outputting the final file. +5. **Provide complete, runnable code** - no placeholders or TODOs +6. **Document all assumptions** about data structure or access patterns +7. **Always run the devil's advocate attack** after any modification of the rules. +8. **Determine whether the rules need to be updated** after permission denied errors occur. +9. **Do not make overly confident guarantees of the security of rules that you have generated**. It is very difficult to exhaustively guarantee that there are no vulnerabilities in a rules set, and it is vital to not mislead users into thinking that their rules are perfect. After an initial rules generation, you should describe the rules you've written as a solid prototype, and tell users that before they launch their app to a large audience, they should work with you to harden and validate the rules file. Be clear that users should carefully review rules to ensure security. diff --git a/skills/firebase-firestore-enterprise-native-mode/references/web_sdk_usage.md b/skills/firebase-firestore-enterprise-native-mode/references/web_sdk_usage.md new file mode 100644 index 0000000..1eee422 --- /dev/null +++ b/skills/firebase-firestore-enterprise-native-mode/references/web_sdk_usage.md @@ -0,0 +1,201 @@ +# Web SDK Usage + +This guide focuses on the **Modular Web SDK** (v9+), which is tree-shakeable and efficient. + +### Initialization + +```javascript +import { initializeApp } from "firebase/app"; +import { getFirestore } from "firebase/firestore"; + +// If running in Firebase App Hosting, you can skip Firebase Config and instead use: +// const app = initializeApp(); + +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 +Creates a document if it doesn't exist, or overwrites it if it does. You can also specify a merge option to only update provided fields. + +```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 +Use when you don't care about the document ID and want Firestore to automatically generate one. + +```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 +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 + +```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 +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) => { + console.log(doc.id, " => ", doc.data()); +}); +``` + +### Realtime Updates + +#### Listen to a Document or Query + +```javascript +import { doc, onSnapshot } from "firebase/firestore"; + +const unsub = onSnapshot(doc(db, "cities", "SF"), (doc) => { + console.log("Current data: ", doc.data()); +}); + +// To stop listening: +// unsub(); +``` + +### Handle Changes + +```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()` and `where()` to combine filters safely. + +```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 a composite index if filtering on different fields +const q2 = query(citiesRef, where("state", "==", "CA"), where("population", ">", 1000000)); +``` + +#### Order and Limit +Sort and limit results cleanly. + +```javascript +import { orderBy, limit } from "firebase/firestore"; + +const q = query(citiesRef, orderBy("name"), limit(3)); +``` + +#### Pipeline Queries + +You can use pipeline queries to perform complex queries. + +```javascript + +const readDataPipeline = db.pipeline() + .collection("users"); + +// Execute the pipeline and handle the result +try { + const querySnapshot = await execute(readDataPipeline); + querySnapshot.results.forEach((result) => { + console.log(`${result.id} => ${result.data()}`); + }); +} catch (error) { + console.error("Error getting documents: ", error); +} +``` diff --git a/skills/firebase-firestore-basics/SKILL.md b/skills/firebase-firestore-standard/SKILL.md similarity index 58% rename from skills/firebase-firestore-basics/SKILL.md rename to skills/firebase-firestore-standard/SKILL.md index a2de73b..0eb6c2e 100644 --- a/skills/firebase-firestore-basics/SKILL.md +++ b/skills/firebase-firestore-standard/SKILL.md @@ -1,12 +1,12 @@ --- -name: firebase-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`. +name: firebase-firestore-standard +description: Comprehensive guide for Firestore Standard Edition, 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. Firebase CLI can be accessed through `npx -y firebase-tools@latest`. --- -# Firestore Basics +# Firestore Standard Edition -This skill provides a complete guide for getting started with Cloud Firestore, including provisioning, securing, and integrating it into your application. +This skill provides a complete guide for getting started with Cloud Firestore Standard Edition, including provisioning, securing, and integrating it into your application. ## Provisioning diff --git a/skills/firebase-firestore-basics/references/indexes.md b/skills/firebase-firestore-standard/references/indexes.md similarity index 94% rename from skills/firebase-firestore-basics/references/indexes.md rename to skills/firebase-firestore-standard/references/indexes.md index 6b0c4a8..7623eb8 100644 --- a/skills/firebase-firestore-basics/references/indexes.md +++ b/skills/firebase-firestore-standard/references/indexes.md @@ -5,7 +5,7 @@ Indexes allow Firestore to ensure that query performance depends on the size of ## Index Types ### Single-Field Indexes -Firestore **automatically creates** a single-field index for every field in a document (and subfields in maps). +In Standard Edition, 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. @@ -78,5 +78,5 @@ Your indexes should be defined in `firestore.indexes.json` (pointed to by `fireb Deploy indexes only: ```bash -firebase deploy --only firestore:indexes +npx -y firebase-tools@latest deploy --only firestore:indexes ``` diff --git a/skills/firebase-firestore-basics/references/provisioning.md b/skills/firebase-firestore-standard/references/provisioning.md similarity index 70% rename from skills/firebase-firestore-basics/references/provisioning.md rename to skills/firebase-firestore-standard/references/provisioning.md index fb5f9f5..5278801 100644 --- a/skills/firebase-firestore-basics/references/provisioning.md +++ b/skills/firebase-firestore-standard/references/provisioning.md @@ -2,7 +2,7 @@ ## Manual Initialization -Initialize the following firebase configuration files manually. Do not use `firebase init`, as it expects interactive inputs. +Initialize the following firebase configuration files manually. Do not use `npx -y firebase-tools@latest 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. @@ -21,7 +21,7 @@ Create a file named `firebase.json` in your project root with the following cont } ``` -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: +This will use the default database with the Standard edition. To use a different database, specify the database ID and location. You can check the list of available databases using `npx -y firebase-tools@latest firestore:databases:list`. If the database does not exist, it will be created when you deploy: ```json { @@ -34,20 +34,6 @@ This will use the default database. To use a different database, specify the dat } ``` - To use Enterprise edition, specify the `enterprise` field. - -```json -{ - "firestore": { - "rules": "firestore.rules", - "indexes": "firestore.indexes.json", - "edition": "enterprise", - "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: @@ -81,13 +67,13 @@ Create a file named `firestore.indexes.json` with an empty configuration to star ## Deploy rules and indexes ```bash # To deploy all rules and indexes -firebase deploy --only firestore +npx -y firebase-tools@latest deploy --only firestore # To deploy just rules -firebase deploy --only firestore:rules +npx -y firebase-tools@latest deploy --only firestore:rules # To deploy just indexes -firebase deploy --only firestore:indexes +npx -y firebase-tools@latest deploy --only firestore:indexes ``` ## Local Emulation @@ -95,7 +81,7 @@ firebase deploy --only firestore:indexes To run Firestore locally for development and testing: ```bash -firebase emulators:start --only firestore +npx -y firebase-tools@latest 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/firebase-firestore-standard/references/security_rules.md b/skills/firebase-firestore-standard/references/security_rules.md new file mode 100644 index 0000000..fe24fad --- /dev/null +++ b/skills/firebase-firestore-standard/references/security_rules.md @@ -0,0 +1,414 @@ +## 1. Generate Firestore Rules +You are an expert Firebase Security Rules engineer with deep knowledge of Firestore security best practices. Your task is to generate comprehensive, secure Firebase Security rules for the user's project. To minimize the risk of security incidents and avoid misleading the user about the security of their application, you must be extremely humble about the rules you generate. Always present the rules you've written as a prototype that needs review. + +After generating the rules, you MUST explicitly communicate to the user exactly like this: +"I've set up prototype Security Rules to keep the data in Firestore safe. They are designed to be secure for . However, you should review and verify them before broadly sharing your app. If you'd like, I can help you harden these rules." + +### Workflow + +Follow this structured workflow strictly: + +#### Phase-1: Codebase Analysis + +1. **Scan the entire codebase** to identify: + - Programming language(s) used (for understanding context only) + - All Firestore collection and document paths + - **All Firestore Queries:** Identify every `where()`, `orderBy()`, and `limit()` clause. The security rules **MUST** allow these specific queries. + - Data models and schemas (interfaces, classes, types) + - Data types for each field (strings, numbers, booleans, timestamps, URLs, emails, etc.) + - Required vs. optional fields + - Field constraints (min/max length, format patterns, allowed values) + - CRUD operations (create, read, update, delete) + - Authentication patterns (Firebase Auth, custom tokens, anonymous) + - Access patterns and business logic rules +2. **Document your findings** in a untracked file. Refer to this file when generating the security rules. + +#### Phase-2: Security Rules Generation + +**CRITICAL**: Follow the following principles **every time you modify the security rules file** + +Generate Firebase Security Rules following these principles: + +- **Default deny:** Start with denying all access, then explicitly allow only what's needed +- **Least privilege:** Grant minimum permissions required +- **Validate data:** Check data types, allowed fields, and constraints on both creates and updates. + - **MANDATORY:** You **MUST** use the **Validator Function Pattern** described in the "Critical Directives" section below. This involves defining a specific validation function (e.g., `isValidUser`) and calling it in **BOTH** `create` and `update` rules. + - **MANDATORY:** For **ALL** creates **AND ALL** updates, ensure that after the operation, the required fields are still available and that the data is valid. +- **Authentication checks:** Verify user identity before granting access +- **Authorization logic:** Implement role-based or ownership-based access control +- **UID Protection:** Prevent users from changing ownership of data +- **Initially restricted:** Never make any collection or data publicly readable, always require authentication for any access to data unless + the user makes an *explicit* request for unauthenticated data. + +This means the first firestore.rules file you generate must never have any "allow read: true" statements. + +**Structure Requirements:** + +1. **Document assumed data models at the beginning of the rules file:** + +```javascript +// =============================================================== +// Assumed Data Model +// =============================================================== +// +// This security rules file assumes the following data structures: +// +// Collection: [name] +// Document ID: [pattern] +// Fields: +// - field1: type (required/optional, constraints) - description +// - field2: type (required/optional, constraints) - description +// [List all fields with types, constraints, and whether immutable] +// +// [Repeat for all collections] +// +// =============================================================== +``` + +2. **Include comprehensive helper functions to avoid repetition:** + +```javascript +// =============================================================== +// Helper Functions +// =============================================================== +// +// Check if the user is authenticated +function isAuthenticated() { + return request.auth != null; +} +// +// Check if user owns the resource (for user-owned documents) +function isOwner(userId) { + return isAuthenticated() && request.auth.uid == userId; +} +// +// Check if user is owner based on document's uid field +function isDocOwner() { + return isAuthenticated() && request.auth.uid == resource.data.uid; +} +// +// Verify UID hasn't been tampered with on create +function uidUnchanged() { + return !('uid' in request.resource.data) || + request.resource.data.uid == request.auth.uid; +} +// +// Ensure uid field is not modified on update +function uidNotModified() { + return !('uid' in request.resource.data) || + request.resource.data.uid == resource.data.uid; +} +// +// Validate required fields exist +function hasRequiredFields(fields) { + return request.resource.data.keys().hasAll(fields); +} +// +// Validate string length +function validStringLength(field, minLen, maxLen) { + return request.resource.data[field] is string && + request.resource.data[field].size() >= minLen && + request.resource.data[field].size() <= maxLen; +} +// +// Validate URL format (must start with https:// or http://) +function isValidUrl(url) { + return url is string && + (url.matches("^https://.*") || url.matches("^http://.*")); +} +// +// Validate email format +function isValidEmail(email) { + return email is string && + email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); +} + +// +// Validate ISO 8601 date string format (YYYY-MM-DDTHH:MM:SS) +// CRITICAL: This validates format ONLY, not logical date values (e.g., month 13). +// Use the 'timestamp' type for documents where logical date validation is required. +function isValidDateString(dateStr) { + return dateStr is string && + dateStr.matches("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*Z?$"); +} + +// +// Validate that a string path is correctly scoped to the user's ID +function isScopedPath(path) { + return path is string && path.matches("^users/" + request.auth.uid + "/.*"); +} +// +// Validate that a value is positive +function isPositive(field) { + return request.resource.data[field] is number && request.resource.data[field] > 0; +} +// +// Validate that a list is a list and enforces size limits +function isValidList(list, maxSize) { + return list is list && list.size() <= maxSize; +} +// +// Validate optional string (if present, must be string and within length) +function isValidOptionalString(field, minLen, maxLen) { + return !('field' in request.resource.data) || + (request.resource.data[field] is string && + request.resource.data[field].size() >= minLen && + request.resource.data[field].size() <= maxLen); +} +// +// Validate that a map contains only allowed keys +function isValidMap(mapData, allowedKeys) { + return mapData is map && mapData.keys().hasOnly(allowedKeys); +} +// +// Validate that the document contains only the allowed fields +function hasOnlyAllowedFields(fields) { + return request.resource.data.keys().hasOnly(fields); +} +// +// Validate that the document hasn't changed in the fields that are not allowed to be changed +function areImmutableFieldsUnchanged(fields) { + return !request.resource.data.diff(resource.data).affectedKeys().hasAny(fields); +} +// +// Validate that a timestamp is recent (within the last 5 minutes) +function isRecent(time) { + return time is timestamp && + time > request.time - duration.value(5, 'm') && + time <= request.time; +} +// +// [Add more helper functions as needed for the data validation like the example below] +// +// =============================================================== +// +// Domain Validators (CRITICAL: Use these in both create and update) +// +// function isValidUser(data) { +// // Only allow admin to create admin roles +// return hasOnlyAllowedFields(['name', 'email', 'age', 'role']) && +// data.name is string && data.name.size() > 0 && data.name.size() < 50 && +// data.email is string && isValidEmail(data.email) && +// data.age is number && data.age >= 18 && +// data.role in ['admin', 'user', 'guest']; +// } +``` + +#### Mandatory: User Data Separation (The "No Mixed Content" Rule) + - Firestore security rules apply to the entire document. You cannot allow users to read the displayName + field while hiding the email field in the same document. + - If a collection (e.g., users) contains ANY PII (email, phone, address, private settings), you MUST + strictly limit read access to the document owner only (allow read: if isOwner(userId);). + - If the application requires public profiles (e.g., showing user names/avatars on posts): + - 1. Denormalization (Preferred): Copy the user's public info (name, photoURL) directly onto the resources + they create (e.g., store authorName and authorPhoto inside the posts document). + - 2. Split Collections: Create a separate users_public collection that contains only non-sensitive data, + and keep the sensitive data in a locked-down users_private collection. + - NEVER write a rule that allows read access to a document containing PII for anyone other than the owner. + +#### **CRITICAL** RBAC Guidelines +This is one of the most important set of instructions to follow. Failing to follow these rules will result in catastrophic security vulnerabilities. + +- **NEVER** allow users to create their own privileged roles. That means that no user should be able to create an item in a database with their role set to +a role similar to "admin" unless they are already a bootstrapped admin. +- **NEVER** allow users to update their own roles or permissions. +- **NEVER** allow users to grant themselves access to other users' data. +- **NEVER** allow users to bypass the role hierarchy. +- **ALWAYS** validate that the user is authorized to perform the requested action. +- **ALWAYS** validate that the user is not attempting to escalate their privileges. +- **ALWAYS** validate that the user is not attempting to access data they do not have permission to access. + +Here's a **bad** example of what **NOT** to do: + +```javascript +match /users/{userId} { + // BAD: Allows users to create their own roles because a user can create a new user document with a role of 'admin' and the isAdmin() function will return true + allow create: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin(); + // BAD: Allows users to update their own roles because a user can update their own user document with a role of 'admin' and the isAdmin() function will return true + allow update: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin(); +} +``` + +Here's a **good** example of what **TO** do: + +```javascript +match /users/{userId} { + // GOOD: Does NOT allow users to create their own roles unless they are an admin or the user is updating their own role to a less privileged role + allow create: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == 'client') || isAdmin()); + // GOOD: Does NOT allow users to update their own roles unless they are an admin + allow update: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == resource.data.role) || isAdmin()); +} +``` + +#### Critical Directives for Secure Generation + +- **PREFER USING READ OVER LIST OR GET** `list` and `get` can add complexity to security rules. Prefer using `read` over them. +- **Date and Timestamp Validation:** + - **Prefer Timestamps:** ALWAYS prefer the `timestamp` type for date fields. Firestore automatically ensures they are logically valid dates. + - **String Date Risks:** If using strings for dates (e.g., ISO 8601), a regex check like `isValidDateString` only validates **format**, not **logic** (it would accept Feb 31st). + - **Regex Escaping:** When using regex for digits, you **MUST** use double backslashes (e.g., `\\\\d`) in the rules string. Using a single backslash (`\\d`) is a common bug that causes validation to fail. +- **Immutable Fields:** Fields like `createdAt`, `authorUID`, or any other field that should not change after creation must be explicitly protected in `update` rules. (e.g., `request.resource.data.createdAt == resource.data.createdAt`). **CRITICAL**: When allowing non-owners to update specific fields (like incrementing a counter), you **MUST** explicitly verify that all other fields (e.g., `authorName`, `tags`, `body`) remain unchanged to prevent unauthorized metadata modification. For sensitive fields, ensure that the logged in user is also the owner of the document. +- **Identity Integrity:** When storing denormalized user identity (e.g. `authorName`, `authorPhoto`), you **MUST** validate this data. + - **Prefer Auth Token:** If possible, check if `request.resource.data.authorName == request.auth.token.name`. + - **Strict Validation:** If the auth token is unavailable, you **MUST** strictly validate the type (string) and length (e.g. < 50 chars) to prevent spoofing with massive or malicious payloads. + - **Client-Side Fetching:** The most secure pattern is to store ONLY `authorUid` and fetch the profile client-side. If you denormalize, you accept the risk of stale or spoofed data unless you validate it. +- **Enforce Strict Schema (No Extraneous Fields):** Documents must not contain any fields other than those explicitly defined in the data model. This prevents users from adding arbitrary data. +- **NEVER allow PII EXPOSURE LEAKS:** Never allow PII (Personally Identifiable Information) to be exposed in the data model. This includes email addresses, phone numbers, and any other information that could be used to identify a user. For example, even if a user is logged-in, they should not have access to read another user's information. +- **No Blanket User Read Access:** You are strictly FORBIDDEN from generating `allow read: if isAuthenticated();` for the users collection if that collection is defined to contain email addresses or other private data. +- **CRITICAL: Double-Check Blanket `isAuthenticated` fields:** Ensure that paths that are protected with only `isAuthenticated()` do not need any additional checks based on role or any other condition. +- **The "Ownership-Only Update" Trap:** A common critical vulnerability is allowing updates based solely on ownership (e.g., `allow update: if isOwner(resource.data.uid);`). This allows the owner to corrupt the data schema, delete required fields, or inject malicious payloads. You **MUST** always combine ownership checks with data validation (e.g., `allow update: if isOwner(...) && isValidEntity(...);`) **AND** validate that self-escalation is not possible. + +- **Deep Array Inspection:** It is insufficient to check if a field `is list`. You **MUST** validate the contents of the array (e.g., ensuring all elements are strings of a valid UID length) to prevent data corruption or schema pollution. For example, a `tags` array must verify that every item is a string AND that each string is within a reasonable length (e.g., < 20 chars). +- **Permission-Field Lockdown:** Fields that control access (e.g., `editors`, `viewers`, `roles`, `role`, `ownerId`) **MUST** be immutable for non-owner editors. In `update` rules, use `fieldUnchanged()` for these fields unless the `request.auth.uid` matches the document's original owner/creator. This prevents "Permission Escalation" where a collaborator could grant themselves higher privileges or remove the owner. + + +### Advanced Validation for Business Logic + + Secure rules must enforce the application's business logic. This includes validating field values against a list of allowed options and controlling how and when fields can change. + + #### 1. Enforce Enum Values + + If a field should only contain specific values (e.g., a status), validate against a list. + + **Example:** + + ```javascript + // A 'task' document's status can only be one of three values + function isValidStatus() { + let validStatuses = ['pending', 'in-progress', 'completed']; + return request.resource.data.status in validStatuses; + } + + allow create: if isValidStatus() && ... + ``` + + #### 2. Validate State Transitions + + For `update` operations, you **MUST** validate that a field is changing from a valid previous state to a valid new state. This prevents users from bypassing workflows (e.g., marking a task as 'completed' from 'archived'). + + **Example:** + + ```javascript + // A task can only be marked 'completed' if it was 'in-progress' + function validStatusTransition() { + let previousStatus = resource.data.status; + let newStatus = request.resource.data.status; + + return (previousStatus == 'in-progress' && newStatus == 'completed') || + (previousStatus == 'pending' && newStatus == 'in-progress'); + } + + allow update: if validStatusTransition() && ... + ``` + +#### 3. Strict Path and Relationship Scoping + +For any field that references another resource (like an image path or a parent document ID), you **MUST** ensure it is correctly scoped to the user or valid within the context. + +**Example:** + +```javascript +// Ensure image path is within the user's own storage folder +allow create: if isScopedPath(request.resource.data.imageBucket) && ... +``` + +#### 4. Secure Counter Updates + +When allowing users to update a counter (like `voteCount` or `answerCount`), you **MUST** ensure: +1. **Atomic Increments:** The field is only changing by exactly +1 or -1. +2. **Isolation:** **NO OTHER FIELDS** are being modified. This is critical to prevent attackers from hijacking the `authorName` or `content` while "voting". +3. **Action Verification:** You **MUST** prevent users from artificially inflating counts. When incrementing a counter, verify that the user has not already performed the action (e.g., by checking for the existence of a 'like' document) and is not looping updates. + * **CRITICAL:** Relying solely on `!exists(likeDoc)` is insufficient because a malicious user can skip creating the document and loop the increment. + * **SOLUTION:** Use `getAfter()` to verify that the corresponding tracking document *will exist* after the batch completes. + +**Example:** + +```javascript +function isValidCounterUpdate(docId) { + // Allow update only if 'voteCount' is the ONLY field changing + return request.resource.data.diff(resource.data).affectedKeys().hasOnly(['voteCount']) && + // And the change is exactly +1 or -1 + math.abs(request.resource.data.voteCount - resource.data.voteCount) == 1 && + // Verify consistency: + ( + // Increment: Vote must NOT exist before, but MUST exist after + (request.resource.data.voteCount > resource.data.voteCount && + !exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) && + getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) != null) || + // Decrement: Vote MUST exist before, but must NOT exist after + (request.resource.data.voteCount < resource.data.voteCount && + exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) && + getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) == null) + ); +} + +allow update: if isValidCounterUpdate(docId) && ... +``` + +#### 5. **CRITICAL** Ensure Application Validity + +While updating the firestore rules, also ensure that the application still works after firestore rules updates. + +3. **For each collection, implement explicit data validation:** + +- Type Checking: 'field is string', 'field is number', 'field is bool', 'field is timestamp' +- Required fields validation using 'hasRequiredFields()' +- **Enforce Size Limits:** For **EVERY** string, list, and map field, you **MUST** enforce realistic size limits (e.g., `text.size() < 1000`, `tags.size() < 20`). **Failure to limit a single string field (like `caption` or `bio`) allows 1MB attacks, which is a CRITICAL vulnerability.** +- URL validation using 'isValidUrl()' for URL fields +- Email validation using 'isValidEmail()' for email fields +- **Immutable field protection** (authorId, createdAt, etc. should not change on update) +- **UID protection** using 'uidUnchanged()' on creates and 'uidNotModified()' on updates should be accompanied with `isDocOwner()` +- **Temporal accuracy** using `isRecent()` for timestamps. +- **Range validation** using `isPositive()` or similar for numbers. +- **Path scoping** using `isScopedPath()` for storage paths. + +Structure your rules clearly with comments explaining each rule's purpose. + +#### Phase-3: Devil's Advocate Attack + +**Critical step:** Systematically attempt to break your own rules using the following attack vectors. You MUST document the outcome of each attempt. + +1. **Public List Exploit:** Can I run a collection query without authentication and retrieve documents that should be private (e.g., where `visible == false`)? +2. **Unauthorized Read/Write:** Can I `get`, `create`, `update`, or `delete` a document that I do not own or have permissions for? +3. **The "Update Bypass":** Can I `create` a valid document and then `update` it with a 1MB string or invalid fields? (Tests if validation logic is missing from `update`). +4. **Ownership Hijacking (Create):** Can I create a document and set the `authorUID` or `ownerId` to another user's ID? +5. **Ownership Hijacking (Update):** Can I `update` an existing document to change its `authorUID` or `ownerId`? +6. **Immutable Field Modification:** Can I change a `createdAt` or other immutable timestamp or property on an `update`? +7. **Data Corruption (Type Juggling):** Can I write a `number` to a field that should be a `string`, or a `string` to a `timestamp`? +8. **Validation Bypass (Create vs. Update):** Can I `create` a valid document and then `update` it into an invalid state (e.g., remove a required field, write a string that's too long)? +9. **Resource Exhaustion / DoS:** Can I write an enormous string (e.g., 1MB) to any field that accepts a string or a massive array to a list field? Every string field (e.g., `bio`, `url`, `name`) MUST have a `.size()` check. If any are missing, it's a "Resource Exhaustion/DoS" risk. +10. **Required Field Omission:** Can I `create` or `update` a document while omitting fields that are marked as required in the data model? +11. **Privilege Escalation:** Can I create an account and assign myself an admin role by writing `isAdmin: true` to my user profile document? (Tests reliance on document data vs. custom claims). +12. **Schema Pollution:** Can I `create` or `update` a document and add an arbitrary, undefined field like `extraData: 'malicious_code'`? (Tests for strict schema enforcement). +13. **Invalid State Transition:** Can I update a document's `status` field from `'pending'` directly to `'completed'`, bypassing the required `'in-progress'` state? (Tests business logic enforcement). +14. **Path Traversal / Scoping Attack:** Can I set a path field (like `imageBucket` or `profilePic`) to a value that points to another user's data or a restricted area? (Tests for regex path scoping). +15. **Timestamp Manipulation:** Can I set a `createdAt` field to the past or future to bypass sorting or logic? (Tests for `request.time` validation). +16. **Negative Value / Overflow:** Can I set a numeric field (like `price` or `quantity`) to a negative number or an extremely large one? (Tests for range validation). +17. **The "Mixed Content" Leak:** Create a second user. Can User B read User A's users document? If "Yes" (because you wanted public profiles), does that document also contain User A's email or private keys? If both are true, the rules are insecure. +18. **Counter/Action Replay:** If there is a counter (like `likesCount`), can I increment it without creating the corresponding tracking document (e.g., inside `likes/{userId}`)? Can I increment it twice? (Tests for `getAfter()` consistency checks). +19. **Orphaned Subcollection Access:** Can I read/write to a subcollection (e.g., `users/123/posts/456`) if the parent document (`users/123`) does not exist? (Tests for parent existence checks). +20. **Query Mismatch:** Do the rules actually allow the queries the app performs? (e.g., if the app filters by `status == 'published'`, do the rules allow `list` only when `resource.data.status == 'published'`?) +21. **Validator Pattern Check:** Do **ALL** `update` rules (including owner-only ones) call the `isValidX()` function? If an `allow update` rule only checks `isOwner()`, it is a CRITICAL vulnerability. + +Document each attack attempt and whether it succeeded. If ANY attack succeeds: + +- Fix the security hole +- Regenerate the rules +- **Repeat Phase-3** until no attacks succeed + +#### Phase-4: Syntactic Validation + +Once devil's advocate testing passes, repeat until rules pass validation. + +**After all phases are complete, create or update the `firestore.rules` file.** + +### Critical Constraints +1. **Never skip the devil's advocate phase** - this is your primary security validation +2. **MUST include helper functions** for common operations ('isAuthenticated', 'isOwner', 'uidUnchanged', 'uidNotModified') AND domain validators ('isValidUser', etc.) +3. **MUST document assumed data models** at the beginning of the rules file +4. **Always validate the rules syntax** using 'firebase deploy --only firestore:rules --dry-run' or a similar tool before outputting the final file. +5. **Provide complete, runnable code** - no placeholders or TODOs +6. **Document all assumptions** about data structure or access patterns +7. **Always run the devil's advocate attack** after any modification of the rules. +8. **Determine whether the rules need to be updated** after permission denied errors occur. +9. **Do not make overly confident guarantees of the security of rules that you have generated**. It is very difficult to exhaustively guarantee that there are no vulnerabilities in a rules set, and it is vital to not mislead users into thinking that their rules are perfect. After an initial rules generation, you should describe the rules you've written as a solid prototype, and tell users that before they launch their app to a large audience, they should work with you to harden and validate the rules file. Be clear that users should carefully review rules to ensure security. diff --git a/skills/firebase-firestore-basics/references/web_sdk_usage.md b/skills/firebase-firestore-standard/references/web_sdk_usage.md similarity index 97% rename from skills/firebase-firestore-basics/references/web_sdk_usage.md rename to skills/firebase-firestore-standard/references/web_sdk_usage.md index d858bfe..3d85134 100644 --- a/skills/firebase-firestore-basics/references/web_sdk_usage.md +++ b/skills/firebase-firestore-standard/references/web_sdk_usage.md @@ -12,7 +12,7 @@ import { getFirestore } from "firebase/firestore"; // const app = initializeApp(); const firebaseConfig = { - // Your config options. Get the values by running 'firebase apps:sdkconfig ' + // Your config options. Get the values by running 'npx -y firebase-tools@latest apps:sdkconfig ' }; const app = initializeApp(firebaseConfig); diff --git a/skills/firebase-hosting-basics/SKILL.md b/skills/firebase-hosting-basics/SKILL.md index a27e60d..a83ac28 100644 --- a/skills/firebase-hosting-basics/SKILL.md +++ b/skills/firebase-hosting-basics/SKILL.md @@ -41,6 +41,6 @@ For instructions on deploying your site, using preview channels, and managing re ### 3. Emulation To test your app locally: ```bash -firebase emulators:start --only hosting +npx -y firebase-tools@latest emulators:start --only hosting ``` This serves your app at `http://localhost:5000` by default. diff --git a/skills/firebase-hosting-basics/references/deploying.md b/skills/firebase-hosting-basics/references/deploying.md index 9e4eb86..df26c5e 100644 --- a/skills/firebase-hosting-basics/references/deploying.md +++ b/skills/firebase-hosting-basics/references/deploying.md @@ -4,7 +4,7 @@ To deploy your Hosting content and configuration to your live site: ```bash -firebase deploy --only hosting +npx -y firebase-tools@latest deploy --only hosting ``` This deploys to your default sites (`PROJECT_ID.web.app` and `PROJECT_ID.firebaseapp.com`). @@ -14,7 +14,7 @@ Preview channels allow you to test changes on a temporary URL before going live. ### Deploy to a Preview Channel ```bash -firebase hosting:channel:deploy CHANNEL_ID +npx -y firebase-tools@latest hosting:channel:deploy CHANNEL_ID ``` Replace `CHANNEL_ID` with a name (e.g., `feature-beta`). This returns a preview URL like `PROJECT_ID--CHANNEL_ID-RANDOM_HASH.web.app`. @@ -22,18 +22,18 @@ This returns a preview URL like `PROJECT_ID--CHANNEL_ID-RANDOM_HASH.web.app`. ### Expiration Channels expire after 7 days by default. To set a different expiration: ```bash -firebase hosting:channel:deploy CHANNEL_ID --expires 1d +npx -y firebase-tools@latest hosting:channel:deploy CHANNEL_ID --expires 1d ``` ## Cloning to Live You can promote a version from a preview channel to your live channel without rebuilding. ```bash -firebase hosting:clone SOURCE_SITE_ID:SOURCE_CHANNEL_ID TARGET_SITE_ID:live +npx -y firebase-tools@latest hosting:clone SOURCE_SITE_ID:SOURCE_CHANNEL_ID TARGET_SITE_ID:live ``` **Example:** Clone the `feature-beta` channel on your default site to live: ```bash -firebase hosting:clone my-project:feature-beta my-project:live +npx -y firebase-tools@latest hosting:clone my-project:feature-beta my-project:live ``` From a20013ed94093fbb06261492bfac3ef244664ca0 Mon Sep 17 00:00:00 2001 From: Joe Hanley Date: Thu, 2 Apr 2026 10:34:41 -0700 Subject: [PATCH 10/10] Fix typo during merge (#65) --- skills/firebase-data-connect-basics/SKILL.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/skills/firebase-data-connect-basics/SKILL.md b/skills/firebase-data-connect-basics/SKILL.md index 27fa78b..e87a3c1 100644 --- a/skills/firebase-data-connect-basics/SKILL.md +++ b/skills/firebase-data-connect-basics/SKILL.md @@ -78,18 +78,6 @@ Common commands (run from project root): ```bash # Initialize Data Connect -<<<<<<< HEAD -firebase init dataconnect - -# Start local emulator -firebase emulators:start --only dataconnect - -# Generate SDK code -firebase dataconnect:sdk:generate - -# Deploy to production -firebase deploy --only dataconnect -======= npx -y firebase-tools@latest init dataconnect # Start local emulator @@ -100,7 +88,6 @@ npx -y firebase-tools@latest dataconnect:sdk:generate # Deploy to production npx -y firebase-tools@latest deploy --only dataconnect ->>>>>>> origin/main ``` ## Examples