From 9c14c0bdef4707fffa89e5df3aa05fbbe2ec129f Mon Sep 17 00:00:00 2001 From: agentnightshift Date: Sat, 10 Oct 2026 22:16:12 -0500 Subject: [PATCH 1/2] Fix documentation that still described CORE #70 as pending and listed list_context twice --- AGENTS.md | 5 ++++- CLAUDE.md | 3 +-- NEXT-STEPS.md | 4 ++-- metacortexplan.md | 4 ++++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 251b626..4a4f6a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -175,7 +175,10 @@ See `docs/DEPLOYMENT.md` for the deployment playbook. - **Project**: `my-brain-88870` (alias: prod) in `.firebaserc` - **Emulators**: Functions on port 5001, Firestore on port 8080 (UI enabled) -- **Firestore indexes**: Three composite indexes on `memory_vectors` — `metadata.module_name` + `embedding`, `metadata.branch_state` + `embedding`, and `metadata.branch_state` + `metadata.module_name` + `embedding` (all 768-dim FLAT) +- **Firestore indexes**: 8 composite indexes and 3 TTL field overrides defined in `firestore.indexes.json`: + - `memory_vectors`: `metadata.module_name` + `embedding` (768 FLAT), `metadata.branch_state` + `embedding` (768 FLAT), `metadata.branch_state` + `metadata.module_name` + `embedding` (768 FLAT), `metadata.branch_state` + `metadata.created_at` (DESC), and `metadata.branch_state` + `metadata.module_name` + `metadata.created_at` (DESC) + - `memory_vectors_eval`: `metadata.module_name` + `embedding` (768 FLAT), `metadata.branch_state` + `embedding` (768 FLAT), and `metadata.branch_state` + `metadata.module_name` + `embedding` (768 FLAT) + - Field overrides: TTL enabled on `expires_at` for `memory_events`, `memory_vectors_write_fingerprints`, and `retrieval_query_events` - **Observability collection**: `memory_events` stores one audit record per tool call and one per ingress rejection request, including `client_id`, `event_type`, `status`, `latency_ms`, and compact request/response or reason metadata - **Predeploy hook**: `npm --prefix "$RESOURCE_DIR" run build` diff --git a/CLAUDE.md b/CLAUDE.md index 8bfce72..44e1eb7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,8 +71,7 @@ Auth uses timing-safe token comparison. Origin allowlisting supports `"*"` wildc | `save_context` | Single write tool for chat and admin clients: save durable memory with optional topic, draft flag or explicit branch state, image input, artifact refs, and provenance metadata | | `search_context` | Query → embedding → Firestore vector similarity search (cosine, top-K) with metadata and provenance origin filters | | `fetch_context` | Retrieve one stored memory by document ID after search | -| `list_context` | Enumerate stored memories with filters and pagination | -| `list_context` | Optional paginated metadata listing; grant explicitly to ordinary clients | +| `list_context` | Enumerate stored memories with filters and pagination; grant explicitly to ordinary clients | | `deprecate_context` | Soft-delete: mark document as deprecated, optionally record the superseding document ID, and record supersession_reason ("changed" sets valid_until, "corrected" does not) | | `consolidate_context` | Merge N related memories into one canonical active memory via LLM; deprecates all sources with `superseded_by` pointing to the merged result. Defaults to WIP queue for a topic; accepts explicit `source_ids` for targeted consolidation | diff --git a/NEXT-STEPS.md b/NEXT-STEPS.md index 5e57b36..10a7f1b 100644 --- a/NEXT-STEPS.md +++ b/NEXT-STEPS.md @@ -1,9 +1,9 @@ # Next steps -**As of 2026-10-03 UTC.** Derived from [the canonical roadmap](metacortexplan.md). +**As of 2026-10-03 UTC, item 2 updated 2026-10-10.** Derived from [the canonical roadmap](metacortexplan.md). 1. Close BASE #68 and VERIFY #69 on the board. DOC #67 is accepted, with the evidence in [the acceptance record](metacortexplan.md#2026-10-03-utc-foundation-acceptance-doc-67). The baseline is on `release` at `97bc073`, [hosted CI passed](https://github.com/chrishonson/Metacortex/actions/runs/37097980532), and a fresh clone passed all three gates. BASE's preserved-work inventory is in [the baseline record](docs/operations/2026-10-01-adoption-baseline.md). Preserve completed work and the independent gate definitions. -2. Start CORE #70 after foundation acceptance: lifecycle correctness, filtering/pagination, idempotency, concurrency and partial-failure handling. RECOVERY #71 follows BASE/VERIFY. TIERING #83 stays deferred. +2. CORE #70 landed in PR #14 (`4e2d337`) and is deployed in revision `metacortexmcp-00031-dig`; see [the CORE record](metacortexplan.md#2026-10-10-utc-core-70-acceptance-record). RECOVERY #71 follows BASE/VERIFY. TIERING #83 stays deferred. 3. Continue the dependency graph in the roadmap, recording exact source, gate and live evidence as each card completes. Card 66 is **done**: PR #12 merged, the new write contract deployed, and all nine scoped profiles plus admin passed save/search/fetch and old-name rejection. See [the rollout record](docs/operations/2026-10-03-card66-rollout.md). Long-running clients may need a tool-catalog refresh. diff --git a/metacortexplan.md b/metacortexplan.md index 7d74255..f4bda0c 100644 --- a/metacortexplan.md +++ b/metacortexplan.md @@ -317,3 +317,7 @@ Remaining limitations: - Production runs revision `metacortexmcp-00030-zaj`, deployed after PR #12 and before PR #13 merged. Code that arrived with PR #13 is not claimed as deployed. - `.claude/settings.local.json` is tracked in this public repository. It holds no credentials, but it contains personal absolute paths and an allow rule for the removed `remember_context` tool. Untrack or scrub it before DISTRIBUTE #84. - The reconciliation ledger above lists `studies/ci-recurrence/` as retained. The study data is not on `release`. It remains on `codex/self-hosted-adoption-baseline`, which is the intended release exclusion. + +### 2026-10-10 UTC CORE #70 acceptance record + +PR #14 merged 2026-10-05 and landed six lifecycle correctness fixes in saving, retiring, consolidating, searching and listing (fix commit `b8b09aa57c77307a6e7610793774b50501119a8a`, merge commit `4e2d3371269135436f04d934837235bdc5f28baf`) with 22 new regression tests; on the combined tree the typecheck, 139 tests with coverage and the 70-file clean Journey install passed, and 11 of 11 checks passed against the Firestore emulator; production serves revision `metacortexmcp-00031-dig`, built from that merge commit; card #70 was closed as done on 2026-10-10. Remaining limitations, stated plainly: no check against production after the deploy is recorded, and dependency card #69 (VERIFY) is still open. From d211d73677d3875c7a3d3a7960b2ce603ebac129 Mon Sep 17 00:00:00 2001 From: agentnightshift Date: Sat, 10 Oct 2026 22:19:22 -0500 Subject: [PATCH 2/2] Stop tracking the emulator log and the agent skill files that .gitignore already excludes --- .agent/skills/firebase-ai-logic | 1 - .agent/skills/firebase-app-hosting-basics | 1 - .agent/skills/firebase-auth-basics | 1 - .agent/skills/firebase-basics | 1 - .agent/skills/firebase-data-connect | 1 - .agent/skills/firebase-firestore-basics | 1 - .agent/skills/firebase-hosting-basics | 1 - .agents/skills/firebase-ai-logic/SKILL.md | 109 ----- .../references/usage_patterns_web.md | 174 -------- .../firebase-app-hosting-basics/SKILL.md | 58 --- .../references/cli_commands.md | 71 ---- .../references/configuration.md | 51 --- .../references/emulation.md | 47 --- .agents/skills/firebase-auth-basics/SKILL.md | 86 ---- .../references/client_sdk_web.md | 287 ------------- .../references/security_rules.md | 38 -- .agents/skills/firebase-basics/SKILL.md | 111 ------ .../firebase-basics/references/web_setup.md | 69 ---- .agents/skills/firebase-data-connect/SKILL.md | 95 ----- .../skills/firebase-data-connect/examples.md | 377 ------------------ .../reference/advanced.md | 303 -------------- .../firebase-data-connect/reference/config.md | 267 ------------- .../reference/operations.md | 357 ----------------- .../firebase-data-connect/reference/schema.md | 278 ------------- .../firebase-data-connect/reference/sdks.md | 287 ------------- .../reference/security.md | 289 -------------- .../skills/firebase-data-connect/templates.md | 269 ------------- .../skills/firebase-firestore-basics/SKILL.md | 27 -- .../references/indexes.md | 82 ---- .../references/provisioning.md | 101 ----- .../references/security_rules.md | 299 -------------- .../references/web_sdk_usage.md | 183 --------- .../skills/firebase-hosting-basics/SKILL.md | 46 --- .../references/configuration.md | 101 ----- .../references/deploying.md | 39 -- .gitignore | 2 +- firestore-debug.log | 16 - skills-lock.json | 40 -- 38 files changed, 1 insertion(+), 4565 deletions(-) delete mode 120000 .agent/skills/firebase-ai-logic delete mode 120000 .agent/skills/firebase-app-hosting-basics delete mode 120000 .agent/skills/firebase-auth-basics delete mode 120000 .agent/skills/firebase-basics delete mode 120000 .agent/skills/firebase-data-connect delete mode 120000 .agent/skills/firebase-firestore-basics delete mode 120000 .agent/skills/firebase-hosting-basics delete mode 100644 .agents/skills/firebase-ai-logic/SKILL.md delete mode 100644 .agents/skills/firebase-ai-logic/references/usage_patterns_web.md delete mode 100644 .agents/skills/firebase-app-hosting-basics/SKILL.md delete mode 100644 .agents/skills/firebase-app-hosting-basics/references/cli_commands.md delete mode 100644 .agents/skills/firebase-app-hosting-basics/references/configuration.md delete mode 100644 .agents/skills/firebase-app-hosting-basics/references/emulation.md delete mode 100644 .agents/skills/firebase-auth-basics/SKILL.md delete mode 100644 .agents/skills/firebase-auth-basics/references/client_sdk_web.md delete mode 100644 .agents/skills/firebase-auth-basics/references/security_rules.md delete mode 100644 .agents/skills/firebase-basics/SKILL.md delete mode 100644 .agents/skills/firebase-basics/references/web_setup.md delete mode 100644 .agents/skills/firebase-data-connect/SKILL.md delete mode 100644 .agents/skills/firebase-data-connect/examples.md delete mode 100644 .agents/skills/firebase-data-connect/reference/advanced.md delete mode 100644 .agents/skills/firebase-data-connect/reference/config.md delete mode 100644 .agents/skills/firebase-data-connect/reference/operations.md delete mode 100644 .agents/skills/firebase-data-connect/reference/schema.md delete mode 100644 .agents/skills/firebase-data-connect/reference/sdks.md delete mode 100644 .agents/skills/firebase-data-connect/reference/security.md delete mode 100644 .agents/skills/firebase-data-connect/templates.md delete mode 100644 .agents/skills/firebase-firestore-basics/SKILL.md delete mode 100644 .agents/skills/firebase-firestore-basics/references/indexes.md delete mode 100644 .agents/skills/firebase-firestore-basics/references/provisioning.md delete mode 100644 .agents/skills/firebase-firestore-basics/references/security_rules.md delete mode 100644 .agents/skills/firebase-firestore-basics/references/web_sdk_usage.md delete mode 100644 .agents/skills/firebase-hosting-basics/SKILL.md delete mode 100644 .agents/skills/firebase-hosting-basics/references/configuration.md delete mode 100644 .agents/skills/firebase-hosting-basics/references/deploying.md delete mode 100644 firestore-debug.log delete mode 100644 skills-lock.json diff --git a/.agent/skills/firebase-ai-logic b/.agent/skills/firebase-ai-logic deleted file mode 120000 index 7fac814..0000000 --- a/.agent/skills/firebase-ai-logic +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/firebase-ai-logic \ No newline at end of file diff --git a/.agent/skills/firebase-app-hosting-basics b/.agent/skills/firebase-app-hosting-basics deleted file mode 120000 index a55aceb..0000000 --- a/.agent/skills/firebase-app-hosting-basics +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/firebase-app-hosting-basics \ No newline at end of file diff --git a/.agent/skills/firebase-auth-basics b/.agent/skills/firebase-auth-basics deleted file mode 120000 index a5c22cd..0000000 --- a/.agent/skills/firebase-auth-basics +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/firebase-auth-basics \ No newline at end of file diff --git a/.agent/skills/firebase-basics b/.agent/skills/firebase-basics deleted file mode 120000 index 1bac363..0000000 --- a/.agent/skills/firebase-basics +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/firebase-basics \ No newline at end of file diff --git a/.agent/skills/firebase-data-connect b/.agent/skills/firebase-data-connect deleted file mode 120000 index edcb837..0000000 --- a/.agent/skills/firebase-data-connect +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/firebase-data-connect \ No newline at end of file diff --git a/.agent/skills/firebase-firestore-basics b/.agent/skills/firebase-firestore-basics deleted file mode 120000 index 44a9c4d..0000000 --- a/.agent/skills/firebase-firestore-basics +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/firebase-firestore-basics \ No newline at end of file diff --git a/.agent/skills/firebase-hosting-basics b/.agent/skills/firebase-hosting-basics deleted file mode 120000 index d7c095b..0000000 --- a/.agent/skills/firebase-hosting-basics +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/firebase-hosting-basics \ No newline at end of file diff --git a/.agents/skills/firebase-ai-logic/SKILL.md b/.agents/skills/firebase-ai-logic/SKILL.md deleted file mode 100644 index f2edc7d..0000000 --- a/.agents/skills/firebase-ai-logic/SKILL.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -name: firebase-ai-logic -description: Official skill for integrating Firebase AI Logic (Gemini API) into web applications. Covers setup, multimodal inference, structured output, and security. -version: 1.0.0 ---- - -# Firebase AI Logic Basics - -## Overview - -Firebase AI Logic is a product of Firebase that allows developers to add gen AI to their mobile and web apps using client-side SDKs. You can call Gemini models directly from your app without managing a dedicated backend. Firebase AI Logic, which was previously known as "Vertex AI for Firebase", represents the evolution of Google's AI integration platform for mobile and web developers. - -It supports the two Gemini API providers: -- **Gemini Developer API**: It has a free tier ideal for prototyping, and pay-as-you-go for production -- **Vertex AI Gemini API**: Ideal for scale with enterprise-grade production readiness, requires Blaze plan - -Use the Gemini Developer API as a default, and only Vertex AI Gemini API if the application requires it. - -## Setup & Initialization - -### Prerequisites - -- Before starting, ensure you have **Node.js 16+** and npm installed. Install them if they aren’t already available. -- Identify the platform the user is interested in building on prior to starting: Android, iOS, Flutter or Web. -- If their platform is unsupported, Direct the user to Firebase Docs to learn how to set up AI Logic for their application (share this link with the user https://firebase.google.com/docs/ai-logic/get-started) - -### Installation - -The library is part of the standard Firebase Web SDK. - -`npm install -g firebase@latest` - -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` - -Ensure there's at least one app associated with the current project - -`firebase apps:list` - -Initialize AI logic SDK with the init command - -`firebase init # Choose AI logic` - -This will automatically enable the Gemini Developer API in the Firebase console. - -More info in [Firebase AI Logic Getting Started](https://firebase.google.com/docs/ai-logic/get-started.md.txt) - -## Core Capabilities - -### Text-Only Generation - -### Multimodal (Text + Images/Audio/Video/PDF input) - -Firebase AI Logic allows Gemini models to analyze image files directly from your app. This enables features like creating captions, answering questions about images, detecting objects, and categorizing images. Beyond images, Gemini can analyze other media types like audio, video, and PDFs by passing them as inline data with their MIME type. For files larger than 20 megabytes (which can cause HTTP 413 errors as inline data), store them in Cloud Storage for Firebase and pass their URLs to the Gemini Developer API. - -### Chat Session (Multi-turn) - -Maintain history automatically using `startChat`. - -### Streaming Responses - -To improve the user experience by showing partial results as they arrive (like a typing effect), use `generateContentStream` instead of `generateContent` for faster display of results. - -### 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. (Example: gemini-2.5-flash-image) -- Requires an upgraded Blaze pay-as-you-go billing plan. - -### Search Grounding with the built in googleSearch tool - -## Supported Platforms and Frameworks - -Supported Platforms and Frameworks include Kotlin and Java for Android, Swift for iOS, JavaScript for web apps, Dart for Flutter, and C Sharp for Unity. - -## Advanced Features - -### Structured Output (JSON) - -Enforce a specific JSON schema for the response. - -### On-Device AI (Hybrid) - -Hybrid on-device inference for web apps, where the Firebase Javascript SDK automatically checks for Gemini Nano's availability (after installation) and switches between on-device or cloud-hosted prompt execution. This requires specific steps to enable model usage in the Chrome browser, more info in the [hybrid-on-device-inference documentation](https://firebase.google.com/docs/ai-logic/hybrid-on-device-inference.md.txt). - -## Security & Production - -### App Check - -Recommended: The developer must enable Firebase App Check to prevent unauthorized clients from using their API quota. see [App-check recaptcha enterprise](https://firebase.google.com/docs/app-check/web/recaptcha-enterprise-provider.md.txt). - -### Remote Config - -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 - -| Language, Framework, Platform | Gemini API provider | Context URL | -| :---- | :---- | :---- | -| Web Modular API | Gemini Developer API (Developer API) | firebase://docs/ai-logic/get-started | - -**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 - -[Web SDK code examples and usage patterns](references/usage_patterns_web.md) - - - diff --git a/.agents/skills/firebase-ai-logic/references/usage_patterns_web.md b/.agents/skills/firebase-ai-logic/references/usage_patterns_web.md deleted file mode 100644 index e6435bb..0000000 --- a/.agents/skills/firebase-ai-logic/references/usage_patterns_web.md +++ /dev/null @@ -1,174 +0,0 @@ -# Firebase AI Logic Basics - -## Initialization Pattern -You must initialize the ai-logic service after the main Firebase App. -```JavaScript -import { initializeApp } from "firebase/app"; -import { getAI, getGenerativeModel, GoogleAIBackend } from "firebase/ai"; - - -// If running in Firebase App Hosting, you can skip Firebase Config and instead use: -// const app = initializeApp(); - -const firebaseConfig = { - // ... your firebase config -}; - -const app = initializeApp(firebaseConfig); - -// Initialize the AI Logic service (defaults to Gemini Developer API) -// To set the AI provider, set the backend as the second parameter -const ai = getAI(firebaseApp, { backend: new GoogleAIBackend() }); - -const generationConfig = { - candidate_count: 1, - maxOutputTokens: 2048, - stopSequences: [], - temperature: 0.7, // Balanced: creative but focused - topP: 0.95, // Standard: allows a wide range of probable tokens - topK: 40, // Standard: considers the top 40 tokens -}; - -// Specify the config as part of creating the `GenerativeModel` instance -const model = getGenerativeModel(ai, { model: "gemini-2.5-flash-lite", generationConfig }); -``` - -## Core Capabilities -Text-Only Generation -```JavaScript -async function generateText(prompt) { - const result = await model.generateContent(prompt); - const response = await result.response; - return response.text(); -} -``` - -## Multimodal (Text + Images/Audio/Video/PDF input) -Firebase AI Logic accepts Base64 encoded data or specific file references. -```JavaScript -// Helper to convert file to base64 generic object -async function fileToGenerativePart(file) { - const base64EncodedDataPromise = new Promise((resolve) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result.split(',')[1]); - reader.readAsDataURL(file); - }); - - return { - inlineData: { - data: await base64EncodedDataPromise, - mimeType: file.type, - }, - }; -} - -async function analyzeImage(prompt, imageFile) { - const imagePart = await fileToGenerativePart(imageFile); - const result = await model.generateContent([prompt, imagePart]); - return result.response.text(); -} -``` - -## Chat Session (Multi-turn) -Maintain history automatically using startChat. -```JavaScript -const chat = model.startChat({ - history: [ - { - role: "user", - parts: [{ text: "Hello, I am a developer." }], - }, - { - role: "model", - parts: [{ text: "Great to meet you. How can I help with code?" }], - }, - ], -}); - -async function sendMessage(msg) { - const result = await chat.sendMessage(msg); - return result.response.text(); -} -``` - -## Streaming Responses -For real-time UI updates (like a typing effect). -```JavaScript -async function streamResponse(prompt) { - const result = await model.generateContentStream(prompt); - for await (const chunk of result.stream) { - const chunkText = chunk.text(); - console.log("Stream chunk:", chunkText); - // Update UI here - } -} -``` - -Generate Images with Nano Banana - -```Javascript -import { initializeApp } from "firebase/app"; -import { getAI, getGenerativeModel, GoogleAIBackend, ResponseModality } from "firebase/ai"; - - -// Initialize FirebaseApp -const firebaseApp = initializeApp(firebaseConfig); - -// Initialize the Gemini Developer API backend service -const ai = getAI(firebaseApp, { backend: new GoogleAIBackend() }); - -// Create a `GenerativeModel` instance with a model that supports your use case -const model = getGenerativeModel(ai, { - model: "gemini-2.5-flash-image", - // Configure the model to respond with text and images (required) - generationConfig: { - responseModalities: [ResponseModality.TEXT, ResponseModality.IMAGE], - }, -}); - -// Provide a text prompt instructing the model to generate an image -const prompt = 'Generate an image of the Eiffel Tower with fireworks in the background.'; - -// To generate an image, call `generateContent` with the text input -const result = model.generateContent(prompt); - -// Handle the generated image -try { - const inlineDataParts = result.response.inlineDataParts(); - if (inlineDataParts?.[0]) { - const image = inlineDataParts[0].inlineData; - console.log(image.mimeType, image.data); - } -} catch (err) { - console.error('Prompt or candidate was blocked:', err); -} -``` - -## Advanced Features -Structured Output (JSON) -Enforce a specific JSON schema for the response. -```JavaScript -import { getGenerativeModel, Schema } from "firebase/ai"; -const jsonModel = getGenerativeModel(ai, { - model: "gemini-2.5-flash-lite", - generationConfig: { - responseMimeType: "application/json", - // Optional: Define a schema - schema = Schema.object({ ... }); - } -}); - -async function getJsonData(prompt) { - const result = await jsonModel.generateContent(prompt); - return JSON.parse(result.response.text()); -} -``` - -On-Device AI (Hybrid) -Automatically switch between local Gemini Nano and cloud models based on device capability. -```JavaScript -import {getGenerativeModel, InferenceMode } from "firebase/ai"; - -const hybridModel = getGenerativeModel(ai, { mode: InferenceMode.PREFER_ON_DEVICE }); -``` - diff --git a/.agents/skills/firebase-app-hosting-basics/SKILL.md b/.agents/skills/firebase-app-hosting-basics/SKILL.md deleted file mode 100644 index 93396fb..0000000 --- a/.agents/skills/firebase-app-hosting-basics/SKILL.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -name: firebase-app-hosting-basics -description: Deploy and manage web apps with Firebase App Hosting. Use this skill when deploying Next.js/Angular apps with backends. ---- - -# App Hosting Basics - -## Description -This skill enables the agent to deploy and manage modern, full-stack web applications (Next.js, Angular, etc.) using Firebase App Hosting. - -**Important**: In order to use App Hosting, your Firebase project must be on the Blaze pricing plan. Direct the user to https://console.firebase.google.com/project/_/overview?purchaseBillingPlan=metered to upgrade their plan. - -## Hosting vs App Hosting - -**Choose Firebase Hosting if:** -- You are deploying a static site (HTML/CSS/JS). -- You are deploying a simple SPA (React, Vue, etc. without SSR). -- You want full control over the build and deploy process via CLI. - -**Choose Firebase App Hosting if:** -- You are using a supported full-stack framework like Next.js or Angular. -- You need Server-Side Rendering (SSR) or ISR. -- You want an automated "git push to deploy" workflow with zero configuration. - -## Deploying to App Hosting - -### Deploy from Source - -This is the recommended flow for most users. -1. Configure `firebase.json` with an `apphosting` block. - ```json - { - "apphosting": { - "backendId": "my-app-id", - "rootDir": "/", - "ignore": [ - "node_modules", - ".git", - "firebase-debug.log", - "firebase-debug.*.log", - "functions" - ] - } - } - ``` -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. - -### Automated deployment via GitHub (CI/CD) - -Alternatively, set up a backend connected to a GitHub repository for automated deployments "git push" deployments. -This is only recommended for more advanced users, and is not required to use App Hosting. -See [CLI Commands](references/cli_commands.md) for more information on how to set this up using CLI commands. - -## Emulation - -See [Emulation](references/emulation.md) for more information on how to test your app locally using the Firebase Local Emulator Suite. diff --git a/.agents/skills/firebase-app-hosting-basics/references/cli_commands.md b/.agents/skills/firebase-app-hosting-basics/references/cli_commands.md deleted file mode 100644 index e1baf5b..0000000 --- a/.agents/skills/firebase-app-hosting-basics/references/cli_commands.md +++ /dev/null @@ -1,71 +0,0 @@ -# App Hosting CLI Commands - -The Firebase CLI provides a comprehensive suite of commands to manage App Hosting resources. These commands are often faster and more scriptable than using the Firebase Console. - -## Initialization - -### `firebase 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. -Alternatively, you can manually edit `firebase.json` and `apphosting.yml`. - -- **Effect**: - - Detects your web framework. - - Creates/updates `apphosting.yaml`. - - Can optionally create a backend if one doesn't exist. - -## Backend Management - -### `firebase apphosting:backends:list` - -- **Purpose**: Lists all backends in the current project. - -### `firebase apphosting:backends:get ` - -- **Purpose**: Shows details for a specific backend. - -### `firebase apphosting:backends:delete ` - -- **Purpose**: Deletes a backend and its associated resources. - -### `firebase apphosting:rollouts:list ` - -- **Purpose**: Lists the history of rollouts for a backend. - -## Secrets Management - -App Hosting uses Cloud Secret Manager to securely handle sensitive environment variables (like API keys). - -### `firebase 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 ` - -- **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. - -### `firebase apphosting:rollouts:create ` - -- **Purpose**: Manually triggers a new rollout (deployment). -- **Options**: - - `--git-branch `: Deploy the latest commit from a specific branch. - - `--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` - -- **Purpose**: Creates a new App Hosting backend. Use this when setting up automated deployments via GitHub. -- **Options**: - - `--app `: The ID of an existing Firebase web app to associate with the backend. - - `--backend `: The ID of the new backend. - - `--primary-region `: The primary region for the backend. - - `--root-dir `: The root directory for the backend. If omitted, defaults to the root directory of the project. - - `--service-account `: The service account used to run the server. If omitted, defaults to the default service account. - \ No newline at end of file diff --git a/.agents/skills/firebase-app-hosting-basics/references/configuration.md b/.agents/skills/firebase-app-hosting-basics/references/configuration.md deleted file mode 100644 index e17cc89..0000000 --- a/.agents/skills/firebase-app-hosting-basics/references/configuration.md +++ /dev/null @@ -1,51 +0,0 @@ -# App Hosting Configuration (`apphosting.yaml`) - -The `apphosting.yaml` file is the source of truth for your backend's configuration. It must be located in the root of your app's directory (or the specific root directory if using a monorepo). - -## File Structure - -```yaml -# apphosting.yaml - -# Cloud Run service configuration -runConfig: - cpu: 1 - memoryMiB: 512 - minInstances: 0 - maxInstances: 100 - concurrency: 80 - -# Environment variables -env: - - variable: STORAGE_BUCKET - value: mybucket.app - availability: - - BUILD - - RUNTIME - - variable: API_KEY - secret: myApiKeySecret -``` - -## `runConfig` -Controls the resources allocated to the Cloud Run service that serves your app. -- `cpu`: Number of vCPUs. Note: If `< 1`, concurrency MUST be set to `1`. -- `memoryMiB`: RAM in MiB (128 to 32768). -- `minInstances`: Minimum containers to keep warm (default 0). Set to >= 1 to avoid cold starts. -- `maxInstances`: Maximum scaling limit (default 100). -- `concurrency`: Max concurrent requests per instance (default 80). - -### Resource Constraints -- **CPU vs Memory**: Higher memory often requires higher CPU. - - > 4GiB RAM -> Needs >= 2 vCPU - - > 8GiB RAM -> Needs >= 4 vCPU - -## `env` (Environment Variables) -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. -- `availability`: Where the variable is needed. - - `BUILD`: Available during the `npm run build` process. - - `RUNTIME`: Available when the app is serving requests. - - Defaults to both if not specified. diff --git a/.agents/skills/firebase-app-hosting-basics/references/emulation.md b/.agents/skills/firebase-app-hosting-basics/references/emulation.md deleted file mode 100644 index dff3445..0000000 --- a/.agents/skills/firebase-app-hosting-basics/references/emulation.md +++ /dev/null @@ -1,47 +0,0 @@ -# App Hosting Emulation - -You can test your App Hosting setup locally using the Firebase Local Emulator Suite. This allows you to verify your app's behavior with environment variables and secrets before deploying. - -## Configuration: `apphosting.emulator.yaml` -This optional file overrides `apphosting.yaml` settings specifically for the local emulator. Use it to provide local secret values or override resource configs. If it contains sensitive values such as API keys, do not commit it to source control. - -```yaml -# apphosting.emulator.yaml (gitignored usually) -runConfig: - cpu: 1 - memoryMiB: 512 - -env: - - variable: API_KEY - value: "local-dev-api-key" # Override secret with local value -``` - -## Running the Emulator -To start the App Hosting emulator: - -```bash -firebase emulators:start --only apphosting -``` - -Or, if you are also using other emulators (Auth, Firestore, etc.): - -```bash -firebase emulators:start -``` - -## Capabilities -- **Builds your app**: Runs the build command defined in your `package.json` to generate the serving artifact. -- **Serves locally**: Runs the app on `localhost:5004` (default). -Configurable by setting `host` and `port` in the `emulators` block of `firebase.json`, like so: - -```json -{ - "emulators": { - "apphosting": { - "host": "localhost", - "port": 5004 - } - } -} -``` -- **Env Var Injection**: Injects variables defined in `apphosting.yaml` and `apphosting.emulator.yaml` into the process. diff --git a/.agents/skills/firebase-auth-basics/SKILL.md b/.agents/skills/firebase-auth-basics/SKILL.md deleted file mode 100644 index 1110503..0000000 --- a/.agents/skills/firebase-auth-basics/SKILL.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -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`. ---- - -## Prerequisites - -- **Firebase Project**: Created via `firebase projects:create` (see `firebase-basics`). -- **Firebase CLI**: Installed and logged in (see `firebase-basics`). - -## Core Concepts - -Firebase Authentication provides backend services, easy-to-use SDKs, and ready-made UI libraries to authenticate users to your app. - -### Users - -A user is an entity that can sign in to your app. Each user is identified by a unique ID (`uid`) which is guaranteed to be unique across all providers. -User properties include: -- `uid`: Unique identifier. -- `email`: User's email address (if available). -- `displayName`: User's display name (if available). -- `photoURL`: URL to user's photo (if available). -- `emailVerified`: Boolean indicating if the email is verified. - -### Identity Providers - -Firebase Auth supports multiple ways to sign in: -- **Email/Password**: Basic email and password authentication. -- **Federated Identity Providers**: Google, Facebook, Twitter, GitHub, Microsoft, Apple, etc. -- **Phone Number**: SMS-based authentication. -- **Anonymous**: Temporary guest accounts that can be linked to permanent accounts later. -- **Custom Auth**: Integrate with your existing auth system. - -Google Sign In is recommended as a good and secure default provider. - -### Tokens - -When a user signs in, they receive an ID Token (JWT). This token is used to identify the user when making requests to Firebase services (Realtime Database, Cloud Storage, Firestore) or your own backend. -- **ID Token**: Short-lived (1 hour), verifies identity. -- **Refresh Token**: Long-lived, used to get new ID tokens. - -## Workflow - -### 1. Provisioning - -#### Option 1. Enabling Authentication via CLI - -Only Google Sign In, anonymous auth, and email/password auth can be enabled via CLI. For other providers, use the Firebase Console. - -Configure Firebase Authentication in `firebase.json` by adding an 'auth' block: - -``` -{ - "auth": { - "providers": { - "anonymous": true, - "emailPassword": true, - "googleSignIn": { - "oAuthBrandDisplayName": "Your Brand Name", - "supportEmail": "support@example.com", - "authorizedRedirectUris": ["https://example.com"] - } - } - } -} -``` - -#### Option 2. Enabling Authentication in Console - -Enable other providers in the Firebase Console. - -1. Go to the https://console.firebase.google.com/project/_/authentication/providers -2. Select your project. -3. Enable the desired Sign-in providers (e.g., Email/Password, Google). - -### 2. Client Setup & Usage - -**Web** -See [references/client_sdk_web.md](references/client_sdk_web.md). - -### 3. Security Rules - -Secure your data using `request.auth` in Firestore/Storage rules. - -See [references/security_rules.md](references/security_rules.md). diff --git a/.agents/skills/firebase-auth-basics/references/client_sdk_web.md b/.agents/skills/firebase-auth-basics/references/client_sdk_web.md deleted file mode 100644 index 493a66a..0000000 --- a/.agents/skills/firebase-auth-basics/references/client_sdk_web.md +++ /dev/null @@ -1,287 +0,0 @@ -# Firebase Authentication Web SDK - -## Initialization - -First, ensure you have initialized the Firebase App (see `firebase-basics` skill). Then, initialize the Auth service: - -```javascript -import { getAuth } from "firebase/auth"; -import { app } from "./firebase"; // Your initialized Firebase App - -const auth = getAuth(app); -export { auth }; -``` - -## Connect to Emulator - -If you are running the Authentication emulator (usually on port 9099), connect to it immediately after initialization. - -```javascript -import { getAuth, connectAuthEmulator } from "firebase/auth"; - -const auth = getAuth(); -// Connect to emulator if running locally -if (location.hostname === "localhost") { - connectAuthEmulator(auth, "http://localhost:9099"); -} -``` - -## Sign Up with Email/Password - -```javascript -import { getAuth, createUserWithEmailAndPassword } from "firebase/auth"; - -const auth = getAuth(); -createUserWithEmailAndPassword(auth, email, password) - .then((userCredential) => { - const user = userCredential.user; - // ... - }) - .catch((error) => { - const errorCode = error.code; - const errorMessage = error.message; - // .. - }); -``` - -## Sign In with Google (Popup) - -```javascript -import { getAuth, signInWithPopup, GoogleAuthProvider } from "firebase/auth"; - -const auth = getAuth(); -const provider = new GoogleAuthProvider(); - -signInWithPopup(auth, provider) - .then((result) => { - // This gives you a Google Access Token. You can use it to access the Google API. - const credential = GoogleAuthProvider.credentialFromResult(result); - const token = credential.accessToken; - // The signed-in user info. - const user = result.user; - // ... - }) - .catch((error) => { - // Handle Errors here. - const errorCode = error.code; - const errorMessage = error.message; - // ... - }); -``` - -## Sign In with Facebook (Popup) - -```javascript -import { getAuth, signInWithPopup, FacebookAuthProvider } from "firebase/auth"; - -const auth = getAuth(); -const provider = new FacebookAuthProvider(); - -signInWithPopup(auth, provider) - .then((result) => { - // The signed-in user info. - const user = result.user; - // This gives you a Facebook Access Token. You can use it to access the Facebook API. - const credential = FacebookAuthProvider.credentialFromResult(result); - const accessToken = credential.accessToken; - }) - .catch((error) => { - // Handle Errors here. - }); -``` - -## Sign In with Apple (Popup) - -```javascript -import { getAuth, signInWithPopup, OAuthProvider } from "firebase/auth"; - -const auth = getAuth(); -const provider = new OAuthProvider('apple.com'); - -signInWithPopup(auth, provider) - .then((result) => { - const user = result.user; - // Apple credential - const credential = OAuthProvider.credentialFromResult(result); - const accessToken = credential.accessToken; - }) - .catch((error) => { - // Handle Errors here. - }); -``` - -## Sign In with Twitter (Popup) - -```javascript -import { getAuth, signInWithPopup, TwitterAuthProvider } from "firebase/auth"; - -const auth = getAuth(); -const provider = new TwitterAuthProvider(); - -signInWithPopup(auth, provider) - .then((result) => { - const user = result.user; - // Twitter credential - const credential = TwitterAuthProvider.credentialFromResult(result); - const token = credential.accessToken; - const secret = credential.secret; - }) - .catch((error) => { - // Handle Errors here. - }); -``` - -## Sign In with GitHub (Popup) - -```javascript -import { getAuth, signInWithPopup, GithubAuthProvider } from "firebase/auth"; - -const auth = getAuth(); -const provider = new GithubAuthProvider(); - -signInWithPopup(auth, provider) - .then((result) => { - const user = result.user; - const credential = GithubAuthProvider.credentialFromResult(result); - const token = credential.accessToken; - }) - .catch((error) => { - // Handle Errors here. - }); -``` - -## Sign In with Microsoft (Popup) - -```javascript -import { getAuth, signInWithPopup, OAuthProvider } from "firebase/auth"; - -const auth = getAuth(); -const provider = new OAuthProvider('microsoft.com'); - -signInWithPopup(auth, provider) - .then((result) => { - const user = result.user; - const credential = OAuthProvider.credentialFromResult(result); - const accessToken = credential.accessToken; - }) - .catch((error) => { - // Handle Errors here. - }); -``` - -## Sign In with Yahoo (Popup) - -```javascript -import { getAuth, signInWithPopup, OAuthProvider } from "firebase/auth"; - -const auth = getAuth(); -const provider = new OAuthProvider('yahoo.com'); - -signInWithPopup(auth, provider) - .then((result) => { - const user = result.user; - const credential = OAuthProvider.credentialFromResult(result); - const accessToken = credential.accessToken; - }) - .catch((error) => { - // Handle Errors here. - }); -``` - -## Sign In Anonymously - -```javascript -import { getAuth, signInAnonymously } from "firebase/auth"; - -const auth = getAuth(); -signInAnonymously(auth) - .then(() => { - // Signed in.. - }) - .catch((error) => { - const errorCode = error.code; - const errorMessage = error.message; - }); -``` - -## Email Link Authentication - -**1. Send Auth Link** - -```javascript -import { getAuth, sendSignInLinkToEmail } from "firebase/auth"; - -const auth = getAuth(); -const actionCodeSettings = { - // URL you want to redirect back to. The domain must be in the authorized domains list in Firebase Console. - url: 'https://www.example.com/finishSignUp?cartId=1234', - handleCodeInApp: true, -}; - -sendSignInLinkToEmail(auth, email, actionCodeSettings) - .then(() => { - // Save the email locally so you don't need to ask the user for it again - window.localStorage.setItem('emailForSignIn', email); - }) - .catch((error) => { - // Error - }); -``` - -**2. Complete Sign In (on landing page)** - -```javascript -import { getAuth, isSignInWithEmailLink, signInWithEmailLink } from "firebase/auth"; - -const auth = getAuth(); - -if (isSignInWithEmailLink(auth, window.location.href)) { - let email = window.localStorage.getItem('emailForSignIn'); - if (!email) { - email = window.prompt('Please provide your email for confirmation'); - } - - signInWithEmailLink(auth, email, window.location.href) - .then((result) => { - window.localStorage.removeItem('emailForSignIn'); - // You can check result.user - }) - .catch((error) => { - // Error - }); -} -``` - -## Observe Auth State - -Recommended way to get the current user. This listener triggers whenever the user signs in or out. - -```javascript -import { getAuth, onAuthStateChanged } from "firebase/auth"; - -const auth = getAuth(); -onAuthStateChanged(auth, (user) => { - if (user) { - // User is signed in, see docs for a list of available properties - // https://firebase.google.com/docs/reference/js/firebase.User - const uid = user.uid; - // ... - } else { - // User is signed out - // ... - } -}); -``` - -## Sign Out - -```javascript -import { getAuth, signOut } from "firebase/auth"; - -const auth = getAuth(); -signOut(auth).then(() => { - // Sign-out successful. -}).catch((error) => { - // An error happened. -}); -``` diff --git a/.agents/skills/firebase-auth-basics/references/security_rules.md b/.agents/skills/firebase-auth-basics/references/security_rules.md deleted file mode 100644 index 4a2463e..0000000 --- a/.agents/skills/firebase-auth-basics/references/security_rules.md +++ /dev/null @@ -1,38 +0,0 @@ -# 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. - -The `request.auth` variable contains authentication information for the user requesting data. - -## Basic Checks - -### Check if user is signed in -``` -allow read, write: if request.auth != null; -``` - -### Check if user owns the data -Access data only if the document ID matches the user's UID. -``` -allow read, write: if request.auth != null && request.auth.uid == userId; -``` -(Where `userId` is a path variable, e.g., `match /users/{userId}`) - -### Check if user owns the document (field-based) -Access data only if the document has a `owner_uid` field matching the user's UID. -``` -allow read, write: if request.auth != null && request.auth.uid == resource.data.owner_uid; -``` - -## Token Properties -`request.auth.token` contains standard JWT claims and custom claims. - -- `request.auth.token.email`: The user's email address. -- `request.auth.token.email_verified`: If the email is verified. -- `request.auth.token.name`: The user's display name. - -### Example: Email Verification Check -``` -allow create: if request.auth.token.email_verified == true; -``` diff --git a/.agents/skills/firebase-basics/SKILL.md b/.agents/skills/firebase-basics/SKILL.md deleted file mode 100644 index 2913de5..0000000 --- a/.agents/skills/firebase-basics/SKILL.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -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. ---- -## 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** -This avoids permission issues when installing global packages. - -1. **Install a Node Version Manager:** - - Mac/Linux: Follow the installation instructions on the [official nvm repository](https://github.com/nvm-sh/nvm#installing-and-updating). - - 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`. diff --git a/.agents/skills/firebase-basics/references/web_setup.md b/.agents/skills/firebase-basics/references/web_setup.md deleted file mode 100644 index 2c453c4..0000000 --- a/.agents/skills/firebase-basics/references/web_setup.md +++ /dev/null @@ -1,69 +0,0 @@ -# Firebase Web Setup Guide - -## 1. Create a Firebase Project and App -If you haven't already created a project: - -```bash -firebase projects:create -``` - -Register your web app: -```bash -firebase apps:create web my-web-app -``` -(Note the **App ID** returned by this command). - -## 2. Installation -Install the Firebase SDK via npm: - -```bash -npm install firebase -``` - -## 3. Initialization -Create a `firebase.js` (or `firebase.ts`) file. You can fetch your config object using the CLI: - -```bash -firebase apps:sdkconfig -``` - -Copy the output config object into your initialization file: - -```javascript -import { initializeApp } from "firebase/app"; -import { getAuth } from "firebase/auth"; - -// Your web app's Firebase configuration -const firebaseConfig = { - apiKey: "API_KEY", - authDomain: "PROJECT_ID.firebaseapp.com", - projectId: "PROJECT_ID", - storageBucket: "PROJECT_ID.firebasestorage.app", - messagingSenderId: "SENDER_ID", - appId: "APP_ID", - measurementId: "G-MEASUREMENT_ID" -}; - -// Initialize Firebase -const app = initializeApp(firebaseConfig); -const auth = getAuth(app); - -export { app }; -``` - -## 4. Using Services -Import specific services as needed (Modular API): - -```javascript -import { getFirestore, collection, getDocs } from "firebase/firestore"; -import { app } from "./firebase"; // Import the initialized app - -const db = getFirestore(app); - -async function getUsers() { - const querySnapshot = await getDocs(collection(db, "users")); - querySnapshot.forEach((doc) => { - console.log(`${doc.id} => ${doc.data()}`); - }); -} -``` diff --git a/.agents/skills/firebase-data-connect/SKILL.md b/.agents/skills/firebase-data-connect/SKILL.md deleted file mode 100644 index ef4e6e9..0000000 --- a/.agents/skills/firebase-data-connect/SKILL.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -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. - -## 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 -``` - -## 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`). - ---- - -## Feature Capability Map - -If you need to implement a specific feature, consult the mapped reference file: - -| 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` | - ---- - -## Deployment & CLI - -> **Read [reference/config.md](reference/config.md)** for deep dive on configuration. - -Common commands (run from project root): - -```bash -# Initialize Data Connect -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 -``` - -## Examples - -For complete, working code examples of schemas and operations, see **[examples.md](examples.md)**. diff --git a/.agents/skills/firebase-data-connect/examples.md b/.agents/skills/firebase-data-connect/examples.md deleted file mode 100644 index 87a6e3d..0000000 --- a/.agents/skills/firebase-data-connect/examples.md +++ /dev/null @@ -1,377 +0,0 @@ -# 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/.agents/skills/firebase-data-connect/reference/advanced.md b/.agents/skills/firebase-data-connect/reference/advanced.md deleted file mode 100644 index 9b5cb3b..0000000 --- a/.agents/skills/firebase-data-connect/reference/advanced.md +++ /dev/null @@ -1,303 +0,0 @@ -# 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/.agents/skills/firebase-data-connect/reference/config.md b/.agents/skills/firebase-data-connect/reference/config.md deleted file mode 100644 index ac88d33..0000000 --- a/.agents/skills/firebase-data-connect/reference/config.md +++ /dev/null @@ -1,267 +0,0 @@ -# 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/.agents/skills/firebase-data-connect/reference/operations.md b/.agents/skills/firebase-data-connect/reference/operations.md deleted file mode 100644 index 6fe7e73..0000000 --- a/.agents/skills/firebase-data-connect/reference/operations.md +++ /dev/null @@ -1,357 +0,0 @@ -# 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/.agents/skills/firebase-data-connect/reference/schema.md b/.agents/skills/firebase-data-connect/reference/schema.md deleted file mode 100644 index 21d34ec..0000000 --- a/.agents/skills/firebase-data-connect/reference/schema.md +++ /dev/null @@ -1,278 +0,0 @@ -# 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/.agents/skills/firebase-data-connect/reference/sdks.md b/.agents/skills/firebase-data-connect/reference/sdks.md deleted file mode 100644 index 5651153..0000000 --- a/.agents/skills/firebase-data-connect/reference/sdks.md +++ /dev/null @@ -1,287 +0,0 @@ -# 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'; - -// 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); - -// 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) - // Access nested fields directly - println(movie.metadata?.director) - println(movie.actors.firstOrNull()?.name) -} - -// 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) - // Access nested fields directly - print(movie.metadata?.director ?? "Unknown") - print(movie.actors.first?.name ?? "No actors") -} - -// 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/.agents/skills/firebase-data-connect/reference/security.md b/.agents/skills/firebase-data-connect/reference/security.md deleted file mode 100644 index 5eacee8..0000000 --- a/.agents/skills/firebase-data-connect/reference/security.md +++ /dev/null @@ -1,289 +0,0 @@ -# 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/.agents/skills/firebase-data-connect/templates.md b/.agents/skills/firebase-data-connect/templates.md deleted file mode 100644 index 10f0538..0000000 --- a/.agents/skills/firebase-data-connect/templates.md +++ /dev/null @@ -1,269 +0,0 @@ -# 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' }); -``` diff --git a/.agents/skills/firebase-firestore-basics/SKILL.md b/.agents/skills/firebase-firestore-basics/SKILL.md deleted file mode 100644 index a2de73b..0000000 --- a/.agents/skills/firebase-firestore-basics/SKILL.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -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`. ---- - -# Firestore Basics - -This skill provides a complete guide for getting started with Cloud Firestore, including provisioning, securing, and integrating it into your application. - -## Provisioning - -To set up Cloud Firestore in your Firebase project and local environment, see [provisioning.md](references/provisioning.md). - -## Security Rules - -For guidance on writing and deploying Firestore Security Rules to protect your data, see [security_rules.md](references/security_rules.md). - -## SDK Usage - -To learn how to use Cloud Firestore in your application code, choose your platform: - -* **Web (Modular SDK)**: [web_sdk_usage.md](references/web_sdk_usage.md) - -## Indexes - -For checking index types, query support tables, and best practices, see [indexes.md](references/indexes.md). diff --git a/.agents/skills/firebase-firestore-basics/references/indexes.md b/.agents/skills/firebase-firestore-basics/references/indexes.md deleted file mode 100644 index 6b0c4a8..0000000 --- a/.agents/skills/firebase-firestore-basics/references/indexes.md +++ /dev/null @@ -1,82 +0,0 @@ -# Firestore Indexes Reference - -Indexes allow Firestore to ensure that query performance depends on the size of the result set, not the size of the database. - -## Index Types - -### Single-Field Indexes -Firestore **automatically creates** a single-field index for every field in a document (and subfields in maps). -* **Support**: Simple equality queries (`==`) and single-field range/sort queries (`<`, `<=`, `orderBy`). -* **Behavior**: You generally don't need to manage these unless you want to *exempt* a field. - -### Composite Indexes -A composite index stores a sorted mapping of all documents based on an ordered list of fields. -* **Support**: Complex queries that filter or sort by **multiple fields**. -* **Creation**: These are **NOT** automatically created. You must define them manually or via the console/CLI. - -## Automatic vs. Manual Management - -### What is Automatic? -* Indexes for simple queries. -* Merging of single-field indexes for multiple equality filters (e.g., `where("state", "==", "CA").where("country", "==", "USA")`). - -### When Do I Need to Act? -If you attempt a query that requires a composite index, the SDK will throw an error containing a **direct link** to the Firebase Console to create that specific index. - -**Example Error:** -> "The query requires an index. You can create it here: https://console.firebase.google.com/project/..." - -## Query Support Examples - -| Query Type | Index Required | -| :--- | :--- | -| **Simple Equality**
`where("a", "==", 1)` | Automatic (Single-Field) | -| **Simple Range/Sort**
`where("a", ">", 1).orderBy("a")` | Automatic (Single-Field) | -| **Multiple Equality**
`where("a", "==", 1).where("b", "==", 2)` | Automatic (Merged Single-Field) | -| **Equality + Range/Sort**
`where("a", "==", 1).where("b", ">", 2)` | **Composite Index** | -| **Multiple Ranges**
`where("a", ">", 1).where("b", ">", 2)` | **Composite Index** (and technically limited query support) | -| **Array Contains + Equality**
`where("tags", "array-contains", "news").where("active", "==", true)` | **Composite Index** | - -## Best Practices & Exemptions - -You can **exempt** fields from automatic indexing to save storage or strictly enforce write limits. - -### 1. High Write Rates (Sequential Values) -* **Problem**: Indexing fields that increase sequentially (like `timestamp`) limits the write rate to ~500 writes/second per collection. -* **Solution**: If you don't query on this field, **exempt** it from simple indexing. - -### 2. Large String/Map/Array Fields -* **Problem**: Indexing limits (40k entries per doc). Indexing large blobs wastes storage. -* **Solution**: Exempt large text blobs or huge arrays if they aren't used for filtering. - -### 3. TTL Fields -* **Problem**: TTL (Time-To-Live) deletion can cause index churn. -* **Solution**: Exempt the TTL timestamp field from indexing if you don't query it. - -## Management - -### Config files -Your indexes should be defined in `firestore.indexes.json` (pointed to by `firebase.json`). - -```json -{ - "indexes": [ - { - "collectionGroup": "cities", - "queryScope": "COLLECTION", - "fields": [ - { "fieldPath": "country", "order": "ASCENDING" }, - { "fieldPath": "population", "order": "DESCENDING" } - ] - } - ], - "fieldOverrides": [] -} -``` - -### CLI Commands - -Deploy indexes only: -```bash -firebase deploy --only firestore:indexes -``` diff --git a/.agents/skills/firebase-firestore-basics/references/provisioning.md b/.agents/skills/firebase-firestore-basics/references/provisioning.md deleted file mode 100644 index fb5f9f5..0000000 --- a/.agents/skills/firebase-firestore-basics/references/provisioning.md +++ /dev/null @@ -1,101 +0,0 @@ -# Provisioning Cloud Firestore - -## Manual Initialization - -Initialize the following firebase configuration files manually. Do not use `firebase init`, as it expects interactive inputs. - -1. **Create `firebase.json`**: This file configures the Firebase CLI. -2. **Create `firestore.rules`**: This file contains your security rules. -3. **Create `firestore.indexes.json`**: This file contains your index definitions. - -### 1. 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" - } -} -``` - -This will use the default database. To use a different database, specify the database ID and location. You can check the list of available databases using `firebase firestore:databases:list`. If the database does not exist, it will be created when you deploy: - -```json -{ - "firestore": { - "rules": "firestore.rules", - "indexes": "firestore.indexes.json", - "database": "my-database-id", - "location": "us-central1" - } -} -``` - - To use Enterprise edition, specify the `enterprise` field. - -```json -{ - "firestore": { - "rules": "firestore.rules", - "indexes": "firestore.indexes.json", - "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: - -``` -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). diff --git a/.agents/skills/firebase-firestore-basics/references/security_rules.md b/.agents/skills/firebase-firestore-basics/references/security_rules.md deleted file mode 100644 index 3c9f02e..0000000 --- a/.agents/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/.agents/skills/firebase-firestore-basics/references/web_sdk_usage.md b/.agents/skills/firebase-firestore-basics/references/web_sdk_usage.md deleted file mode 100644 index d858bfe..0000000 --- a/.agents/skills/firebase-firestore-basics/references/web_sdk_usage.md +++ /dev/null @@ -1,183 +0,0 @@ -# Firestore Web SDK Usage Guide - -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 (`setDoc`) -Creates a document if it doesn't exist, or overwrites it if it does. - -```javascript -import { doc, setDoc } from "firebase/firestore"; - -// Create/Overwrite document with ID "LA" -await setDoc(doc(db, "cities", "LA"), { - name: "Los Angeles", - state: "CA", - country: "USA" -}); - -// To merge with existing data instead of overwriting: -await setDoc(doc(db, "cities", "LA"), { population: 3900000 }, { merge: true }); -``` - -### Add a Document with Auto-ID (`addDoc`) -Use when you don't care about the document ID. - -```javascript -import { collection, addDoc } from "firebase/firestore"; - -const docRef = await addDoc(collection(db, "cities"), { - name: "Tokyo", - country: "Japan" -}); -console.log("Document written with ID: ", docRef.id); -``` - -### Update a Document (`updateDoc`) -Update some fields of an existing document without overwriting the entire document. Fails if the document doesn't exist. - -```javascript -import { doc, updateDoc } from "firebase/firestore"; - -const laRef = doc(db, "cities", "LA"); - -await updateDoc(laRef, { - capital: true -}); -``` - -### Transactions -Perform an atomic read-modify-write operation. - -```javascript -import { runTransaction, doc } from "firebase/firestore"; - -const sfDocRef = doc(db, "cities", "SF"); - -try { - await runTransaction(db, async (transaction) => { - const sfDoc = await transaction.get(sfDocRef); - if (!sfDoc.exists()) { - throw "Document does not exist!"; - } - - const newPopulation = sfDoc.data().population + 1; - transaction.update(sfDocRef, { population: newPopulation }); - }); - console.log("Transaction successfully committed!"); -} catch (e) { - console.log("Transaction failed: ", e); -} -``` - -## Reading Data - -### Get a Single Document (`getDoc`) - -```javascript -import { doc, getDoc } from "firebase/firestore"; - -const docRef = doc(db, "cities", "SF"); -const docSnap = await getDoc(docRef); - -if (docSnap.exists()) { - console.log("Document data:", docSnap.data()); -} else { - console.log("No such document!"); -} -``` - -### Get Multiple Documents (`getDocs`) -Fetches all documents in a query or collection once. - -```javascript -import { collection, getDocs } from "firebase/firestore"; - -const querySnapshot = await getDocs(collection(db, "cities")); -querySnapshot.forEach((doc) => { - // doc.data() is never undefined for query doc snapshots - console.log(doc.id, " => ", doc.data()); -}); -``` - -## Realtime Updates - -### Listen to a Document/Query (`onSnapshot`) - -```javascript -import { doc, onSnapshot } from "firebase/firestore"; - -const unsub = onSnapshot(doc(db, "cities", "SF"), (doc) => { - console.log("Current data: ", doc.data()); -}); - -// Stop listening -// unsub(); -``` - -### Handle Changes (Added/Modified/Removed) - -```javascript -import { collection, query, where, onSnapshot } from "firebase/firestore"; - -const q = query(collection(db, "cities"), where("state", "==", "CA")); -const unsubscribe = onSnapshot(q, (snapshot) => { - snapshot.docChanges().forEach((change) => { - if (change.type === "added") { - console.log("New city: ", change.doc.data()); - } - if (change.type === "modified") { - console.log("Modified city: ", change.doc.data()); - } - if (change.type === "removed") { - console.log("Removed city: ", change.doc.data()); - } - }); -}); -``` - -## Queries - -### Simple and Compound Queries -Use `query()` to combine filters. - -```javascript -import { collection, query, where, getDocs } from "firebase/firestore"; - -const citiesRef = collection(db, "cities"); - -// Simple equality -const q1 = query(citiesRef, where("state", "==", "CA")); - -// Compound (AND) -// Note: Requires an index if filtering on different fields -const q2 = query(citiesRef, where("state", "==", "CA"), where("population", ">", 1000000)); -``` - -### Order and Limit -Sort and limit results. - -```javascript -import { orderBy, limit } from "firebase/firestore"; - -const q = query(citiesRef, orderBy("name"), limit(3)); -``` diff --git a/.agents/skills/firebase-hosting-basics/SKILL.md b/.agents/skills/firebase-hosting-basics/SKILL.md deleted file mode 100644 index a27e60d..0000000 --- a/.agents/skills/firebase-hosting-basics/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -name: firebase-hosting-basics -description: Skill for working with Firebase Hosting (Classic). Use this when you want to deploy static web apps, Single Page Apps (SPAs), or simple microservices. Do NOT use for Firebase App Hosting. ---- - -# hosting-basics - -This skill provides instructions and references for working with Firebase Hosting, a fast and secure hosting service for your web app, static and dynamic content, and microservices. - -## Overview - -Firebase Hosting provides production-grade web content hosting for developers. With a single command, you can deploy web apps and serve both static and dynamic content to a global CDN (content delivery network). - -**Key Features:** -- **Fast Content Delivery:** Files are cached on SSDs at CDN edges around the world. -- **Secure by Default:** Zero-configuration SSL is built-in. -- **Preview Channels:** View and test changes on temporary preview URLs before deploying live. -- **GitHub Integration:** Automate previews and deploys with GitHub Actions. -- **Dynamic Content:** Serve dynamic content and microservices using Cloud Functions or Cloud Run. - -## Hosting vs App Hosting - -**Choose Firebase Hosting if:** -- You are deploying a static site (HTML/CSS/JS). -- You are deploying a simple SPA (React, Vue, etc. without SSR). -- You want full control over the build and deploy process via CLI. - -**Choose Firebase App Hosting if:** -- You are using a supported full-stack framework like Next.js or Angular. -- You need Server-Side Rendering (SSR) or ISR. -- You want an automated "git push to deploy" workflow with zero configuration. - -## Instructions - -### 1. Configuration (`firebase.json`) -For details on configuring Hosting behavior, including public directories, redirects, rewrites, and headers, see [configuration.md](references/configuration.md). - -### 2. Deploying -For instructions on deploying your site, using preview channels, and managing releases, see [deploying.md](references/deploying.md). - -### 3. Emulation -To test your app locally: -```bash -firebase emulators:start --only hosting -``` -This serves your app at `http://localhost:5000` by default. diff --git a/.agents/skills/firebase-hosting-basics/references/configuration.md b/.agents/skills/firebase-hosting-basics/references/configuration.md deleted file mode 100644 index adb9050..0000000 --- a/.agents/skills/firebase-hosting-basics/references/configuration.md +++ /dev/null @@ -1,101 +0,0 @@ -# Hosting Configuration (`firebase.json`) - -The `hosting` section of `firebase.json` configures how your site is deployed and served. - -## Key Attributes - -### `public` (Required) -Specifies the directory to deploy to Firebase Hosting. -```json -"hosting": { - "public": "public" -} -``` - -### `ignore` (Optional) -Files to ignore on deploy. Uses glob patterns (like `.gitignore`). -**Default ignores:** `firebase.json`, `**/.*`, `**/node_modules/**` - -### `redirects` (Optional) -URL redirects to prevent broken links or shorten URLs. -```json -"redirects": [ - { - "source": "/foo", - "destination": "/bar", - "type": 301 - } -] -``` - -### `rewrites` (Optional) -Serve the same content for multiple URLs, useful for SPAs or Dynamic Content. -```json -"rewrites": [ - { - "source": "**", - "destination": "/index.html" - }, - { - "source": "/api/**", - "function": "apiFunction" - }, - { - "source": "/container/**", - "run": { - "serviceId": "helloworld", - "region": "us-central1" - } - } -] -``` - -### `headers` (Optional) -Custom response headers. -```json -"headers": [ - { - "source": "**/*.@(eot|otf|ttf|ttc|woff|font.css)", - "headers": [ - { - "key": "Access-Control-Allow-Origin", - "value": "*" - } - ] - } -] -``` - -### `cleanUrls` (Optional) -If `true`, drops `.html` extension from URLs. -```json -"cleanUrls": true -``` - -### `trailingSlash` (Optional) -Controls trailing slashes in static content URLs. -- `true`: Adds trailing slash. -- `false`: Removes trailing slash. - -## Full Example - -```json -{ - "hosting": { - "public": "dist", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ], - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - } - ], - "cleanUrls": true, - "trailingSlash": false - } -} -``` diff --git a/.agents/skills/firebase-hosting-basics/references/deploying.md b/.agents/skills/firebase-hosting-basics/references/deploying.md deleted file mode 100644 index 9e4eb86..0000000 --- a/.agents/skills/firebase-hosting-basics/references/deploying.md +++ /dev/null @@ -1,39 +0,0 @@ -# Deploying to Firebase Hosting - -## Standard Deployment -To deploy your Hosting content and configuration to your live site: - -```bash -firebase deploy --only hosting -``` - -This deploys to your default sites (`PROJECT_ID.web.app` and `PROJECT_ID.firebaseapp.com`). - -## Preview Channels -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 -``` -Replace `CHANNEL_ID` with a name (e.g., `feature-beta`). -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 -``` - -## 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 -``` - -**Example:** -Clone the `feature-beta` channel on your default site to live: -```bash -firebase hosting:clone my-project:feature-beta my-project:live -``` diff --git a/.gitignore b/.gitignore index 676babc..87f7bb0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ lib .firebaserc .DS_Store coverage -firebase-debug.log +*-debug.log functions/node_modules functions/lib functions/coverage diff --git a/firestore-debug.log b/firestore-debug.log deleted file mode 100644 index f64c39a..0000000 --- a/firestore-debug.log +++ /dev/null @@ -1,16 +0,0 @@ -Jun 01, 2026 8:59:26 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketServer start -INFO: Started WebSocket server on ws://127.0.0.1:9150 -API endpoint: http://127.0.0.1:8080 -If you are using a library that supports the FIRESTORE_EMULATOR_HOST environment variable, run: - - export FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 - -If you are running a Firestore in Datastore Mode project, run: - - export DATASTORE_EMULATOR_HOST=127.0.0.1:8080 - -Note: Support for Datastore Mode is in preview. If you encounter any bugs please file at https://github.com/firebase/firebase-tools/issues. -Dev App Server is now running. - -*** shutting down gRPC server since JVM is shutting down -*** server shut down diff --git a/skills-lock.json b/skills-lock.json deleted file mode 100644 index 804779a..0000000 --- a/skills-lock.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "version": 1, - "skills": { - "firebase-ai-logic": { - "source": "firebase/agent-skills", - "sourceType": "github", - "computedHash": "05c2527b8d943ebaef10de21155fb95b90e8d88f7ddac5ea9ffd8cf78f0c7ffb" - }, - "firebase-app-hosting-basics": { - "source": "firebase/agent-skills", - "sourceType": "github", - "computedHash": "dec34a68bb14bdf14137938fe946309c44630216060bcd1e20a1ed1ff51496a1" - }, - "firebase-auth-basics": { - "source": "firebase/agent-skills", - "sourceType": "github", - "computedHash": "6e5d03858be7f2a55ceef29d2b74100db1813393c262562960e39f1f7fae5f3b" - }, - "firebase-basics": { - "source": "firebase/agent-skills", - "sourceType": "github", - "computedHash": "75d1c711575183510c31047ffc2608f8c3cb31ce4af63bcbf32e44b998eb62b3" - }, - "firebase-data-connect": { - "source": "firebase/agent-skills", - "sourceType": "github", - "computedHash": "9ca50625fd96c08ce12958c6bee79e25dbbee278626a6032914f5dcf5778c454" - }, - "firebase-firestore-basics": { - "source": "firebase/agent-skills", - "sourceType": "github", - "computedHash": "e7e21f72b693e8eba6797e0799e1f9d96ec9f73669cb741a7ca2e541fa460b55" - }, - "firebase-hosting-basics": { - "source": "firebase/agent-skills", - "sourceType": "github", - "computedHash": "f495d67cd8c131deeadaf8c03623537b2ac4e0e73bba3163678eaa0b46d87bd5" - } - } -}