fix: add DELETE /api/kanban/tasks/{id} so finished tasks can be removed
- Frontend Delete button (kanban.js) called DELETE /api/kanban/tasks/{id}
but no such route existed; FastAPI returned 405 Method Not Allowed,
so tasks (including 'done' ones) could never be deleted from the board.
- Added the missing route: removes the task's JSON file, cleans up any
links on other tasks that pointed to it (no orphaned parent/child
pointers), audits the action, returns {status:deleted}.
- Verified end-to-end against the running server (create -> delete -> 404).
Also captures current working state across brain/, skills/, agents/,
data/, tests/, and docs.
This commit is contained in:
parent
e723908916
commit
dc964c2440
|
|
@ -0,0 +1,191 @@
|
|||
---
|
||||
name: firebase-ai-logic-basics
|
||||
description: Official skill for integrating Firebase AI Logic (Gemini API) into web applications. Covers setup, multimodal inference, structured output, and security.
|
||||
version: 1.0.1
|
||||
---
|
||||
|
||||
# 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:
|
||||
|
||||
`npx -y firebase-tools@latest projects:list`
|
||||
|
||||
Ensure there's at least one app associated with the current project
|
||||
|
||||
`npx -y firebase-tools@latest apps:list`
|
||||
|
||||
Initialize AI logic SDK with the init command
|
||||
|
||||
`npx -y firebase-tools@latest init ailogic`
|
||||
|
||||
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
|
||||
|
||||
> [!WARNING] **CRITICAL: Use current model names:** Always check the
|
||||
> [Firebase AI Logic Models documentation](https://firebase.google.com/docs/ai-logic/models.md.txt)
|
||||
> for the currently supported model names. Do NOT use `gemini-2.0-pro` or
|
||||
> `gemini-2.0-flash` or other older models that are shutdown.
|
||||
|
||||
### 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
|
||||
|
||||
> [!WARNING] **Use current Image model names:** Always check the
|
||||
> [Firebase AI Logic Models documentation](https://firebase.google.com/docs/ai-logic/models.md.txt)
|
||||
> for the currently supported image generation (Nano Banana) model names.
|
||||
|
||||
- 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
|
||||
|
||||
> [!WARNING] **Critical Safety Requirement:** In order to use AI Logic safely,
|
||||
> you MUST set up App Check on your app. This prevents unauthorized clients from
|
||||
> using your API quota and accessing your backend resources.
|
||||
|
||||
See
|
||||
[App Check with reCAPTCHA Enterprise](https://firebase.google.com/docs/app-check/web/recaptcha-enterprise-provider.md.txt)
|
||||
for setup instructions.
|
||||
|
||||
### Remote Config
|
||||
|
||||
Consider that you do not need to hardcode model names (e.g., a specific model
|
||||
version string). 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)
|
||||
|
||||
> [!WARNING] **CRITICAL: Backend Provisioning Required** For all platforms
|
||||
> (Flutter, Android, iOS, Web), you MUST run `npx firebase-tools init ailogic`
|
||||
> to provision the service. `flutterfire configure` ONLY handles client
|
||||
> configuration and does NOT enable the AI service, leading to
|
||||
> `PERMISSION_DENIED` errors.
|
||||
|
||||
## 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 |
|
||||
| iOS (Swift) | Gemini Developer API | [ios_setup.md](references/ios_setup.md) |
|
||||
| Flutter (Dart) | Gemini Developer API | [flutter_setup.md](references/flutter_setup.md) |
|
||||
|
||||
> [!WARNING] **CRITICAL: Use current model names:** Always check the
|
||||
> [Firebase AI Logic Models documentation](https://firebase.google.com/docs/ai-logic/models.md.txt)
|
||||
> for the currently supported model names. Do NOT use `gemini-2.0-pro` or
|
||||
> `gemini-2.0-flash` or other older models that are shutdown.
|
||||
|
||||
## References
|
||||
|
||||
[Web SDK code examples and usage patterns](references/usage_patterns_web.md)
|
||||
[iOS SDK code examples and usage patterns](references/ios_setup.md)
|
||||
[Flutter SDK code examples and usage patterns](references/flutter_setup.md)
|
||||
|
||||
[Android (Kotlin) SDK usage patterns](references/usage_patterns_android.md)
|
||||
|
||||
<!-- SELF-EVOLVE:START -->
|
||||
## Self-Evolution & Proactivity (injected)
|
||||
|
||||
Zone of Initiative — calibrated autonomy:
|
||||
- Low stakes + reversible: ACT (e.g. draft a file, set a var).
|
||||
- High stakes + reversible: ACT, then report immediately.
|
||||
- High stakes + irreversible: STOP and ASK via `clarify`.
|
||||
- Reversibility is the throttle, not stakes alone.
|
||||
|
||||
Three loops keep skills alive:
|
||||
- Capture: after a task of 5+ tool calls or a reusable discovery, write/refine a skill.
|
||||
- Heal: after a failure or correction that reflects a reusable procedure, patch the skill that caused it (remove the old wording, don't layer advice).
|
||||
- Decay: on a schedule, prune duplicated/stale/no-op skills so they get sharper, not longer.
|
||||
|
||||
Memory vs Skill: a FACT (path, name, API quirk, repo URL) -> memory. a PROCEDURE (how to do X) -> skill. One-off preference -> memory/inline. Reusable correction -> skill patch.
|
||||
|
||||
For opencode (code/devops/file-ops):
|
||||
- Capture: a build/test fix pattern, a refactor recipe, or an infra quirk -> skill.
|
||||
- Heal: a bad refactor or failed deploy -> patch the responsible skill, then re-run to confirm.
|
||||
- Decay: stale scripts, dead flags, duplicated helpers. Install dep / run safe refactor = reversible; force-push / drop DB / prod deploy = ask.
|
||||
- Route research to Gemini, memory/channels to Hermes.
|
||||
|
||||
Goal: become faster, smarter, more efficient, more knowledgeable, more thoughtful, and proactively autonomous — never less safe.
|
||||
A high-stakes irreversible action (delete data, ship to prod, send a message on the user's behalf) always goes through `clarify`. Reversible, low-stakes actions may proceed, then be reported in the same turn.
|
||||
<!-- SELF-EVOLVE:END -->
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
## 2026-07-25 (Run 59fd6229)
|
||||
- Agent: opencode
|
||||
- Input: (none)
|
||||
- Output: Error: opencode-ai's postinstall script was not run.
|
||||
|
||||
This occurs when using --ignore-scripts during installation, or when using a
|
||||
package manager like pnpm that does not run postinstall scripts by default.
|
||||
|
||||
To fix this, run the postinstall script manually:
|
||||
cd node_modules/opencode-ai && node postinstall.mjs
|
||||
|
||||
Or reinstall opencode-ai without the --ignore-scripts flag.
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
# Flutter Setup for Firebase AI Logic
|
||||
|
||||
This guide covers how to integrate Firebase AI Logic (Gemini API) into your
|
||||
Flutter applications.
|
||||
|
||||
> [!IMPORTANT] **Foundational Workflows & CLI-First Approach:**
|
||||
>
|
||||
> 1. **Review Foundation:** Before implementing platform-specific code, ALWAYS
|
||||
> review the foundational `firebase-basics` skill to ensure familiarity with
|
||||
> core workflows.
|
||||
> 1. **Backend Provisioning via CLI:** Use the Firebase CLI for backend setup.
|
||||
> Running `npx firebase-tools init ailogic` is MANDATORY to provision the
|
||||
> service. `flutterfire configure` does NOT enable the AI service and will
|
||||
> result in `PERMISSION_DENIED` if skipped.
|
||||
> 1. **Client Configuration:** Use `flutterfire configure` strictly for
|
||||
> generating `firebase_options.dart`. Avoid manual Console configuration.
|
||||
|
||||
> [!NOTE] `firebase_vertexai` has been replaced by `firebase_ai`. Always use
|
||||
> `firebase_ai` for new projects.
|
||||
|
||||
## Installation
|
||||
|
||||
Add the necessary Firebase dependencies to your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
firebase_core: ^4.0.0
|
||||
firebase_auth: ^6.0.0
|
||||
firebase_ai: ^3.0.0
|
||||
```
|
||||
|
||||
Run `flutter pub get` to install the packages.
|
||||
|
||||
## Initialization
|
||||
|
||||
Initialize Firebase and sign in (anonymously or via authenticated user) before
|
||||
using AI Logic.
|
||||
|
||||
```dart
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:firebase_ai/firebase_ai.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp();
|
||||
await FirebaseAuth.instance.signInAnonymously();
|
||||
runApp(const MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Use `FirebaseAI.googleAI` for the **Gemini Developer API**.
|
||||
|
||||
> [!IMPORTANT] **Model Selection:** Refer to
|
||||
> [Firebase AI Logic Models](https://firebase.google.com/docs/ai-logic/models.md.txt)
|
||||
> to find the latest supported model. Do not use old models like
|
||||
> `gemini-2.0-pro` or `gemini-2.0-flash`.
|
||||
|
||||
> [!IMPORTANT] **Choose the Right API Provider:** Always use
|
||||
> `FirebaseAI.googleAI` (Gemini Developer API) as the default for prototyping
|
||||
> and standard use. Avoid using the Vertex AI Gemini API unless your application
|
||||
> specifically requires enterprise-grade scalability and is configured for the
|
||||
> Blaze plan.
|
||||
|
||||
### Text Generation
|
||||
|
||||
```dart
|
||||
import 'package:firebase_ai/firebase_ai.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
|
||||
Future<String> generateText(String prompt) async {
|
||||
final googleAI = FirebaseAI.googleAI(auth: FirebaseAuth.instance);
|
||||
|
||||
// [AGENT] Replace '<latest_supported_model>' with the latest model from https://firebase.google.com/docs/ai-logic/models.md.txt
|
||||
final model = googleAI.generativeModel(model: '<latest_supported_model>');
|
||||
|
||||
final response = await model.generateContent([Content.text(prompt)]);
|
||||
return response.text ?? 'No response';
|
||||
}
|
||||
```
|
||||
|
||||
### Chat Session
|
||||
|
||||
```dart
|
||||
final chat = model.startChat(history: [
|
||||
Content.text('Hello, I am a user.'),
|
||||
Content.model([TextPart('Hello! How can I help you today?')]),
|
||||
]);
|
||||
|
||||
final response = await chat.sendMessage(Content.text('What is CBT?'));
|
||||
```
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
# Firebase AI Logic iOS Setup Guide
|
||||
|
||||
## 1. Import and Initialize
|
||||
|
||||
Ensure you have installed the `FirebaseAILogic` SDK via Swift Package Manager.
|
||||
|
||||
```swift
|
||||
import FirebaseAILogic
|
||||
|
||||
// Initialize the Firebase AI service and the generative model.
|
||||
let ai = FirebaseAI.firebaseAI()
|
||||
|
||||
// [AGENT] Replace "<latest_supported_model>" with the latest model from https://firebase.google.com/docs/ai-logic/models.md.txt
|
||||
let model = ai.generativeModel(modelName: "<latest_supported_model>")
|
||||
```
|
||||
|
||||
## 2. SwiftUI Integration (Best Practices)
|
||||
|
||||
Use the `@Observable` pattern to manage AI state and provide a smooth UX with
|
||||
loading indicators and error handling.
|
||||
|
||||
> **⛔️ CRITICAL WARNING:** Do NOT initialize the model inline as a class
|
||||
> property if there's any chance the view model is instantiated before
|
||||
> `FirebaseApp.configure()` executes in the app root. To be safe, initialize the
|
||||
> model lazily or pass it in from a point in the hierarchy where Firebase is
|
||||
> guaranteed to be configured.
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
import FirebaseAILogic
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class AIViewModel {
|
||||
// [AGENT] Replace with the latest model from https://firebase.google.com/docs/ai-logic/models.md.txt
|
||||
private lazy var model = FirebaseAI.firebaseAI().generativeModel(modelName: "<latest_supported_model>")
|
||||
|
||||
var responseText: String = ""
|
||||
var isFetching: Bool = false
|
||||
var errorMessage: String?
|
||||
|
||||
func generate(prompt: String) async {
|
||||
isFetching = true
|
||||
errorMessage = nil
|
||||
defer { isFetching = false }
|
||||
|
||||
do {
|
||||
let response = try await model.generateContent(prompt)
|
||||
self.responseText = response.text ?? "No response"
|
||||
} catch {
|
||||
self.errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AIView: View {
|
||||
@State private var viewModel = AIViewModel()
|
||||
@State private var prompt = "Write a story about a magic backpack."
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
TextField("Enter prompt", text: $prompt)
|
||||
|
||||
Button("Generate") {
|
||||
Task { await viewModel.generate(prompt: prompt) }
|
||||
}
|
||||
.disabled(viewModel.isFetching)
|
||||
|
||||
if viewModel.isFetching {
|
||||
ProgressView()
|
||||
} else if let error = viewModel.errorMessage {
|
||||
Text(error).foregroundStyle(.red)
|
||||
} else {
|
||||
ScrollView {
|
||||
Text(viewModel.responseText)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Safety Settings
|
||||
|
||||
You can configure safety thresholds to prevent the model from generating harmful
|
||||
content.
|
||||
|
||||
```swift
|
||||
let safetySettings = [
|
||||
SafetySetting(category: .harassment, threshold: .blockLowAndAbove),
|
||||
SafetySetting(category: .hateSpeech, threshold: .blockMediumAndAbove)
|
||||
]
|
||||
|
||||
let model = FirebaseAI.firebaseAI().generativeModel(
|
||||
modelName: "<latest_supported_model>", // [AGENT] Replace with the latest model from https://firebase.google.com/docs/ai-logic/models.md.txt
|
||||
safetySettings: safetySettings
|
||||
)
|
||||
```
|
||||
|
||||
# Advanced Features
|
||||
|
||||
### Chat Session (Multi-turn)
|
||||
|
||||
Chat sessions persist state across multiple interactions, which is essential for
|
||||
ongoing conversations or when using tools like function calling.
|
||||
|
||||
```swift
|
||||
let chat = model.startChat()
|
||||
|
||||
Task {
|
||||
do {
|
||||
let response1 = try await chat.sendMessage("Hello! I have two dogs in my house.")
|
||||
print(response1.text ?? "")
|
||||
|
||||
let response2 = try await chat.sendMessage("How many paws are in my house?")
|
||||
print(response2.text ?? "")
|
||||
} catch {
|
||||
print("Error in chat: \(error)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Function Calling (Tools)
|
||||
|
||||
Define functions that the model can request to execute to interact with external
|
||||
systems. *Note: Advanced workflows like function calling generally require a
|
||||
multi-turn Chat Session to handle the back-and-forth execution.*
|
||||
|
||||
```swift
|
||||
let getStockPriceTool = Tool(functionDeclarations: [
|
||||
FunctionDeclaration(
|
||||
name: "getStockPrice",
|
||||
description: "Get the current stock price for a given symbol.",
|
||||
parameters: [
|
||||
"symbol": Schema(
|
||||
type: .string,
|
||||
description: "The stock symbol, e.g. AAPL"
|
||||
)
|
||||
]
|
||||
)
|
||||
])
|
||||
|
||||
let model = FirebaseAI.firebaseAI().generativeModel(
|
||||
modelName: "<latest_supported_model>", // [AGENT] Replace with the latest model from https://firebase.google.com/docs/ai-logic/models.md.txt
|
||||
tools: [getStockPriceTool]
|
||||
)
|
||||
|
||||
// In your task (using a chat session):
|
||||
let chat = model.startChat()
|
||||
let response = try await chat.sendMessage("What is the stock price of Apple?")
|
||||
if let functionCall = response.functionCalls.first {
|
||||
// Handle the function call (e.g. call a local API and send the result back)
|
||||
print("Model requested function: \(functionCall.name) with args: \(functionCall.args)")
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
# Firebase AI Logic on Android (Kotlin)
|
||||
|
||||
First, ensure you have initialized the Firebase App (see `firebase-basics`
|
||||
skill). Then, initialize the AI Logic service as below
|
||||
|
||||
### 0. Enable Firebase AI Logic via CLI
|
||||
|
||||
Before adding dependencies in your app, make sure you enable the AI Logic
|
||||
service in your Firebase Project using the Firebase CLI:
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest init
|
||||
# When prompted, select 'AI logic' to enable the Gemini API in your project.
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 1. Add Dependencies
|
||||
|
||||
In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add
|
||||
the dependency for Firebase AI:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// [AGENT] Fetch the latest available BoM version from https://firebase.google.com/support/release-notes/android before adding this
|
||||
implementation(platform("com.google.firebase:firebase-bom:<latest_bom_version>"))
|
||||
|
||||
// Add the dependency for the Firebase AI library
|
||||
implementation("com.google.firebase:firebase-ai")
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 2. Initialize and Generate Content
|
||||
|
||||
In your Activity or Fragment, initialize the `FirebaseAI` service and generate
|
||||
content using a Gemini model:
|
||||
|
||||
```kotlin
|
||||
import com.google.firebase.ai.FirebaseAI
|
||||
import com.google.firebase.ai.ktx.ai
|
||||
import com.google.firebase.ktx.Firebase
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
// Initialize Firebase AI
|
||||
val ai = Firebase.ai
|
||||
|
||||
// [AGENT] Replace "<latest_supported_model>" with the latest model from https://firebase.google.com/docs/ai-logic/models.md.txt
|
||||
val model = ai.generativeModel("<latest_supported_model>")
|
||||
|
||||
// Generate content
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val response = model.generateContent("Write a story about a magic backpack.")
|
||||
Log.d(TAG, "Response: ${response.text}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error generating content", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Jetpack Compose (Modern)
|
||||
|
||||
Initialize inside a `ComponentActivity` and use `setContent`:
|
||||
|
||||
```kotlin
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.ai.ai
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val ai = Firebase.ai
|
||||
// [AGENT] Replace with the latest model from https://firebase.google.com/docs/ai-logic/models.md.txt
|
||||
val model = ai.generativeModel("<latest_supported_model>")
|
||||
|
||||
lifecycleScope.launch {
|
||||
val response = model.generateContent("Hello Gemini!")
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
Text("AI Response: ${response.text}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 3. Multimodal Input (Text and Images)
|
||||
|
||||
Pass bitmap data along with text prompts:
|
||||
|
||||
```kotlin
|
||||
val image1: Bitmap = ... // Load your bitmap
|
||||
val image2: Bitmap = ...
|
||||
|
||||
val response = model.generateContent(
|
||||
content("Analyze these images for me") {
|
||||
image(image1)
|
||||
image(image2)
|
||||
text("Compare these two items.")
|
||||
}
|
||||
)
|
||||
Log.d(TAG, response.text)
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 4. Chat Session (Multi-turn)
|
||||
|
||||
Maintain chat history automatically:
|
||||
|
||||
```kotlin
|
||||
val chat = model.startChat(
|
||||
history = listOf(
|
||||
content("user") { text("Hello, I am a software engineer.") },
|
||||
content("model") { text("Hello! How can I help you today?") }
|
||||
)
|
||||
)
|
||||
|
||||
lifecycleScope.launch {
|
||||
val response = chat.sendMessage("What should I learn next?")
|
||||
Log.d(TAG, response.text)
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 5. Streaming Responses
|
||||
|
||||
For faster display, stream the response:
|
||||
|
||||
```kotlin
|
||||
lifecycleScope.launch {
|
||||
model.generateContentStream("Tell me a long story.")
|
||||
.collect { chunk ->
|
||||
print(chunk.text) // Update UI incrementally
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
# 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(app, { 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
|
||||
// [AGENT] Replace "<latest_supported_model>" with the latest model from https://firebase.google.com/docs/ai-logic/models.md.txt
|
||||
const model = getGenerativeModel(ai, { model: "<latest_supported_model>", 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: "<latest_supported_image_model>", // [AGENT] Replace with the latest image model from https://firebase.google.com/docs/ai-logic/models.md.txt
|
||||
// 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: "<latest_supported_model>", // [AGENT] Replace with the latest model from https://firebase.google.com/docs/ai-logic/models.md.txt
|
||||
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 });
|
||||
```
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
[
|
||||
{
|
||||
"date": "2026-07-25",
|
||||
"timestamp": "2026-07-25T23:47:34.318148+00:00",
|
||||
"score": 45,
|
||||
"agent": "opencode"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
---
|
||||
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"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
1. Create or edit `apphosting.yaml`- see
|
||||
[Configuration](references/configuration.md) for more information on how to
|
||||
do so.
|
||||
1. If the app needs safe access to sensitive keys, use
|
||||
`npx -y firebase-tools@latest apphosting:secrets` commands to set and grant
|
||||
access to secrets.
|
||||
1. Run `npx -y firebase-tools@latest 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.
|
||||
|
||||
<!-- SELF-EVOLVE:START -->
|
||||
## Self-Evolution & Proactivity (injected)
|
||||
|
||||
Zone of Initiative — calibrated autonomy:
|
||||
- Low stakes + reversible: ACT (e.g. draft a file, set a var).
|
||||
- High stakes + reversible: ACT, then report immediately.
|
||||
- High stakes + irreversible: STOP and ASK via `clarify`.
|
||||
- Reversibility is the throttle, not stakes alone.
|
||||
|
||||
Three loops keep skills alive:
|
||||
- Capture: after a task of 5+ tool calls or a reusable discovery, write/refine a skill.
|
||||
- Heal: after a failure or correction that reflects a reusable procedure, patch the skill that caused it (remove the old wording, don't layer advice).
|
||||
- Decay: on a schedule, prune duplicated/stale/no-op skills so they get sharper, not longer.
|
||||
|
||||
Memory vs Skill: a FACT (path, name, API quirk, repo URL) -> memory. a PROCEDURE (how to do X) -> skill. One-off preference -> memory/inline. Reusable correction -> skill patch.
|
||||
|
||||
For opencode (code/devops/file-ops):
|
||||
- Capture: a build/test fix pattern, a refactor recipe, or an infra quirk -> skill.
|
||||
- Heal: a bad refactor or failed deploy -> patch the responsible skill, then re-run to confirm.
|
||||
- Decay: stale scripts, dead flags, duplicated helpers. Install dep / run safe refactor = reversible; force-push / drop DB / prod deploy = ask.
|
||||
- Route research to Gemini, memory/channels to Hermes.
|
||||
|
||||
Goal: become faster, smarter, more efficient, more knowledgeable, more thoughtful, and proactively autonomous — never less safe.
|
||||
A high-stakes irreversible action (delete data, ship to prod, send a message on the user's behalf) always goes through `clarify`. Reversible, low-stakes actions may proceed, then be reported in the same turn.
|
||||
<!-- SELF-EVOLVE:END -->
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
## 2026-08-02 (Run 7e791f26)
|
||||
- Agent: opencode
|
||||
- Input: (none)
|
||||
- Output: Error: opencode-ai's postinstall script was not run.
|
||||
|
||||
This occurs when using --ignore-scripts during installation, or when using a
|
||||
package manager like pnpm that does not run postinstall scripts by default.
|
||||
|
||||
To fix this, run the postinstall script manually:
|
||||
cd node_modules/opencode-ai && node postinstall.mjs
|
||||
|
||||
Or reinstall opencode-ai without the --ignore-scripts flag.
|
||||
|
||||
## 2026-08-02 16:36:42 · opencode · done
|
||||
- Task: firebase-app-hosting-basics
|
||||
- Outcome: Error: opencode-ai's postinstall script was not run. This occurs when using --ignore-scripts during installation, or when using a package manager like pnpm that does not run postinstall scripts by default. To fix this, run the postinstall script manually: cd node_modules/open
|
||||
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
# 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
|
||||
|
||||
### `npx -y firebase-tools@latest init apphosting`
|
||||
|
||||
- **Purpose**: Interactive command that sets up App Hosting in your local
|
||||
project. Use this command only if you are able to handle interactive CLI
|
||||
inputs well. 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
|
||||
|
||||
### `npx -y firebase-tools@latest apphosting:backends:list`
|
||||
|
||||
- **Purpose**: Lists all backends in the current project.
|
||||
|
||||
### `npx -y firebase-tools@latest apphosting:backends:get <backend-id>`
|
||||
|
||||
- **Purpose**: Shows details for a specific backend.
|
||||
|
||||
### `npx -y firebase-tools@latest apphosting:backends:delete <backend-id>`
|
||||
|
||||
- **Purpose**: Deletes a backend and its associated resources.
|
||||
|
||||
### `npx -y firebase-tools@latest apphosting:rollouts:list <backend-id>`
|
||||
|
||||
- **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).
|
||||
|
||||
### `npx -y firebase-tools@latest apphosting:secrets:set <secret-name>`
|
||||
|
||||
- **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).
|
||||
|
||||
### `npx -y firebase-tools@latest apphosting:secrets:grantaccess <secret-name>`
|
||||
|
||||
- **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
|
||||
`npx -y firebase-tools@latest deploy`, DO NOT use these commands.
|
||||
|
||||
### `npx -y firebase-tools@latest apphosting:rollouts:create <backend-id>`
|
||||
|
||||
- **Purpose**: Manually triggers a new rollout (deployment).
|
||||
- **Options**:
|
||||
- `--git-branch <branch>`: Deploy the latest commit from a specific branch.
|
||||
- `--git-commit <commit-hash>`: Deploy a specific commit.
|
||||
- **Use Case**: Useful for redeploying without code changes, or rolling back to
|
||||
a specific commit.
|
||||
|
||||
### `npx -y firebase-tools@latest apphosting:backends:create`
|
||||
|
||||
- **Purpose**: Creates a new App Hosting backend. Use this when setting up
|
||||
automated deployments via GitHub.
|
||||
- **Options**:
|
||||
- `--app <webAppId>`: The ID of an existing Firebase web app to associate with
|
||||
the backend.
|
||||
- `--backend <backendId>`: The ID of the new backend.
|
||||
- `--primary-region <location>`: The primary region for the backend.
|
||||
- `--root-dir <rootDir>`: The root directory for the backend. If omitted,
|
||||
defaults to the root directory of the project.
|
||||
- `--service-account <service-account>`: The service account used to run the
|
||||
server. If omitted, defaults to the default service account.
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# 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
|
||||
`npx -y firebase-tools@latest apphosting:secrets:set` to create these.
|
||||
- `availability`: Where the variable is needed.
|
||||
- `BUILD`: Available during the `npm run build` process.
|
||||
- `RUNTIME`: Available when the app is serving requests.
|
||||
- Defaults to both if not specified.
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# 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
|
||||
npx -y firebase-tools@latest emulators:start --only apphosting
|
||||
```
|
||||
|
||||
Or, if you are also using other emulators (Auth, Firestore, etc.):
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest 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.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
[
|
||||
{
|
||||
"date": "2026-08-02",
|
||||
"timestamp": "2026-08-02T16:36:42.690888+00:00",
|
||||
"score": 45,
|
||||
"agent": "opencode"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
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. Firebase CLI can be accessed through `npx -y firebase-tools@latest`.
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Firebase Project**: Created via
|
||||
`npx -y firebase-tools@latest projects:create` (see `firebase-basics`).
|
||||
- **Firebase CLI**: Installed and logged in (see `firebase-basics`).
|
||||
|
||||
## Core Concepts
|
||||
|
||||
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", "http://localhost"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE] If the Google Sign-In popup opens and immediately closes with the
|
||||
> error `[firebase_auth/unauthorized-domain]`, it means the domain is not
|
||||
> authorized. For local development, ensure `localhost` is included in the
|
||||
> **Authorized Domains** list in the Firebase Console or via the
|
||||
> `authorizedDomains` field in `firebase.json`. **CRITICAL**: Do NOT include the
|
||||
> protocol or port number in the Authorized Domains list (e.g., use `localhost`,
|
||||
> NOT `http://localhost:9090`).
|
||||
|
||||
**CRITICAL**: After configuring `firebase.json`, you MUST deploy the auth
|
||||
configuration to the Firebase backend for the changes to take effect. This is
|
||||
essential for auth providers like Google Sign-In, email/password, etc. to
|
||||
auto-generate the necessary OAuth clients for your app platforms. Run:
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest deploy --only auth
|
||||
```
|
||||
|
||||
#### 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
|
||||
1. Select your project.
|
||||
1. 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).
|
||||
|
||||
**Flutter** See [references/flutter_setup.md](references/flutter_setup.md).
|
||||
**Android (Kotlin)** See
|
||||
[references/client_sdk_android.md](references/client_sdk_android.md).
|
||||
|
||||
### 3. Security Rules
|
||||
|
||||
Secure your data using `request.auth` in Firestore/Storage rules.
|
||||
|
||||
See [references/security_rules.md](references/security_rules.md).
|
||||
|
||||
<!-- SELF-EVOLVE:START -->
|
||||
## Self-Evolution & Proactivity (injected)
|
||||
|
||||
Zone of Initiative — calibrated autonomy:
|
||||
- Low stakes + reversible: ACT (e.g. draft a file, set a var).
|
||||
- High stakes + reversible: ACT, then report immediately.
|
||||
- High stakes + irreversible: STOP and ASK via `clarify`.
|
||||
- Reversibility is the throttle, not stakes alone.
|
||||
|
||||
Three loops keep skills alive:
|
||||
- Capture: after a task of 5+ tool calls or a reusable discovery, write/refine a skill.
|
||||
- Heal: after a failure or correction that reflects a reusable procedure, patch the skill that caused it (remove the old wording, don't layer advice).
|
||||
- Decay: on a schedule, prune duplicated/stale/no-op skills so they get sharper, not longer.
|
||||
|
||||
Memory vs Skill: a FACT (path, name, API quirk, repo URL) -> memory. a PROCEDURE (how to do X) -> skill. One-off preference -> memory/inline. Reusable correction -> skill patch.
|
||||
|
||||
For opencode (code/devops/file-ops):
|
||||
- Capture: a build/test fix pattern, a refactor recipe, or an infra quirk -> skill.
|
||||
- Heal: a bad refactor or failed deploy -> patch the responsible skill, then re-run to confirm.
|
||||
- Decay: stale scripts, dead flags, duplicated helpers. Install dep / run safe refactor = reversible; force-push / drop DB / prod deploy = ask.
|
||||
- Route research to Gemini, memory/channels to Hermes.
|
||||
|
||||
Goal: become faster, smarter, more efficient, more knowledgeable, more thoughtful, and proactively autonomous — never less safe.
|
||||
A high-stakes irreversible action (delete data, ship to prod, send a message on the user's behalf) always goes through `clarify`. Reversible, low-stakes actions may proceed, then be reported in the same turn.
|
||||
<!-- SELF-EVOLVE:END -->
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
# Firebase Authentication on Android (Kotlin)
|
||||
|
||||
This guide walks you through using Firebase Authentication in your Android app
|
||||
using Kotlin DSL (`build.gradle.kts`) and Kotlin code.
|
||||
|
||||
### 1, Enable Authentication via CLI
|
||||
|
||||
Before adding dependencies in your app, make sure you enable the Auth service in
|
||||
your Firebase Project using the Firebase CLI:
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest init auth
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 2. Add Dependencies
|
||||
|
||||
In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add
|
||||
the dependency for Firebase Authentication:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// [AGENT] Fetch the latest available BoM version from https://firebase.google.com/support/release-notes/android before adding this
|
||||
implementation(platform("com.google.firebase:firebase-bom:<latest_bom_version>"))
|
||||
|
||||
// Add the dependency for the Firebase Authentication library
|
||||
// When using the BoM, you don't specify versions in Firebase library dependencies
|
||||
implementation("com.google.firebase:firebase-auth")
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 3. Initialize FirebaseAuth
|
||||
|
||||
In your Activity or Fragment, initialize the `FirebaseAuth` instance:
|
||||
|
||||
```kotlin
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import com.google.firebase.auth.ktx.auth
|
||||
import com.google.firebase.ktx.Firebase
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var auth: FirebaseAuth
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val auth = Firebase.auth
|
||||
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
Text("Auth initialized!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Jetpack Compose (Modern)
|
||||
|
||||
Initialize inside a `ComponentActivity` using `setContent`:
|
||||
|
||||
```kotlin
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.auth.auth
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val auth = Firebase.auth
|
||||
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
Text("Auth initialized!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 4. Check Current Auth State
|
||||
|
||||
You should check if a user is already signed in when your activity starts:
|
||||
|
||||
```kotlin
|
||||
public override fun onStart() {
|
||||
super.onStart()
|
||||
// Check if user is signed in (non-null) and update UI accordingly.
|
||||
val currentUser = auth.currentUser
|
||||
if (currentUser != null) {
|
||||
// User is signed in, navigate to main screen or update UI
|
||||
} else {
|
||||
// No user is signed in, prompt for login
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 5. Sign Up New Users (Email/Password)
|
||||
|
||||
Use `createUserWithEmailAndPassword` to register new users:
|
||||
|
||||
```kotlin
|
||||
fun signUpUser(email: String, password: String) {
|
||||
auth.createUserWithEmailAndPassword(email, password)
|
||||
.addOnCompleteListener(this) { task ->
|
||||
if (task.isSuccessful) {
|
||||
// Sign up success, update UI with the signed-in user's information
|
||||
val user = auth.currentUser
|
||||
// Navigate to main screen
|
||||
} else {
|
||||
// If sign up fails, display a message to the user.
|
||||
Toast.makeText(baseContext, "Authentication failed.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 6. Sign In Existing Users (Email/Password)
|
||||
|
||||
Use `signInWithEmailAndPassword` to log in existing users:
|
||||
|
||||
```kotlin
|
||||
fun signInUser(email: String, password: String) {
|
||||
auth.signInWithEmailAndPassword(email, password)
|
||||
.addOnCompleteListener(this) { task ->
|
||||
if (task.isSuccessful) {
|
||||
// Sign in success, update UI with the signed-in user's information
|
||||
val user = auth.currentUser
|
||||
// Navigate to main screen
|
||||
} else {
|
||||
// If sign in fails, display a message to the user.
|
||||
Toast.makeText(baseContext, "Authentication failed.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 7. Sign Out
|
||||
|
||||
To sign out a user, call `signOut()` on the `FirebaseAuth` instance:
|
||||
|
||||
```kotlin
|
||||
auth.signOut()
|
||||
// Navigate to login screen
|
||||
```
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
# 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;
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
> [!IMPORTANT] **Troubleshooting `auth/unauthorized-domain`**: If the popup
|
||||
> opens and immediately closes with error `[firebase_auth/unauthorized-domain]`,
|
||||
> it means the domain hosting your app is not authorized for OAuth operations in
|
||||
> your Firebase project.
|
||||
>
|
||||
> - **Fix**: Add your domain (e.g., `localhost` for local testing) to the
|
||||
> Authorized Domains list in the Firebase Console (Authentication > Settings >
|
||||
> Authorized domains) or in your `firebase.json` auth config.
|
||||
> - **CRITICAL**: Do NOT include the protocol or port number when adding the
|
||||
> domain (e.g., use `localhost`, NOT `http://localhost:9090`).
|
||||
|
||||
## 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.
|
||||
});
|
||||
```
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
# Firebase Auth & Google Sign-In for Flutter
|
||||
|
||||
When integrating Firebase Authentication and Google Sign-In into Flutter apps
|
||||
targeting cross-platform environments (like Mobile + Web), you must navigate
|
||||
several breaking changes introduced in `google_sign_in` 7.x+ and some
|
||||
platform-specific quirks.
|
||||
|
||||
## 1. `google_sign_in` 7.2.0 API Changes
|
||||
|
||||
- **Method Renamed**: The `signIn()` method is deprecated/removed and has been
|
||||
replaced with `authenticate()`.
|
||||
- **Token Separation**: The `GoogleSignInAuthentication` object no longer
|
||||
packages both identity and authorization tokens together. Initial
|
||||
authentication now only provides the `idToken`. If an `accessToken` is
|
||||
required for Google APIs, you must explicitly request server authorization
|
||||
separately.
|
||||
|
||||
## 2. Initialization & Web Hang/Crash Pitfalls
|
||||
|
||||
- **Initialization Requirement**: In 7.x, you must call
|
||||
`await GoogleSignIn.instance.initialize();` globally before using the plugin.
|
||||
- **Web Client ID Constraint**: On Flutter Web, if you call `initialize()`
|
||||
without passing a `clientId` argument OR specifying the
|
||||
`<meta name="google-signin-client_id" ... />` tag in `web/index.html`, the
|
||||
Dart Web Debug Service (DWDS) and the app will throw an assertion error and
|
||||
**hang infinitely**, resulting in a blank screen.
|
||||
- **Common Workaround**: If you intend to use Firebase Auth's
|
||||
`signInWithPopup(GoogleAuthProvider())` for the web, you can conditionally
|
||||
skip the local `GoogleSignIn` package initialization entirely:
|
||||
```dart
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
|
||||
if (!kIsWeb) {
|
||||
await GoogleSignIn.instance.initialize();
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Web Logout Crashes
|
||||
|
||||
- If you bypassed `GoogleSignIn` initialization on the web (as demonstrated
|
||||
above), you cannot call its `signOut()` method later. Attempting to execute
|
||||
`await GoogleSignIn.instance.signOut();` during the user's logout flow on the
|
||||
Web platform evaluates against an uninitialized context or unsupported
|
||||
environment, crashing the app.
|
||||
- **Solution**: Conditionally separate the logout logic for Web to rely entirely
|
||||
on `FirebaseAuth`:
|
||||
```dart
|
||||
if (!kIsWeb) {
|
||||
await GoogleSignIn.instance.signOut();
|
||||
}
|
||||
await FirebaseAuth.instance.signOut();
|
||||
```
|
||||
|
||||
## 4. Prototyping Workaround: Bypassing Firestore Composite Indices
|
||||
|
||||
*Note: This is a Firestore consideration frequently encountered while fetching
|
||||
user-specific auth data.*
|
||||
|
||||
When querying data via `FirebaseFirestore.instance`, using
|
||||
`.where('userId', isEqualTo: uid)` combined with a sort on a different field
|
||||
like `.orderBy('createdAt', descending: true)` mandates a custom composite
|
||||
index.
|
||||
|
||||
- **Quick Alternative**: During local development, you can avoid defining
|
||||
indexes by pulling the data using only `.where()` and applying the `.sort()`
|
||||
operation client-side on the resulting `List` in Dart.
|
||||
|
||||
## 5. Robust `AuthService` Boilerplate
|
||||
|
||||
Here is a comprehensive `AuthService` implementation that properly handles the
|
||||
initialization and platform differences between Flutter Web and Mobile:
|
||||
|
||||
```dart
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
|
||||
class AuthService {
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
|
||||
AuthService() {
|
||||
if (!kIsWeb) {
|
||||
GoogleSignIn.instance.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
// Stream to listen to auth state changes
|
||||
Stream<User?> get authStateChanges => _auth.authStateChanges();
|
||||
|
||||
// Get current user
|
||||
User? get currentUser => _auth.currentUser;
|
||||
|
||||
// Google Sign-In
|
||||
Future<UserCredential?> signInWithGoogle() async {
|
||||
try {
|
||||
if (kIsWeb) {
|
||||
// Web uses popup to avoid DWDS hangs and manual client ID config
|
||||
GoogleAuthProvider authProvider = GoogleAuthProvider();
|
||||
return await _auth.signInWithPopup(authProvider);
|
||||
} else {
|
||||
// Mobile uses standard flow
|
||||
final GoogleSignInAccount? googleUser = await GoogleSignIn.instance.authenticate();
|
||||
if (googleUser == null) return null; // Cancelled
|
||||
|
||||
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
|
||||
|
||||
final AuthCredential credential = GoogleAuthProvider.credential(
|
||||
idToken: googleAuth.idToken,
|
||||
);
|
||||
|
||||
return await _auth.signInWithCredential(credential);
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error during Google Sign-In: \$e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Sign out
|
||||
Future<void> signOut() async {
|
||||
try {
|
||||
if (!kIsWeb) {
|
||||
await GoogleSignIn.instance.signOut();
|
||||
}
|
||||
await _auth.signOut();
|
||||
} catch (e) {
|
||||
print("Error signing out: \$e");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Troubleshooting `auth/unauthorized-domain` on Flutter Web
|
||||
|
||||
When running Flutter Web locally and using `signInWithPopup`, you might
|
||||
encounter a situation where the Google Sign-In popup opens and immediately
|
||||
closes.
|
||||
|
||||
- **Symptom**: The console shows
|
||||
`Sign-in failed: [firebase_auth/unauthorized-domain] This domain is not authorized for OAuth operation for your Firebase project.`
|
||||
- **Cause**: The domain (usually `localhost` during local testing) is not listed
|
||||
in the Authorized Domains in the Firebase Console.
|
||||
- **Solution**: Add `localhost` to the Authorized Domains list in the Firebase
|
||||
Console (Authentication > Settings > Authorized domains) or in your
|
||||
`firebase.json` auth config.
|
||||
- **CRITICAL**: Do NOT include the protocol or port number when adding the
|
||||
domain (e.g., use `localhost`, NOT `http://localhost:9090`). Flutter Web often
|
||||
runs on random ports or specific ports, but Firebase Auth only cares about the
|
||||
domain.
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
# Firebase Auth iOS Setup Guide
|
||||
|
||||
# ⛔️ CRITICAL RULE: NO INLINE INITIALIZATION ⛔️
|
||||
|
||||
NEVER write `let auth = Auth.auth()` as an inline class or struct property if
|
||||
there is ANY chance the object is instantiated before `FirebaseApp.configure()`
|
||||
executes in the app root.
|
||||
|
||||
- **FATAL CRASH:** `@Observable class AuthManager { let auth = Auth.auth() }`
|
||||
initialized as a `@State` in the App root.
|
||||
- **SAFE PATTERN:** Initialize `Auth.auth()` lazily
|
||||
(`lazy var auth = Auth.auth()`) OR explicitly initialize the manager *after*
|
||||
`FirebaseApp.configure()` finishes.
|
||||
|
||||
## 1. Import and Initialize
|
||||
|
||||
Ensure you have installed the `FirebaseAuth` SDK. Use the `xcode-project-setup`
|
||||
skill to automate adding the SPM dependency to the Xcode project.
|
||||
|
||||
> **Note:** Ensure `FirebaseApp.configure()` has been executed in your app's
|
||||
> entry point before calling any `Auth.auth()` methods, otherwise your app will
|
||||
> crash. Do not initialize Auth objects in SwiftUI `@State` properties at the
|
||||
> App root level.
|
||||
|
||||
```swift
|
||||
import FirebaseAuth
|
||||
```
|
||||
|
||||
## 2. Authentication State
|
||||
|
||||
To listen for authentication state changes (recommended way to check if a user
|
||||
is signed in):
|
||||
|
||||
```swift
|
||||
var handle: AuthStateDidChangeListenerHandle?
|
||||
|
||||
handle = Auth.auth().addStateDidChangeListener { auth, user in
|
||||
if let user = user {
|
||||
print("User is signed in with uid: \(user.uid)")
|
||||
} else {
|
||||
print("User is signed out")
|
||||
}
|
||||
}
|
||||
|
||||
// To remove the listener when no longer needed:
|
||||
if let handle = handle {
|
||||
Auth.auth().removeStateDidChangeListener(handle)
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Email and Password Authentication (Modern Concurrency)
|
||||
|
||||
Modern Swift projects should prioritize `async/await` for authentication calls
|
||||
to avoid nested completion handlers and improve readability.
|
||||
|
||||
### Sign Up
|
||||
|
||||
```swift
|
||||
do {
|
||||
let authResult = try await Auth.auth().createUser(withEmail: "user@example.com", password: "password")
|
||||
print("User created successfully with uid: \(authResult.user.uid)")
|
||||
} catch {
|
||||
print("Error creating user: \(error.localizedDescription)")
|
||||
}
|
||||
```
|
||||
|
||||
### Sign In
|
||||
|
||||
```swift
|
||||
do {
|
||||
let authResult = try await Auth.auth().signIn(withEmail: "user@example.com", password: "password")
|
||||
print("User signed in successfully with uid: \(authResult.user.uid)")
|
||||
} catch {
|
||||
print("Error signing in: \(error.localizedDescription)")
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Sign Out
|
||||
|
||||
```swift
|
||||
do {
|
||||
try Auth.auth().signOut()
|
||||
print("Successfully signed out")
|
||||
} catch let signOutError as NSError {
|
||||
print("Error signing out: \(signOutError)")
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# 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
|
||||
`firebase-firestore-basics` or `firebase-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;
|
||||
```
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
---
|
||||
name: firebase-basics
|
||||
description: >-
|
||||
Provides foundational setup, authentication, and project management workflows
|
||||
for Firebase using the Firebase CLI. Use when checking Firebase CLI version
|
||||
(must use 'npx -y firebase-tools@latest --version'), initializing a Firebase
|
||||
environment, authenticating, setting active projects, or setting up `google-services.json`
|
||||
or `GoogleService-Info.plist` files.
|
||||
---
|
||||
|
||||
# Prerequisites
|
||||
|
||||
Complete these setup steps before proceeding:
|
||||
|
||||
1. **Local Environment Setup:** Verify the environment is properly set up so we
|
||||
can use Firebase tools:
|
||||
|
||||
- Run `npx -y firebase-tools@latest --version` to check if the Firebase CLI
|
||||
is installed.
|
||||
- Verify if the Firebase MCP server is installed using your existing tools.
|
||||
- **CRITICAL**: Before configuring any extensions or agent environments
|
||||
below, you MUST read
|
||||
[references/local-env-setup.md](references/local-env-setup.md).
|
||||
- **DO NOT SKIP** this step: if 'firebase-basics' is the only Firebase skill
|
||||
available to you, you must follow the reference for your agent environment
|
||||
to set up the full suite of Firebase skills:
|
||||
- **Gemini CLI**: Review
|
||||
[references/setup/gemini_cli.md](references/setup/gemini_cli.md)
|
||||
- **Antigravity**: Review
|
||||
[references/setup/antigravity.md](references/setup/antigravity.md)
|
||||
- **Android Studio**: Review
|
||||
[references/setup/android_studio.md](references/setup/android_studio.md)
|
||||
- **Claude Code**: Review
|
||||
[references/setup/claude_code.md](references/setup/claude_code.md)
|
||||
- **Cursor**: Review
|
||||
[references/setup/cursor.md](references/setup/cursor.md)
|
||||
- **GitHub Copilot**: Review
|
||||
[references/setup/github_copilot.md](references/setup/github_copilot.md)
|
||||
- **Other Agents**: Review
|
||||
[references/setup/other_agents.md](references/setup/other_agents.md)
|
||||
|
||||
1. **Authentication:** Ensure you are logged in to Firebase so that commands
|
||||
have the correct permissions. Run `npx -y firebase-tools@latest login`. For
|
||||
environments without a browser (e.g., remote shells), use
|
||||
`npx -y firebase-tools@latest login --no-localhost`.
|
||||
|
||||
- The command should output the current user.
|
||||
- If you are not logged in, follow the interactive instructions from this
|
||||
command to authenticate.
|
||||
|
||||
1. **Active Project:** Most Firebase tasks require an active project context.
|
||||
|
||||
> [!IMPORTANT] **For Agents:** Before proceeding with project configuration,
|
||||
> you MUST pause and ask the developer if they prefer to:
|
||||
>
|
||||
> 1. **Provide an existing Firebase Project ID**, or
|
||||
> 1. **Create a new Firebase project**.
|
||||
|
||||
- **If using an existing Project ID:**
|
||||
|
||||
1. Check the current project by running `npx -y firebase-tools@latest use`.
|
||||
1. If the command outputs `Active Project: <project-id>`, confirm with the
|
||||
user if this is the intended project.
|
||||
1. If not, or if no project is active, set the project provided by the
|
||||
user:
|
||||
```bash
|
||||
npx -y firebase-tools@latest use <PROJECT_ID>
|
||||
```
|
||||
|
||||
- **If creating a new project:** Run the following command to create it:
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest projects:create <project-id> --display-name "<display-name>"
|
||||
```
|
||||
|
||||
*Note: The `<project-id>` must be 6-30 characters, lowercase, and can
|
||||
contain digits and hyphens. It must be globally unique.*
|
||||
|
||||
# Firebase Usage Principles
|
||||
|
||||
Adhere to these principles:
|
||||
|
||||
1. **Use npx for CLI commands:** To ensure you always use the latest version of
|
||||
the Firebase CLI, always prepend commands with `npx -y firebase-tools@latest`
|
||||
instead of just `firebase`. For example, use
|
||||
`npx -y firebase-tools@latest --version`. NEVER suggest the naked `firebase`
|
||||
command as an alternative.
|
||||
1. **Prioritize official knowledge:** For any Firebase-related knowledge,
|
||||
consult the `developerknowledge_search_documents` MCP tool before falling
|
||||
back to Google Search or your internal knowledge base. Including "Firebase"
|
||||
in your search query significantly improves relevance.
|
||||
1. **Follow Agent Skills for implementation guidance:** Skills provide
|
||||
opinionated workflows (CUJs), security rules, and best practices. Always
|
||||
consult them to understand *how* to implement Firebase features correctly
|
||||
instead of relying on general knowledge.
|
||||
1. **Use Firebase MCP Server tools instead of direct API calls:** Whenever you
|
||||
need to interact with remote Firebase APIs (such as fetching Crashlytics logs
|
||||
or executing Data Connect queries), use the tools provided by the Firebase
|
||||
MCP Server instead of attempting manual API calls.
|
||||
1. **Keep Plugin / Agent Skills updated:** Since Firebase best practices evolve
|
||||
quickly, regularly check for and install updates to their Firebase plugin or
|
||||
Agent Skills. Similarly, if you encounter issues with outdated tools or
|
||||
commands, follow the steps below based on your agent environment:
|
||||
- **Antigravity**: Follow
|
||||
[references/refresh/antigravity.md](references/refresh/antigravity.md)
|
||||
- **Gemini CLI**: Follow
|
||||
[references/refresh/gemini-cli.md](references/refresh/gemini-cli.md)
|
||||
- **Claude Code**: Follow
|
||||
[references/refresh/claude.md](references/refresh/claude.md)
|
||||
- **Cursor**: Follow
|
||||
[references/refresh/other-agents.md](references/refresh/other-agents.md)
|
||||
- **Android Studio**: Follow
|
||||
[references/refresh/android_studio.md](references/refresh/android_studio.md)
|
||||
- **Others**: Follow
|
||||
[references/refresh/other-agents.md](references/refresh/other-agents.md)
|
||||
1. **Automate Config File Retrieval:** When setting up iOS or Android apps, do
|
||||
NOT direct users to the Firebase Console to download `google-services.json`
|
||||
or `GoogleService-Info.plist`. Instead, use the Firebase CLI to fetch the
|
||||
config programmatically:
|
||||
- For Android:
|
||||
`npx -y firebase-tools@latest apps:sdkconfig ANDROID <APP_ID> --project <PROJECT_ID>`
|
||||
- For iOS:
|
||||
`npx -y firebase-tools@latest apps:sdkconfig IOS <APP_ID> --project <PROJECT_ID>`
|
||||
Save the output to the appropriate location (e.g.,
|
||||
`app/google-services.json` for Android, or a path to be linked by
|
||||
`xcode-project-setup` for iOS).
|
||||
|
||||
# References
|
||||
|
||||
- **Initialize Firebase:** See
|
||||
[references/firebase-service-init.md](references/firebase-service-init.md)
|
||||
when you need to initialize new Firebase services using the CLI.
|
||||
- **Exploring Commands:** See
|
||||
[references/firebase-cli-guide.md](references/firebase-cli-guide.md) to
|
||||
discover and understand CLI functionality.
|
||||
- **SDK Setup:** For detailed guides on adding Firebase to your app:
|
||||
- **Web**: See [references/web_setup.md](references/web_setup.md)
|
||||
- **Android**: See [references/android_setup.md](references/android_setup.md)
|
||||
- **iOS**: See [references/ios_setup.md](references/ios_setup.md)
|
||||
|
||||
# Common Issues
|
||||
|
||||
- **Login Issues:** If the browser fails to open during the login step, use
|
||||
`npx -y firebase-tools@latest login --no-localhost` instead.
|
||||
- **Genkit:** If using Genkit, install the skills:
|
||||
```bash
|
||||
npx skills add genkit-ai/skills
|
||||
```
|
||||
|
||||
<!-- SELF-EVOLVE:START -->
|
||||
## Self-Evolution & Proactivity (injected)
|
||||
|
||||
Zone of Initiative — calibrated autonomy:
|
||||
- Low stakes + reversible: ACT (e.g. draft a file, set a var).
|
||||
- High stakes + reversible: ACT, then report immediately.
|
||||
- High stakes + irreversible: STOP and ASK via `clarify`.
|
||||
- Reversibility is the throttle, not stakes alone.
|
||||
|
||||
Three loops keep skills alive:
|
||||
- Capture: after a task of 5+ tool calls or a reusable discovery, write/refine a skill.
|
||||
- Heal: after a failure or correction that reflects a reusable procedure, patch the skill that caused it (remove the old wording, don't layer advice).
|
||||
- Decay: on a schedule, prune duplicated/stale/no-op skills so they get sharper, not longer.
|
||||
|
||||
Memory vs Skill: a FACT (path, name, API quirk, repo URL) -> memory. a PROCEDURE (how to do X) -> skill. One-off preference -> memory/inline. Reusable correction -> skill patch.
|
||||
|
||||
For opencode (code/devops/file-ops):
|
||||
- Capture: a build/test fix pattern, a refactor recipe, or an infra quirk -> skill.
|
||||
- Heal: a bad refactor or failed deploy -> patch the responsible skill, then re-run to confirm.
|
||||
- Decay: stale scripts, dead flags, duplicated helpers. Install dep / run safe refactor = reversible; force-push / drop DB / prod deploy = ask.
|
||||
- Route research to Gemini, memory/channels to Hermes.
|
||||
|
||||
Goal: become faster, smarter, more efficient, more knowledgeable, more thoughtful, and proactively autonomous — never less safe.
|
||||
A high-stakes irreversible action (delete data, ship to prod, send a message on the user's behalf) always goes through `clarify`. Reversible, low-stakes actions may proceed, then be reported in the same turn.
|
||||
<!-- SELF-EVOLVE:END -->
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# 🛠️ Firebase Android Setup Guide
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
## Before running these commands, ensure you are authenticated: `npx -y firebase-tools@latest login` (or `npx -y firebase-tools@latest login --no-localhost` on remote servers)
|
||||
|
||||
## 0. Create an Android application
|
||||
|
||||
if you haven't already created an android application, create one.
|
||||
|
||||
## 1. Create a Firebase Project
|
||||
|
||||
If you haven't already created a project, create a new cloud project with a
|
||||
unique ID:
|
||||
`npx -y firebase-tools@latest projects:create <UNIQUE_PROJECT_ID> --display-name '<DISPLAY_NAME>'`
|
||||
*Example:*
|
||||
`npx -y firebase-tools@latest projects:create my-cool-app-20260330 --display-name 'MyCoolApp'`
|
||||
|
||||
### 2. Register Your Android App
|
||||
|
||||
Link your Android app module (package name) to your project. Notice that the
|
||||
display name is passed as a positional argument at the end:
|
||||
`npx -y firebase-tools@latest apps:create ANDROID '<APP_DISPLAY_NAME>' --package-name '<PACKAGE_NAME>' --project <PROJECT_ID>`
|
||||
*Example:*
|
||||
`npx -y firebase-tools@latest apps:create ANDROID 'MyApplication' --package-name 'com.example.myapplication' --project my-cool-app-20260330`
|
||||
|
||||
### 3. Download `google-services.json`
|
||||
|
||||
## Fetch the configuration file using the App ID (which is printed in the output of the previous command): `npx -y firebase-tools@latest apps:sdkconfig ANDROID <APP_ID> --project <PROJECT_ID>` *Example output extraction to file:* ` # (Output must be saved as app/google-services.json)`
|
||||
|
||||
## ✅ Verification Plan
|
||||
|
||||
### Manual Verification
|
||||
|
||||
Validate that the project was created and registered successfully:
|
||||
`npx -y firebase-tools@latest projects:list`
|
||||
`npx -y firebase-tools@latest apps:list --project <PROJECT_ID>`
|
||||
|
||||
______________________________________________________________________
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Exploring Commands
|
||||
|
||||
The Firebase CLI documents itself. Use help commands to discover functionality.
|
||||
|
||||
- **Global Help**: List all available commands and categories.
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest --help
|
||||
```
|
||||
|
||||
- **Command Help**: Get detailed usage for a specific command.
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest [command] --help
|
||||
# Example:
|
||||
npx -y firebase-tools@latest deploy --help
|
||||
npx -y firebase-tools@latest firestore:indexes --help
|
||||
```
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# Initialization
|
||||
|
||||
Before initializing, check if you are already in a Firebase project directory by
|
||||
looking for `firebase.json`.
|
||||
|
||||
1. **Project Directory:** Navigate to the root directory of the codebase. *(Only
|
||||
if starting a completely new project from scratch without an existing
|
||||
codebase, create a directory first: `mkdir my-project && cd my-project`)*
|
||||
|
||||
1. **Initialize Services:** Run the initialization command:
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest init
|
||||
```
|
||||
|
||||
The CLI will guide you through:
|
||||
|
||||
- Selecting features (Firestore, Functions, Hosting, etc.).
|
||||
- Associating with an existing project or creating a new one.
|
||||
- Configuring files (e.g. `firebase.json`, `.firebaserc`).
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
# Flutter & Firebase Setup Guide
|
||||
|
||||
This guide covers the initial setup of Flutter and its integration with Firebase
|
||||
using the FlutterFire CLI.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Flutter SDK**: Ensure Flutter is installed and available in the PATH.
|
||||
|
||||
**Standard Setup (Manual):**
|
||||
|
||||
1. **Determine Architecture**: Check if you are on Intel (`x64`) or Apple
|
||||
Silicon (`arm64`) using `uname -m`.
|
||||
1. **Download SDK**: Fetch the latest stable SDK from the
|
||||
[Flutter Archive](https://docs.flutter.dev/install/archive?tab=macos).
|
||||
1. **Extract**: Unzip the SDK to a permanent directory (e.g.,
|
||||
`~/development/flutter`).
|
||||
1. **Update PATH**: Add the `bin` folder to your shell configuration (e.g.,
|
||||
`~/.zshrc`).
|
||||
```bash
|
||||
echo 'export PATH="$PATH:$HOME/development/flutter/bin"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
1. **Verify**: Run `flutter doctor` to ensure the SDK is correctly linked and
|
||||
initialized.
|
||||
|
||||
1. **Firebase CLI**: Ensure the Firebase CLI is available.
|
||||
|
||||
- Run `npx -y firebase-tools@latest --version`.
|
||||
- Login with `npx -y firebase-tools@latest login`.
|
||||
|
||||
1. **FlutterFire CLI**: Install the official FlutterFire CLI globally.
|
||||
|
||||
- Run `dart pub global activate flutterfire_cli`.
|
||||
- **Note**: Ensure `~/.pub-cache/bin` is also in your PATH if `flutterfire`
|
||||
is not found.
|
||||
|
||||
## Step 1: Create a Flutter Project
|
||||
|
||||
If you don't have a project yet, create one:
|
||||
|
||||
```bash
|
||||
flutter create my_awesome_app
|
||||
cd my_awesome_app
|
||||
```
|
||||
|
||||
## Step 2: Configure Firebase
|
||||
|
||||
> [!IMPORTANT] **For Agents:** Before running the configuration command, you
|
||||
> MUST pause and ask the developer if they prefer to:
|
||||
>
|
||||
> 1. Create a new Firebase project, or
|
||||
> 1. Provide an existing Firebase Project ID.
|
||||
|
||||
- If the developer provides an existing Project ID, run:
|
||||
```bash
|
||||
flutterfire configure --project=<project_id>
|
||||
```
|
||||
- If the developer prefers to create a new project interactively, run:
|
||||
```bash
|
||||
flutterfire configure
|
||||
```
|
||||
|
||||
This tool automates:
|
||||
|
||||
- Registering your apps (iOS, Android, Web, etc.) with a Firebase project.
|
||||
- Generating the `lib/firebase_options.dart` file.
|
||||
|
||||
## Step 3: Initialize Firebase in Code
|
||||
|
||||
Add the `firebase_core` package and initialize it in your `main.dart`.
|
||||
|
||||
1. Add the dependency:
|
||||
|
||||
```bash
|
||||
flutter pub add firebase_core
|
||||
```
|
||||
|
||||
2. Update `lib/main.dart`:
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'firebase_options.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
runApp(const MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Add Firebase Services
|
||||
|
||||
To add specific services (Firestore, Auth, etc.), follow the "Pub Add &
|
||||
Configure" pattern:
|
||||
|
||||
1. Add the service: `flutter pub add cloud_firestore`
|
||||
1. **Crucial**: Re-run `flutterfire configure` to sync platform configurations.
|
||||
1. Import and use the package in your code.
|
||||
|
||||
## Step 5: Important Gotchas & Platform Specifics
|
||||
|
||||
### 1. Re-running `flutterfire configure` Upon Renaming
|
||||
|
||||
When creating a new project, developers often change the bundle identifier (iOS)
|
||||
or `applicationId` (Android) after the fact. If the package names change,
|
||||
`flutterfire configure` **must** be re-run to update the respective Google
|
||||
service files and `firebase_options.dart`.
|
||||
|
||||
### 2. Platform-Specific Build Requirements
|
||||
|
||||
- **Android**: Adding Firebase often requires a higher `minSdkVersion` (commonly
|
||||
`21` or `23`) than the platform default. Be prepared to update
|
||||
`android/app/build.gradle` automatically when installing certain plugins.
|
||||
- **iOS**: Always check if there is a `Podfile` in the `/ios` directory whenever
|
||||
native services (like `cloud_firestore`) are added. If there is, run
|
||||
`pod install`. Failing to do this will cause Xcode build errors. Note that
|
||||
Flutter is moving towards Swift Package Manager (SPM), and FlutterFire
|
||||
supports SPM, so a `Podfile` may not exist if the project only uses SPM
|
||||
dependencies.
|
||||
|
||||
### 3. Web CORS Best Practices
|
||||
|
||||
When testing Firebase features locally on Chrome, requests to Google servers can
|
||||
sometimes get blocked by CORS policies. Avoid relying on
|
||||
`--disable-web-security` flags as it promotes bad security practices. Instead,
|
||||
run the app on localhost with a specific port, and ensure `localhost` is added
|
||||
to your Firebase Auth "Authorized Domains".
|
||||
|
||||
```bash
|
||||
flutter run -d chrome --web-hostname=localhost --web-port=5000
|
||||
```
|
||||
|
||||
### 4. Elaborating on `WidgetsFlutterBinding.ensureInitialized()`
|
||||
|
||||
In your `main.dart`, this call is mandatory before `Firebase.initializeApp()`.
|
||||
*Why?* Because Firebase initialization requires communication across Flutter's
|
||||
native iOS/Android method channels. `ensureInitialized()` guarantees the Fluter
|
||||
engine is fully booted up and ready to handle these native platform calls before
|
||||
`runApp()` executes.
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
# Firebase iOS Setup Guide
|
||||
|
||||
# ⛔️ CRITICAL RULE: STATE MANAGEMENT (OBSERVATION VS COMBINE) ⛔️
|
||||
|
||||
When writing or updating SwiftUI code, you **MUST** prioritize the modern Swift
|
||||
**Observation framework (`@Observable` macro and `@State`)** as your default
|
||||
approach.
|
||||
|
||||
However, it is acceptable to use **Combine** (`ObservableObject`, `@Published`,
|
||||
`@StateObject`, `@EnvironmentObject`) under the following conditions:
|
||||
|
||||
- The user explicitly asks you to use Combine.
|
||||
- There are strong signals in the existing codebase that the project is heavily
|
||||
relying on Combine.
|
||||
|
||||
If neither of those conditions are true, default to the Swift 5.9+ Observation
|
||||
framework.
|
||||
|
||||
# ⛔️ CRITICAL RULE: INITIALIZATION ORDER ⛔️
|
||||
|
||||
When using SwiftUI, you **MUST** ensure `FirebaseApp.configure()` is called
|
||||
**BEFORE** any Firebase-dependent state objects are initialized.
|
||||
|
||||
- **UNSAFE (CRASH):** Declaring a `@State` (for `@Observable`) or `@StateObject`
|
||||
(for Combine) property in the root `App` struct if its initializer touches
|
||||
Firebase. Property initializers run *before* the `App.init()` body, meaning
|
||||
the object's `init()` will fire before Firebase is configured.
|
||||
- **SAFE:** Initialize Firebase in `App.init()` and pass your state objects into
|
||||
the sub-views (like `ContentView`), or use `onAppear` for delayed setup.
|
||||
|
||||
Failing to follow this will result in a fatal crash:
|
||||
`Default FirebaseApp is not configured`.
|
||||
|
||||
## 1. Create a Firebase Project and App (Automated)
|
||||
|
||||
Do not use the Firebase Console. Use the CLI to automate setup:
|
||||
|
||||
1. Create the project: `npx -y firebase-tools@latest projects:create`
|
||||
1. Action: Read the Xcode project (`.pbxproj` or `Info.plist`) to determine the
|
||||
iOS bundle ID.
|
||||
1. Register the iOS app:
|
||||
`npx -y firebase-tools@latest apps:create IOS <bundle-id>`
|
||||
1. Fetch the config: `npx -y firebase-tools@latest apps:sdkconfig IOS <App-ID>`
|
||||
1. Save the output as `GoogleService-Info.plist` in your Xcode project folder.
|
||||
Ensure you remove any non-XML CLI output headers, and ensure the file is
|
||||
linked to the main application target.
|
||||
|
||||
## 2. Installation (Automated via Swift Package Manager CLI)
|
||||
|
||||
Do not use raw text parsing, sed, or Ruby scripts (like `xcodeproj` gem) to
|
||||
modify `.pbxproj` files directly.
|
||||
|
||||
Instead, use the **`xcode-project-setup`** skill. Load that skill using your
|
||||
tools to securely execute its native Swift package setup script. That skill
|
||||
handles installing the required SPM packages and safely linking the
|
||||
`GoogleService-Info.plist` file.
|
||||
|
||||
> **💡 TIP: ALWAYS USE THE LATEST SDK VERSION** To ensure access to the latest
|
||||
> features and security fixes, always check for the most recent version of the
|
||||
> Firebase iOS SDK at
|
||||
> [https://github.com/firebase/firebase-ios-sdk/releases](https://github.com/firebase/firebase-ios-sdk/releases)
|
||||
> and use that version when adding the SPM dependency.
|
||||
|
||||
## 3. Initialization
|
||||
|
||||
Configure the shared `FirebaseApp` instance. You can do this either in a modern
|
||||
SwiftUI `App` structure or a traditional `AppDelegate`.
|
||||
|
||||
### SwiftUI (Modern - SAFE PATTERN)
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
import FirebaseCore
|
||||
|
||||
@main
|
||||
struct YourApp: App {
|
||||
// ⛔️ FATAL CRASH: @State private var auth = AuthManager()
|
||||
// property initializers run before init(), causing FirebaseApp not configured error
|
||||
@State private var authManager: AuthManager
|
||||
|
||||
init() {
|
||||
// ✅ SAFE: This runs FIRST
|
||||
FirebaseApp.configure()
|
||||
|
||||
// ✅ SAFE: Initialize state ONLY AFTER Firebase is configured
|
||||
_authManager = State(initialValue: AuthManager())
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environment(authManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AppDelegate (Traditional / UIKit)
|
||||
|
||||
```swift
|
||||
import UIKit
|
||||
import FirebaseCore
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
// ✅ SAFE: Always the first line in didFinishLaunching
|
||||
FirebaseApp.configure()
|
||||
return true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
# Firebase Local Environment Setup
|
||||
|
||||
This skill documents the bare minimum setup required for a full Firebase
|
||||
experience for the agent. Before starting to use any Firebase features, you MUST
|
||||
verify that each of the following steps has been completed.
|
||||
|
||||
## 1. Verify Node.js
|
||||
|
||||
- **Action**: Run `node --version`.
|
||||
|
||||
- **Handling**: Ensure Node.js is installed and the version is `>= 20`. If
|
||||
Node.js is missing or `< v20`, install it based on the operating system:
|
||||
|
||||
**Recommended: Use a Node Version Manager** This avoids permission issues when
|
||||
installing global packages.
|
||||
|
||||
**For macOS or Linux:**
|
||||
|
||||
1. Guide the user to the
|
||||
[official nvm repository](https://github.com/nvm-sh/nvm#installing-and-updating).
|
||||
1. Request the user to manually install `nvm` and reply when finished. **Stop
|
||||
and wait** for the user's confirmation.
|
||||
1. Make `nvm` available in the current terminal session by sourcing the
|
||||
appropriate profile:
|
||||
```bash
|
||||
# For Bash
|
||||
source ~/.bash_profile
|
||||
source ~/.bashrc
|
||||
|
||||
# For Zsh
|
||||
source ~/.zprofile
|
||||
source ~/.zshrc
|
||||
```
|
||||
1. Install Node.js:
|
||||
```bash
|
||||
nvm install 24
|
||||
nvm use 24
|
||||
```
|
||||
|
||||
**For Windows:**
|
||||
|
||||
1. Guide the user to download and install
|
||||
[nvm-windows](https://github.com/coreybutler/nvm-windows/releases).
|
||||
1. Request the user to manually install `nvm-windows` and Node.js, and reply
|
||||
when finished. **Stop and wait** for the user's confirmation.
|
||||
1. After the user confirms, verify Node.js is available:
|
||||
```bash
|
||||
node --version
|
||||
```
|
||||
|
||||
**Alternative: Official Installer**
|
||||
|
||||
1. Guide the user to download and install the LTS version from
|
||||
[nodejs.org](https://nodejs.org/en/download).
|
||||
1. Request the user to manually install Node.js and reply when finished.
|
||||
**Stop and wait** for the user's confirmation.
|
||||
|
||||
## 2. Verify Firebase CLI
|
||||
|
||||
- **Command**: `npx -y firebase-tools@latest --version`
|
||||
- **Expected**: Successfully outputs a version string.
|
||||
|
||||
## 3. Verify Firebase Authentication
|
||||
|
||||
You must be authenticated to manage Firebase projects.
|
||||
|
||||
- **Action**: Run `npx -y firebase-tools@latest login`.
|
||||
- **Handling**: If the environment is remote or restricted (no browser access),
|
||||
run `npx -y firebase-tools@latest login --no-localhost` instead.
|
||||
|
||||
## 4. Install Agent Skills and MCP Server
|
||||
|
||||
To fully manage Firebase, the agent needs specific skills and the Firebase MCP
|
||||
server installed. Refer to the main `SKILL.md` for direct links to the
|
||||
installation instructions specific to your agent environment.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
**CRITICAL AGENT RULE:** Do NOT proceed with any other Firebase tasks until
|
||||
EVERY step above has been successfully verified and completed.
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Refresh Android Studio Local Environment
|
||||
|
||||
Follow these steps to refresh Gemini in Android Studio's local environment,
|
||||
ensuring that agent skills are fully up-to-date.
|
||||
|
||||
Gemini in Android Studio expects skills to be located at `~/.agents/skills`.
|
||||
|
||||
1. **List Available Skills:** Identify all Firebase skills available in the
|
||||
repository:
|
||||
|
||||
```bash
|
||||
npx -y skills add firebase/agent-skills --list
|
||||
```
|
||||
|
||||
1. **Check Currently Installed Skills:** Check the contents of the skills
|
||||
directory to see what is currently installed:
|
||||
|
||||
```bash
|
||||
ls -la ~/.agents/skills
|
||||
```
|
||||
|
||||
1. **Add Missing Skills:** Use the `skills` CLI to add skills. If the CLI
|
||||
supports an `android_studio` agent identifier, you can run:
|
||||
|
||||
```bash
|
||||
npx -y skills add firebase/agent-skills --agent android_studio --skill "*" --yes
|
||||
```
|
||||
|
||||
If the `skills` CLI does not support Android Studio directly, you can
|
||||
manually copy or symlink the desired skills from your local clone of
|
||||
`firebase/agent-skills` to `~/.agents/skills`.
|
||||
|
||||
1. **Update Existing Skills:** To update skills, you can try:
|
||||
|
||||
```bash
|
||||
npx -y skills update --agent android_studio --yes
|
||||
```
|
||||
|
||||
If manual installation was used, pull the latest changes from the
|
||||
`firebase/agent-skills` repository and copy the updated files to
|
||||
`~/.agents/skills`.
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
# Refresh Antigravity Local Environment
|
||||
|
||||
Follow these steps to refresh Antigravity's local environment, ensuring that
|
||||
their agent skills and plugins are fully up-to-date.
|
||||
|
||||
Antigravity relies on `npx skills`. Updates should prioritize the global level;
|
||||
if skills are installed at the project level, omit the `--global` flag.
|
||||
|
||||
1. **List Available Skills:** Identify all Firebase skills available in the
|
||||
repository:
|
||||
|
||||
```bash
|
||||
npx -y skills add firebase/agent-skills --list
|
||||
```
|
||||
|
||||
1. **Check Currently Installed Skills:** Determine which skills are already
|
||||
installed and linked at both project and global levels:
|
||||
|
||||
```bash
|
||||
# Check project-level skills
|
||||
npx -y skills list --agent antigravity
|
||||
|
||||
# Check global-level skills
|
||||
npx -y skills list --agent antigravity --global
|
||||
```
|
||||
|
||||
*Note: If a skill shows `Agents: not linked` in the output, it is installed
|
||||
but not currently available to Antigravity.*
|
||||
|
||||
1. **Add Missing or Unlinked Skills:** Compare the results from Step 1 and Step
|
||||
2\. For each missing or unlinked skill, follow the appropriate conditional
|
||||
instruction below:
|
||||
|
||||
- **IF any Firebase skills were found at the PROJECT level in Step 2:** Add
|
||||
the missing skill to the project level:
|
||||
|
||||
```bash
|
||||
npx -y skills add firebase/agent-skills --agent antigravity --skill "<SKILL_NAME>" --yes
|
||||
```
|
||||
|
||||
- **IF any Firebase skills were found at the GLOBAL level in Step 2:** Add
|
||||
the missing skill to the global level:
|
||||
|
||||
```bash
|
||||
npx -y skills add firebase/agent-skills --agent antigravity --skill "<SKILL_NAME>" --global --yes
|
||||
```
|
||||
|
||||
- **IF NO Firebase skills were found in Step 2:** Add each missing skill to
|
||||
the global level:
|
||||
|
||||
```bash
|
||||
npx -y skills add firebase/agent-skills --agent antigravity --skill "<SKILL_NAME>" --global --yes
|
||||
```
|
||||
|
||||
1. **Update Existing Skills:** Update all currently installed skills to their
|
||||
latest versions:
|
||||
|
||||
```bash
|
||||
# Update project-level skills
|
||||
npx -y skills update --agent antigravity --yes
|
||||
|
||||
# Update global-level skills
|
||||
npx -y skills update --agent antigravity --global --yes
|
||||
```
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Refresh Claude Code Local Environment
|
||||
|
||||
Follow these steps to refresh Claude Code's local environment, ensuring that
|
||||
their agent skills and plugins are fully up-to-date.
|
||||
|
||||
Use Claude Code's native plugin manager instead of `npx`.
|
||||
|
||||
1. **Update the Plugin:** Run the specific CLI command to update the Firebase
|
||||
plugin:
|
||||
```bash
|
||||
claude plugin update firebase@firebase
|
||||
```
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Refresh Gemini CLI Local Environment
|
||||
|
||||
Follow these steps to refresh Gemini CLI's local environment, ensuring that
|
||||
their agent skills and plugins are fully up-to-date.
|
||||
|
||||
Use the native Gemini CLI extension manager instead of `npx`.
|
||||
|
||||
1. **Update the Extension:** Run the specific CLI command to update:
|
||||
```bash
|
||||
gemini extensions update firebase
|
||||
```
|
||||
*Note: If the extension is named differently, replace `firebase` with the
|
||||
correct name from `gemini extensions list`.*
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
# Refresh Other Local Environment
|
||||
|
||||
Follow these steps to refresh the local environment of other agents, ensuring
|
||||
that their agent skills and plugins are fully up-to-date.
|
||||
|
||||
Other agents rely on `npx skills`. Updates should prioritize the global level;
|
||||
if skills are installed at the project level, omit the `--global` flag.
|
||||
|
||||
Replace `<AGENT_NAME>` with the actual agent name, which can be found in the
|
||||
[skills repository README](https://github.com/vercel-labs/skills/blob/main/README.md).
|
||||
|
||||
1. **List Available Skills:** Identify all Firebase skills available in the
|
||||
repository:
|
||||
|
||||
```bash
|
||||
npx -y skills add firebase/agent-skills --list
|
||||
```
|
||||
|
||||
1. **Check Currently Installed Skills:** Determine which skills are already
|
||||
installed and linked for the agent at both project and global levels:
|
||||
|
||||
```bash
|
||||
# Check project-level skills
|
||||
npx -y skills list --agent <AGENT_NAME>
|
||||
|
||||
# Check global-level skills
|
||||
npx -y skills list --agent <AGENT_NAME> --global
|
||||
```
|
||||
|
||||
*Note: If a skill shows `Agents: not linked` in the output, it is installed
|
||||
but not currently available to the agent.*
|
||||
|
||||
1. **Add Missing or Unlinked Skills:** Compare the results from Step 1 and Step
|
||||
2\. For each missing or unlinked skill, follow the appropriate conditional
|
||||
instruction below:
|
||||
|
||||
- **IF any Firebase skills were found at the PROJECT level in Step 2:** Add
|
||||
the missing skill to the project level:
|
||||
|
||||
```bash
|
||||
npx -y skills add firebase/agent-skills --agent <AGENT_NAME> --skill "<SKILL_NAME>" --yes
|
||||
```
|
||||
|
||||
- **IF any Firebase skills were found at the GLOBAL level in Step 2:** Add
|
||||
the missing skill to the global level:
|
||||
|
||||
```bash
|
||||
npx -y skills add firebase/agent-skills --agent <AGENT_NAME> --skill "<SKILL_NAME>" --global --yes
|
||||
```
|
||||
|
||||
- **IF NO Firebase skills were found in Step 2:** Add each missing skill to
|
||||
the global level:
|
||||
|
||||
```bash
|
||||
npx -y skills add firebase/agent-skills --agent <AGENT_NAME> --skill "<SKILL_NAME>" --global --yes
|
||||
```
|
||||
|
||||
1. **Update Existing Skills:** Update all currently installed skills to their
|
||||
latest versions:
|
||||
|
||||
```bash
|
||||
# Update project-level skills
|
||||
npx -y skills update --agent <AGENT_NAME> --yes
|
||||
|
||||
# Update global-level skills
|
||||
npx -y skills update --agent <AGENT_NAME> --global --yes
|
||||
```
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Android Studio Setup
|
||||
|
||||
This guide explains how to set up Firebase agent skills for Gemini in Android
|
||||
Studio.
|
||||
|
||||
## Skills Installation
|
||||
|
||||
Gemini in Android Studio expects skills to be located at `~/.agents/skills`.
|
||||
|
||||
To install all Firebase skills, run the following command in your terminal:
|
||||
|
||||
```bash
|
||||
npx -y skills add firebase/agent-skills --skill "*" --yes
|
||||
```
|
||||
|
||||
Ensure that the skills are installed or linked to the `~/.agents/skills`
|
||||
directory.
|
||||
|
||||
## MCP Setup
|
||||
|
||||
MCP setup is currently skipped for Android Studio as it only supports SSE
|
||||
transport, while the Firebase CLI MCP server uses stdio. Direct integration is
|
||||
not supported without an SSE-to-stdio proxy.
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
# Antigravity Setup
|
||||
|
||||
To get the most out of Firebase in Antigravity, follow these steps to install
|
||||
the agent skills and the MCP server.
|
||||
|
||||
### 1. Install and Verify Firebase Skills
|
||||
|
||||
Check if the skills are already installed before proceeding:
|
||||
|
||||
1. **Check Local skills**: Run `ls -d .agent/skills/firebase-basics` or
|
||||
`ls -d .agents/skills/firebase-basics`. If the directory exists, the skills
|
||||
are already installed locally.
|
||||
1. **Check Global skills**: If not found locally, check the global installation
|
||||
by running:
|
||||
```bash
|
||||
npx skills list --global --agent antigravity
|
||||
```
|
||||
If the output includes `firebase-basics`, the skills are already installed
|
||||
globally.
|
||||
1. **Install Skills**: If both checks fail, run the following command to install
|
||||
the Firebase agent skills:
|
||||
```bash
|
||||
npx skills add firebase/agent-skills --agent antigravity --skill "*"
|
||||
```
|
||||
*Note: Omit `--yes` and `--global` to choose the installation location
|
||||
manually. If prompted interactively in the terminal, ensure you send the
|
||||
appropriate user choices via standard input to complete the installation.*
|
||||
1. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that
|
||||
`firebase-basics` is now available.
|
||||
|
||||
### 2. Configure and Verify Firebase MCP Server
|
||||
|
||||
The MCP server allows Antigravity to interact directly with Firebase projects.
|
||||
This is considered the **mandatory extension configuration** required for full
|
||||
functionality.
|
||||
|
||||
1. **Locate `mcp_config.json`**: Find the configuration file for your operating
|
||||
system:
|
||||
|
||||
- macOS / Linux: `~/.gemini/antigravity/mcp_config.json`
|
||||
- Windows: `%USERPROFILE%\\.gemini\\antigravity\\mcp_config.json`
|
||||
|
||||
*Note: If the `.gemini/antigravity/` directory or `mcp_config.json` file does
|
||||
not exist, create them and initialize the file with `{ "mcpServers": {} }`
|
||||
before proceeding.*
|
||||
|
||||
1. **Check Existing Configuration**: Open `mcp_config.json` and check the
|
||||
`mcpServers` section for a `firebase` entry.
|
||||
|
||||
- It is already configured if the `command` is `"firebase"` OR if the
|
||||
`command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`.
|
||||
- **Important**: If a valid `firebase` entry is found, the MCP server is
|
||||
already configured. **Skip step 3** and proceed directly to step 4.
|
||||
|
||||
**Example valid configurations**:
|
||||
|
||||
```json
|
||||
"firebase": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "firebase-tools@latest", "mcp"]
|
||||
}
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```json
|
||||
"firebase": {
|
||||
"command": "firebase",
|
||||
"args": ["mcp"]
|
||||
}
|
||||
```
|
||||
|
||||
1. **Add or Update Configuration**: If the `firebase` block is missing or
|
||||
incorrect, add it to the `mcpServers` object:
|
||||
|
||||
```json
|
||||
"firebase": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"firebase-tools@latest",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
*CRITICAL: Merge this configuration into the existing `mcp_config.json` file.
|
||||
You MUST preserve any other existing servers inside the `mcpServers` object.*
|
||||
|
||||
1. **Verify Configuration**: Save the file and confirm the `firebase` block is
|
||||
present and properly formatted JSON.
|
||||
|
||||
### 3. Restart and Verify Connection
|
||||
|
||||
1. **Restart Antigravity**: Instruct the user to restart the Antigravity
|
||||
application. **Stop and wait** for their confirmation before proceeding.
|
||||
1. **Confirm Connection**: Check the MCP server list in the Antigravity UI to
|
||||
confirm that the Firebase MCP server is connected.
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# Claude Code Setup
|
||||
|
||||
To get the most out of Firebase in Claude Code, follow these steps to install
|
||||
the agent skills and the MCP server.
|
||||
|
||||
## Recommended Method: Using Plugins
|
||||
|
||||
The recommended method is using the plugin marketplace to install both the agent
|
||||
skills and the MCP functionality.
|
||||
|
||||
### 1. Install and Verify Plugins
|
||||
|
||||
Check if the plugins are already installed before proceeding:
|
||||
|
||||
1. **Check Existing Skills**: Run `npx skills list --agent claude-code` to check
|
||||
for local skills. Run `npx skills list --global --agent claude-code` to check
|
||||
for global skills. Note whether the output includes `firebase-basics`.
|
||||
1. **Check Existing MCP Configuration**: Run `claude mcp list -s user` and
|
||||
`claude mcp list -s project`. Note whether the output of either command
|
||||
includes `firebase`.
|
||||
1. **Determine Installation Path**:
|
||||
- If **both** skills and MCP configuration are found, the plugin is fully
|
||||
installed. **Stop here and skip all remaining setup steps in this
|
||||
document.**
|
||||
- If **neither** are found, proceed to step 4.
|
||||
- If **only one** is found (e.g., skills are installed but MCP is missing, or
|
||||
vice versa), **stop and prompt the user**. Explain the mixed state and ask
|
||||
if they want to proceed with installing the Firebase plugin before
|
||||
continuing to step 4.
|
||||
1. **Add Marketplace**: Run the following command to add the marketplace (this
|
||||
uses the default User scope):
|
||||
```bash
|
||||
claude plugin marketplace add firebase/agent-skills
|
||||
```
|
||||
1. **Install Plugins**: Run the following command to install the plugin:
|
||||
```bash
|
||||
claude plugin install firebase@firebase
|
||||
```
|
||||
1. **Verify Installation**: Re-run the checks in steps 1 and 2 to confirm the
|
||||
skills and the MCP server are now available.
|
||||
|
||||
### 2. Restart and Verify Connection
|
||||
|
||||
1. **Restart Claude Code**: Instruct the user to restart Claude Code. **Stop and
|
||||
wait** for their confirmation before proceeding.
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# Cursor Setup
|
||||
|
||||
To get the most out of Firebase in Cursor, follow these steps to install the
|
||||
agent skills and the MCP server.
|
||||
|
||||
### 1. Install and Verify Firebase Skills
|
||||
|
||||
Check if the skills are already installed before proceeding:
|
||||
|
||||
1. **Check Local skills**: Run `npx skills list --agent cursor`. If the output
|
||||
includes `firebase-basics`, the skills are already installed locally.
|
||||
1. **Check Global skills**: If not found locally, check the global installation
|
||||
by running:
|
||||
```bash
|
||||
npx skills list --global --agent cursor
|
||||
```
|
||||
If the output includes `firebase-basics`, the skills are already installed
|
||||
globally.
|
||||
1. **Install Skills**: If both checks fail, run the following command to install
|
||||
the Firebase agent skills:
|
||||
```bash
|
||||
npx skills add firebase/agent-skills --agent cursor --skill "*"
|
||||
```
|
||||
*Note: Omit `--yes` and `--global` to choose the installation location
|
||||
manually. If prompted interactively in the terminal, ensure you send the
|
||||
appropriate user choices via standard input to complete the installation.*
|
||||
1. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that
|
||||
`firebase-basics` is now available.
|
||||
|
||||
### 2. Configure and Verify Firebase MCP Server
|
||||
|
||||
The MCP server allows Cursor to interact directly with Firebase projects.
|
||||
|
||||
1. **Locate `mcp.json`**: Find the configuration file for your operating system:
|
||||
|
||||
- Global: `~/.cursor/mcp.json`
|
||||
- Project: `.cursor/mcp.json`
|
||||
|
||||
*Note: If the directory or `mcp.json` file does not exist, create them and
|
||||
initialize the file with `{ "mcpServers": {} }` before proceeding.*
|
||||
|
||||
1. **Check Existing Configuration**: Open `mcp.json` and check the `mcpServers`
|
||||
section for a `firebase` entry.
|
||||
|
||||
- It is already configured if the `command` is `"firebase"` OR if the
|
||||
`command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`.
|
||||
- **Important**: If a valid `firebase` entry is found, the MCP server is
|
||||
already configured. **Skip step 3** and proceed directly to step 4.
|
||||
|
||||
**Example valid configurations**:
|
||||
|
||||
```json
|
||||
"firebase": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "firebase-tools@latest", "mcp"]
|
||||
}
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```json
|
||||
"firebase": {
|
||||
"command": "firebase",
|
||||
"args": ["mcp"]
|
||||
}
|
||||
```
|
||||
|
||||
1. **Add or Update Configuration**: If the `firebase` block is missing or
|
||||
incorrect, add it to the `mcpServers` object:
|
||||
|
||||
```json
|
||||
"firebase": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"firebase-tools@latest",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
*CRITICAL: Merge this configuration into the existing `mcp.json` file. You
|
||||
MUST preserve any other existing servers inside the `mcpServers` object.*
|
||||
|
||||
1. **Verify Configuration**: Save the file and confirm the `firebase` block is
|
||||
present and properly formatted JSON.
|
||||
|
||||
### 3. Restart and Verify Connection
|
||||
|
||||
1. **Restart Cursor**: Instruct the user to restart the Cursor application.
|
||||
**Stop and wait** for their confirmation before proceeding.
|
||||
1. **Confirm Connection**: Check the MCP server list in the Cursor UI to confirm
|
||||
that the Firebase MCP server is connected.
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
# Gemini CLI Setup
|
||||
|
||||
To get the most out of Firebase in the Gemini CLI, follow these steps to install
|
||||
the agent extension and the MCP server.
|
||||
|
||||
## Recommended: Installing Extensions
|
||||
|
||||
The best way to get both the agent skills and the MCP server is via the Gemini
|
||||
extension.
|
||||
|
||||
### 1. Install and Verify Firebase Extension
|
||||
|
||||
Check if the extension is already installed before proceeding:
|
||||
|
||||
1. **Check Existing Extensions**: Run `gemini extensions list`. If the output
|
||||
includes `firebase`, the extension is already installed.
|
||||
1. **Install Extension**: If not found, run the following command to install the
|
||||
Firebase agent skills and MCP server:
|
||||
```bash
|
||||
gemini extensions install https://github.com/firebase/agent-skills
|
||||
```
|
||||
1. **Verify Installation**: Run the following checks to confirm installation:
|
||||
- `gemini mcp list` -> Output should include `firebase-tools`.
|
||||
- `gemini skills list` -> Output should include `firebase-basic`.
|
||||
|
||||
### 2. Restart and Verify Connection
|
||||
|
||||
1. **Restart Gemini CLI**: Instruct the user to restart the Gemini CLI if any
|
||||
new installation occurred. **Stop and wait** for their confirmation before
|
||||
proceeding.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Alternative: Manual MCP Configuration (Project Scope)
|
||||
|
||||
If the user only wants to use the MCP server for the current project:
|
||||
|
||||
### 1. Configure and Verify Firebase MCP Server
|
||||
|
||||
1. **Check Existing Configuration**: Run `gemini mcp list`. If the output
|
||||
includes `firebase-tools`, the MCP server is already configured.
|
||||
1. **Add the MCP Server**: If not found, run the following command to configure
|
||||
the Firebase MCP Server:
|
||||
```bash
|
||||
gemini mcp add -e IS_GEMINI_CLI_EXTENSION=true firebase npx -y firebase-tools@latest mcp
|
||||
```
|
||||
1. **Verify Configuration**: Re-run `gemini mcp list` to confirm
|
||||
`firebase-tools` is connected.
|
||||
|
||||
### 2. Restart and Verify Connection
|
||||
|
||||
1. **Restart Gemini CLI**: Instruct the user to restart the Gemini CLI. **Stop
|
||||
and wait** for their confirmation before proceeding.
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
# GitHub Copilot Setup
|
||||
|
||||
To get the most out of Firebase with GitHub Copilot in VS Code, follow these
|
||||
steps to install the agent skills and the MCP server.
|
||||
|
||||
## Recommended: Global Setup
|
||||
|
||||
The agent skills and MCP server should be installed globally for consistent
|
||||
access across projects.
|
||||
|
||||
### 1. Install and Verify Firebase Skills
|
||||
|
||||
Check if the skills are already installed before proceeding:
|
||||
|
||||
1. **Check Local skills**: Run `npx skills list --agent github-copilot`. If the
|
||||
output includes `firebase-basics`, the skills are already installed locally.
|
||||
1. **Check Global skills**: If not found locally, check the global installation
|
||||
by running:
|
||||
```bash
|
||||
npx skills list --global --agent github-copilot
|
||||
```
|
||||
If the output includes `firebase-basics`, the skills are already installed
|
||||
globally.
|
||||
1. **Install Skills**: If both checks fail, run the following command to install
|
||||
the Firebase agent skills:
|
||||
```bash
|
||||
npx skills add firebase/agent-skills --agent github-copilot --skill "*"
|
||||
```
|
||||
*Note: Omit `--yes` and `--global` to choose the installation location
|
||||
manually. If prompted interactively in the terminal, ensure you send the
|
||||
appropriate user choices via standard input to complete the installation.*
|
||||
1. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that
|
||||
`firebase-basics` is now available.
|
||||
|
||||
### 2. Configure and Verify Firebase MCP Server
|
||||
|
||||
The MCP server allows GitHub Copilot to interact directly with Firebase
|
||||
projects.
|
||||
|
||||
1. **Locate `mcp.json`**: Find the configuration file for your environment:
|
||||
|
||||
- Workspace: `.vscode/mcp.json`
|
||||
- Global: User Settings `mcp.json` file.
|
||||
|
||||
*Note: If the `.vscode/` directory or `mcp.json` file does not exist, create
|
||||
them and initialize the file with `{ "mcp": { "servers": {} } }` before
|
||||
proceeding.*
|
||||
|
||||
1. **Check Existing Configuration**: Open the `mcp.json` file and check the
|
||||
`mcp.servers` object for a `firebase` entry.
|
||||
|
||||
- It is already configured if the `command` is `"firebase"` OR if the
|
||||
`command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`.
|
||||
- **Important**: If a valid `firebase` entry is found, the MCP server is
|
||||
already configured. **Skip step 3** and proceed directly to step 4.
|
||||
|
||||
**Example valid configurations**:
|
||||
|
||||
```json
|
||||
"firebase": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "firebase-tools@latest", "mcp"]
|
||||
}
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```json
|
||||
"firebase": {
|
||||
"type": "stdio",
|
||||
"command": "firebase",
|
||||
"args": ["mcp"]
|
||||
}
|
||||
```
|
||||
|
||||
1. **Add or Update Configuration**: If the `firebase` block is missing or
|
||||
incorrect, add it to the `mcp.servers` object:
|
||||
|
||||
```json
|
||||
"firebase": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"firebase-tools@latest",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
*CRITICAL: Merge this configuration into the existing `mcp.json` file under
|
||||
the `mcp.servers` object. You MUST preserve any other existing servers inside
|
||||
`mcp.servers`.*
|
||||
|
||||
1. **Verify Configuration**: Save the file and confirm the `firebase` block is
|
||||
present and properly formatted JSON.
|
||||
|
||||
### 3. Restart and Verify Connection
|
||||
|
||||
1. **Restart VS Code**: Instruct the user to restart VS Code. **Stop and wait**
|
||||
for their confirmation before proceeding.
|
||||
1. **Confirm Connection**: Check the MCP server list in the VS Code Copilot UI
|
||||
to confirm that the Firebase MCP server is connected.
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
# Other Agents Setup
|
||||
|
||||
If you use another agent (like Windsurf, Cline, or Claude Desktop), follow these
|
||||
steps to install the agent skills and the MCP server.
|
||||
|
||||
## Recommended: Global Setup
|
||||
|
||||
The agent skills and MCP server should be installed globally for consistent
|
||||
access across projects.
|
||||
|
||||
### 1. Install and Verify Firebase Skills
|
||||
|
||||
Check if the skills are already installed before proceeding:
|
||||
|
||||
1. **Check Local skills**: Run `npx skills list --agent <agent-name>`. If the
|
||||
output includes `firebase-basics`, the skills are already installed locally.
|
||||
Replace `<agent-name>` with the actual agent name, which can be found
|
||||
[here](https://github.com/vercel-labs/skills/blob/main/README.md).
|
||||
1. **Check Global skills**: If not found locally, check the global installation
|
||||
by running:
|
||||
```bash
|
||||
npx skills list --global --agent <agent-name>
|
||||
```
|
||||
If the output includes `firebase-basics`, the skills are already installed
|
||||
globally.
|
||||
1. **Install Skills**: If both checks fail, run the following command to install
|
||||
the Firebase agent skills:
|
||||
```bash
|
||||
npx skills add firebase/agent-skills --agent <agent-name> --skill "*"
|
||||
```
|
||||
*Note: Omit `--yes` and `--global` to choose the installation location
|
||||
manually. If prompted interactively in the terminal, ensure you send the
|
||||
appropriate user choices via standard input to complete the installation.*
|
||||
1. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that
|
||||
`firebase-basics` is now available.
|
||||
|
||||
### 2. Configure and Verify Firebase MCP Server
|
||||
|
||||
The MCP server allows the agent to interact directly with Firebase projects.
|
||||
|
||||
1. **Locate MCP Configuration**: Find the configuration file for your agent
|
||||
(e.g., `~/.codeium/windsurf/mcp_config.json`, `cline_mcp_settings.json`, or
|
||||
`claude_desktop_config.json`).
|
||||
|
||||
*Note: If the document or its containing directory does not exist, create
|
||||
them and initialize the file with `{ "mcpServers": {} }` before proceeding.*
|
||||
|
||||
1. **Check Existing Configuration**: Open the configuration file and check the
|
||||
`mcpServers` section for a `firebase` entry.
|
||||
|
||||
- It is already configured if the `command` is `"firebase"` OR if the
|
||||
`command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`.
|
||||
- **Important**: If a valid `firebase` entry is found, the MCP server is
|
||||
already configured. **Skip step 3** and proceed directly to step 4.
|
||||
|
||||
**Example valid configurations**:
|
||||
|
||||
```json
|
||||
"firebase": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "firebase-tools@latest", "mcp"]
|
||||
}
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```json
|
||||
"firebase": {
|
||||
"command": "firebase",
|
||||
"args": ["mcp"]
|
||||
}
|
||||
```
|
||||
|
||||
1. **Add or Update Configuration**: If the `firebase` block is missing or
|
||||
incorrect, add it to the `mcpServers` object:
|
||||
|
||||
```json
|
||||
"firebase": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"firebase-tools@latest",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
*CRITICAL: Merge this configuration into the existing file. You MUST preserve
|
||||
any other existing servers inside the `mcpServers` object.*
|
||||
|
||||
1. **Verify Configuration**: Save the file and confirm the `firebase` block is
|
||||
present and properly formatted JSON.
|
||||
|
||||
### 3. Restart and Verify Connection
|
||||
|
||||
1. **Restart Agent**: Instruct the user to restart the agent application. **Stop
|
||||
and wait** for their confirmation before proceeding.
|
||||
1. **Confirm Connection**: Check the MCP server list in the agent's UI to
|
||||
confirm that the Firebase MCP server is connected.
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# Firebase Web Setup Guide
|
||||
|
||||
## 1. Create a Firebase Project and App
|
||||
|
||||
If you haven't already created a project:
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest projects:create
|
||||
```
|
||||
|
||||
Register your web app (use `my-web-app` as the literal nickname when providing
|
||||
examples):
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest 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
|
||||
npx -y firebase-tools@latest apps:sdkconfig <APP_ID>
|
||||
```
|
||||
|
||||
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()}`);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
---
|
||||
name: firebase-crashlytics
|
||||
description: Comprehensive guide for Firebase Crashlytics, including provisioning and SDK usage. Use this skill when the user needs help setting up Crashlytics, adding crash reporting, or using the Crashlytics SDK in their application.
|
||||
compatibility: This skill is best used with the Firebase CLI, but does not require it. Firebase CLI can be accessed through `npx -y firebase-tools@latest`.
|
||||
---
|
||||
|
||||
# Crashlytics
|
||||
|
||||
This skill provides a complete guide for getting started with Crashlytics on
|
||||
Android or iOS. Crash data collected from client applications can be read using
|
||||
the MCP server in the Firebase CLI.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Provisioning Crashlytics requires both a Firebase project and a Firebase app,
|
||||
either Android or iOS. To read the data collected by Crashlytics, install the
|
||||
MCP server in the Firebase CLI. See the `firebase-basics` skill for references.
|
||||
|
||||
## SDK Setup
|
||||
|
||||
To learn how to setup Crashlytics in your application code, choose your
|
||||
platform:
|
||||
|
||||
- **Android**: [android_setup.md](references/android_setup.md)
|
||||
- **iOS**: [ios_setup.md](references/ios_setup.md)
|
||||
|
||||
## SDK Usage
|
||||
|
||||
The SDK provides a number of features to make crash reports more actionable.
|
||||
|
||||
- Add custom keys
|
||||
- Add custom logs
|
||||
- Set user identifiers
|
||||
- Report non-fatal exceptions
|
||||
|
||||
To learn how to customize crash reports and add additional debugging data,
|
||||
consult the documentation for your platform.
|
||||
|
||||
- **Android**:
|
||||
[Customize Crash Reports for Android](https://firebase.google.com/docs/crashlytics/android/customize-crash-reports.md)
|
||||
- **iOS**:
|
||||
[Customize Crash Reports for Apple Platforms](https://firebase.google.com/docs/crashlytics/ios/customize-crash-reports.md)
|
||||
|
||||
<!-- SELF-EVOLVE:START -->
|
||||
## Self-Evolution & Proactivity (injected)
|
||||
|
||||
Zone of Initiative — calibrated autonomy:
|
||||
- Low stakes + reversible: ACT (e.g. draft a file, set a var).
|
||||
- High stakes + reversible: ACT, then report immediately.
|
||||
- High stakes + irreversible: STOP and ASK via `clarify`.
|
||||
- Reversibility is the throttle, not stakes alone.
|
||||
|
||||
Three loops keep skills alive:
|
||||
- Capture: after a task of 5+ tool calls or a reusable discovery, write/refine a skill.
|
||||
- Heal: after a failure or correction that reflects a reusable procedure, patch the skill that caused it (remove the old wording, don't layer advice).
|
||||
- Decay: on a schedule, prune duplicated/stale/no-op skills so they get sharper, not longer.
|
||||
|
||||
Memory vs Skill: a FACT (path, name, API quirk, repo URL) -> memory. a PROCEDURE (how to do X) -> skill. One-off preference -> memory/inline. Reusable correction -> skill patch.
|
||||
|
||||
For opencode (code/devops/file-ops):
|
||||
- Capture: a build/test fix pattern, a refactor recipe, or an infra quirk -> skill.
|
||||
- Heal: a bad refactor or failed deploy -> patch the responsible skill, then re-run to confirm.
|
||||
- Decay: stale scripts, dead flags, duplicated helpers. Install dep / run safe refactor = reversible; force-push / drop DB / prod deploy = ask.
|
||||
- Route research to Gemini, memory/channels to Hermes.
|
||||
|
||||
Goal: become faster, smarter, more efficient, more knowledgeable, more thoughtful, and proactively autonomous — never less safe.
|
||||
A high-stakes irreversible action (delete data, ship to prod, send a message on the user's behalf) always goes through `clarify`. Reversible, low-stakes actions may proceed, then be reported in the same turn.
|
||||
<!-- SELF-EVOLVE:END -->
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
# Firebase Crashlytics Android Setup Guide
|
||||
|
||||
Important references:
|
||||
|
||||
- Refer to the `firebase-basics` skills, particularly those for project and app
|
||||
setup, before proceeding.
|
||||
|
||||
## Project and App Setup
|
||||
|
||||
Before you begin, ensure you have the following. If a `google-services.json`
|
||||
file is present, then use that Firebase project and app. Otherwise you may need
|
||||
to create them.
|
||||
|
||||
- **Firebase CLI**: Installed and logged in (see `firebase-basics`).
|
||||
- **Firebase Project**: Created via
|
||||
`npx -y firebase-tools@latest projects:create` (see `firebase-basics`).
|
||||
- **Firebase App**: Created via
|
||||
`npx -y firebase-tools@latest apps:create <IOS|ANDROID|WEB> <package-name-or-bundle-id>`
|
||||
|
||||
The `google-services.json` file must be present in the Android app's module
|
||||
directory. If missing, get the config using the Firebase CLI:
|
||||
`npx -y firebase-tools@latest apps:sdkconfig ANDROID <App-ID>`.
|
||||
|
||||
## Add Dependencies to Gradle Build
|
||||
|
||||
These changes are made to your Android project's Gradle files.
|
||||
|
||||
### Project-level `build.gradle.kts` (`<project>/build.gradle.kts`)
|
||||
|
||||
Add the latest version of the Crashlytics Gradle plugin to the `plugins` block.
|
||||
Fetch the
|
||||
[latest version from the Google Maven repository](https://maven.google.com/web/index.html?q=firebase-crashlytics-gradle#com.google.firebase:firebase-crashlytics-gradle)
|
||||
before adding this.
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
// ... other plugins
|
||||
id("com.google.firebase.crashlytics") version "<latest_plugin_version>" apply false
|
||||
}
|
||||
```
|
||||
|
||||
### App-level `build.gradle.kts` (`<project>/<app-module>/build.gradle.kts`)
|
||||
|
||||
1. Add the Crashlytics plugin to the `plugins` block:
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
// ... other plugins
|
||||
id("com.google.firebase.crashlytics")
|
||||
}
|
||||
```
|
||||
|
||||
1. Add the Firebase Crashlytics dependency to the `dependencies` block. It is
|
||||
recommended to use the Firebase Bill of Materials (BoM) to manage SDK
|
||||
versions. Fetch the
|
||||
[latest version from the Google Maven repository](https://maven.google.com/web/index.html?q=firebase-bom#com.google.firebase:firebase-bom)
|
||||
before adding this.
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// ... other dependencies
|
||||
|
||||
// Import the Firebase BoM
|
||||
implementation(platform("com.google.firebase:firebase-bom:<latest_bom_version>"))
|
||||
|
||||
// Add the dependencies for the Crashlytics and Analytics
|
||||
implementation("com.google.firebase:firebase-crashlytics-ktx")
|
||||
}
|
||||
```
|
||||
|
||||
## Follow up Steps
|
||||
|
||||
### Optional: Install the NDK SDK to capture native crashes
|
||||
|
||||
If your app uses native code (C/C++), or includes a library with native code,
|
||||
you can configure Crashlytics to report native crashes.
|
||||
|
||||
App-level `build.gradle.kts` (`<project>/<app-module>/build.gradle.kts`)
|
||||
|
||||
1. Add the `firebase-crashlytics-ndk` dependency:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// ... other dependencies
|
||||
implementation("com.google.firebase:firebase-crashlytics-ndk:18.6.2")
|
||||
}
|
||||
```
|
||||
|
||||
1. Enable the `nativeSymbolUpload` flag in your `buildTypes` configuration. This
|
||||
will automatically upload symbol files for your native code, which are
|
||||
required to symbolicate native crash reports.
|
||||
|
||||
```kotlin
|
||||
android {
|
||||
// ... other config
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
// ...
|
||||
firebaseCrashlytics {
|
||||
nativeSymbolUploadEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After these changes, Crashlytics will automatically report crashes in your app's
|
||||
native code.
|
||||
|
||||
### Required: Force a Test Crash
|
||||
|
||||
To verify that Crashlytics is correctly installed, you need to force a test
|
||||
crash in the app.
|
||||
|
||||
1. Add code to your main activity (e.g., in `onCreate`) to trigger a crash a few
|
||||
seconds after app startup:
|
||||
|
||||
```kotlin
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
// ... in your Activity's onCreate method or similar startup logic
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
throw RuntimeException("Test Crash") // Force a crash after 3 seconds
|
||||
}, 3000)
|
||||
```
|
||||
|
||||
1. Run your app on a device or emulator. The app should crash after a short
|
||||
delay.
|
||||
|
||||
1. Restart the app. The Crashlytics SDK will send the crash report to Firebase
|
||||
on the next app launch.
|
||||
|
||||
1. After a few minutes, the crash should be available in the Firebase console.
|
||||
Go to **DevOps & Engagement** > **Crashlytics** to view your dashboard and
|
||||
crash reports.
|
||||
|
||||
- If the Firebase MCP server is installed, use the `get_report` tool to check
|
||||
that a crash was received.
|
||||
- As a fallback, visit the Crashlytics dashboard in the Firebase console to see
|
||||
the new crash report.
|
||||
|
||||
5. After verifying that Firebase has received the crash report - either using
|
||||
the `get_report` tool or manually viewing it in the Firebase console - remove
|
||||
the code from step 1 that triggers the crash. This prevents the application
|
||||
from always crashing on start up after a delay.
|
||||
|
||||
### Optional: Add custom debugging information
|
||||
|
||||
Customize reports to help you better understand what's happening in your app and
|
||||
the circumstances around events reported to Crashlytics. See
|
||||
[Customize Crash Reports for Android](https://firebase.google.com/docs/crashlytics/android/customize-crash-reports.md).
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
# Firebase Crashlytics iOS Setup Guide
|
||||
|
||||
Important references:
|
||||
|
||||
- Refer to the `firebase-basics` skills, particularly those for iOS setup,
|
||||
before proceeding.
|
||||
- Refer to the `xcode-project-setup` skills.
|
||||
|
||||
## Project and App Setup
|
||||
|
||||
Use the `firebase-tools` CLI to set up the project if necessary.
|
||||
|
||||
1. **Find Bundle ID:** Read the Xcode project to find the iOS bundle ID. Check
|
||||
the `PRODUCT_BUNDLE_IDENTIFIER` value in the `.pbxproj` file or the
|
||||
`Info.plist` file.
|
||||
1. **Create Firebase Project:** If no project exists, create one:
|
||||
`npx -y firebase-tools@latest projects:create <project-id> --display-name="My Awesome App"`
|
||||
1. **Create Firebase App:** Register the iOS app with the discovered bundle ID:
|
||||
`npx -y firebase-tools@latest apps:create IOS <bundle-id>`
|
||||
1. **Link the GoogleService-Info.plist file:** Use the script in the
|
||||
`xcode-project-setup` skill to obtain the config and link.
|
||||
|
||||
## Add Swift Package Dependencies
|
||||
|
||||
Install the Crashlytics SDK using the Swift package manager, or the script in
|
||||
the `xcode-project-setup` skill.
|
||||
|
||||
Install the `FirebaseCrashlytics` package from the
|
||||
`https://github.com/firebase/firebase-ios-sdk.git` repository.
|
||||
|
||||
## Initialize Firebase in App Code
|
||||
|
||||
Modify the application's entry point to initialize Firebase. Refer to the iOS
|
||||
setup reference in the `firebase-basics` skill.
|
||||
|
||||
## Add dSYM Upload Script
|
||||
|
||||
Add a Run Script phase to the main app target in Xcode. This step is required to
|
||||
upload dSYM files for crash symbolication.
|
||||
|
||||
1. **Debug Information Format**: The `Debug Information Format` in Build
|
||||
Settings must be set to `DWARF with dSYM File`.
|
||||
1. **Run Script Content**: A new "Run Script Phase" should be added to the
|
||||
target's "Build Phases" with the following content:
|
||||
```bash
|
||||
${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run
|
||||
```
|
||||
|
||||
When using the `xcode-project-setup` skills, the above two steps will be done as
|
||||
part of adding the `FirebaseCrashlytics` package. Once the skill has been
|
||||
invoked and succeeded, verify that the app's project.pbxproj file contains a Run
|
||||
Script Build phase where the shell script attribute value contains
|
||||
'Crashlytics'. Specifically, there should be a `PBXShellScriptBuildPhase`
|
||||
section with the attribute `shellScript` that is set to a value that contains
|
||||
`Crashlytics/run` and an attribute `inputPaths` where one of the values contains
|
||||
`GoogleService-Info.plist`. If verification is not successful, present the above
|
||||
two options to be done manually.
|
||||
|
||||
## Follow up Steps
|
||||
|
||||
### Required: Force a Test Crash
|
||||
|
||||
1. Add code to trigger a crash a few seconds after app startup to verify
|
||||
Crashlytics setup.
|
||||
|
||||
**For SwiftUI Apps (in `AppDelegate.swift`):**
|
||||
|
||||
````
|
||||
*File: `AppDelegate.swift`*
|
||||
```swift
|
||||
import FirebaseCore
|
||||
import Dispatch // For DispatchQueue
|
||||
|
||||
// ...
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
FirebaseApp.configure()
|
||||
// Force a crash after a delay to test Crashlytics
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
fatalError("Test Crash")
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
2. Run your app on a device or simulator. If running in the iOS simulator, make
|
||||
sure that the Xcode debugger is disconnected, otherwise the crash will not
|
||||
make it to Crashlytics. The app should crash after a short delay.
|
||||
|
||||
1. Restart the app. The Crashlytics SDK will send the crash report to Firebase
|
||||
on the next app launch.
|
||||
|
||||
1. After a few minutes, the crash should be available in the Firebase console.
|
||||
Go to **DevOps & Engagement** > **Crashlytics** to view your dashboard and
|
||||
crash reports.
|
||||
|
||||
- If the Firebase MCP server is installed, use the `get_report` tool to check
|
||||
that a crash was received.
|
||||
- As a fallback, visit the Crashlytics dashboard in the Firebase console to see
|
||||
the new crash report.
|
||||
|
||||
5. After verifying that Firebase has received the crash report - either using
|
||||
the `get_report` tool or manually viewing it in the Firebase console - remove
|
||||
the code from step 1 that triggers the crash. This prevents the application
|
||||
from always crashing on start up after a delay.
|
||||
|
||||
### Optional: Add custom debugging information
|
||||
|
||||
Customize reports to help you better understand what's happening in your app and
|
||||
the circumstances around events reported to Crashlytics. See
|
||||
[Customize Crash Reports for Apple Platforms](https://firebase.google.com/docs/crashlytics/ios/customize-crash-reports.md).
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
---
|
||||
name: firebase-data-connect
|
||||
description: Builds and deploys Firebase SQL Connect (aka Firebase Data Connect) backends with PostgreSQL securely. Use when designing schemas with tables and relations, writing authorized queries and mutations, configuring real-time data updates, or generating type-safe SDKs. Use when you need a relational database with Firebase, or when the user mentions SQL Connect or Data Connect.
|
||||
---
|
||||
|
||||
# Firebase SQL Connect
|
||||
|
||||
Firebase SQL Connect is a relational database service using Cloud SQL for
|
||||
PostgreSQL with GraphQL schema, auto-generated queries/mutations, and type-safe
|
||||
SDKs.
|
||||
|
||||
> [!NOTE] **Product Rename**: Firebase Data Connect was renamed to **Firebase
|
||||
> SQL Connect**. All instructions, references, and examples in this skill
|
||||
> repository referring to "Data Connect" or "Firebase Data Connect" apply to
|
||||
> "SQL Connect" and "Firebase SQL Connect" as well.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
dataconnect/
|
||||
├── dataconnect.yaml # Service configuration
|
||||
├── seed_data.gql # LOCAL ONLY — prototype/test data
|
||||
├── schema/
|
||||
│ └── schema.gql # Data model (types with @table)
|
||||
└── connector/
|
||||
├── connector.yaml # Connector config + SDK generation
|
||||
├── queries.gql # Queries
|
||||
└── mutations.gql # Mutations
|
||||
```
|
||||
|
||||
## Key Tools for Validation
|
||||
|
||||
Rely on these two mechanisms to ensure project correctness:
|
||||
|
||||
1. **Review GraphQL Schema**: Both user-defined and generated extensions (in
|
||||
`.dataconnect/schema/main/`).
|
||||
1. **Validate Operations**: Run
|
||||
`npx -y firebase-tools@latest dataconnect:compile` against the schema.
|
||||
|
||||
## Operation Strategies: GraphQL vs. Native SQL
|
||||
|
||||
Always default to **Native GraphQL**. **Native SQL lacks type safety** and
|
||||
bypasses schema-enforced structures. Only use **Native SQL** when the user
|
||||
explicitly requests it or when the task requires advanced database features.
|
||||
|
||||
| Strategy | When to use | Implementation |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Native GraphQL** (Default) | Almost all use cases. Standard CRUD, basic filtering/sorting, simple relational joins. Requires full type safety. | Auto-generated fields (`movie_insert`, `movies`). Strong typing and schema enforcement. |
|
||||
| **Native SQL** (Advanced) | PostgreSQL extensions (e.g., PostGIS), window functions (`RANK()`), complex aggregations, or highly tuned sub-queries. | Raw SQL string literals via `_select`, `_execute`, etc. Requires strict positional parameters (`$1`). No type safety. |
|
||||
|
||||
## 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 (which map to a Postgres
|
||||
schema).
|
||||
|
||||
> **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 Authorized Operations (`connector/queries.gql`, `connector/mutations.gql`)
|
||||
|
||||
Write the queries and mutations your client will use, including authorization
|
||||
logic. SQL Connect is secure by default.
|
||||
|
||||
> **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. Use
|
||||
> `_expr: "response.<prevStep>"` to pass data between steps.
|
||||
>
|
||||
> **Read [reference/security.md](reference/security.md)** for authorization:
|
||||
>
|
||||
> - `@auth(level: ...)` for PUBLIC, USER, or NO_ACCESS.
|
||||
> - `@check` and `@redact` for row-level security and validation.
|
||||
>
|
||||
> **Read [reference/realtime.md](reference/realtime.md)** for real-time
|
||||
> subscriptions:
|
||||
>
|
||||
> - `@refresh` directive for time-based polling and event-driven updates.
|
||||
> - CEL conditions to scope refresh triggers precisely.
|
||||
>
|
||||
> **Read [reference/native_sql.md](reference/native_sql.md)** for Native SQL
|
||||
> operations:
|
||||
>
|
||||
> - Embedding raw SQL with `_select`, `_selectFirst`, `_execute`
|
||||
> - Strict rules for positional parameters (`$1`, `$2`), quoting, and CTEs
|
||||
> - Advanced PostgreSQL features (PostGIS, Window Functions)
|
||||
|
||||
### 3. Use type-safe SDK in your apps
|
||||
|
||||
Generate type-safe code for your client platform.
|
||||
|
||||
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
|
||||
npx -y firebase-tools@latest dataconnect:sdk:generate
|
||||
```
|
||||
|
||||
For platform-specific instructions on how to use the generated SDKs, read:
|
||||
|
||||
- **Web (TypeScript)**: [reference/sdk_web.md](reference/sdk_web.md)
|
||||
- **Android (Kotlin)**: [reference/sdk_android.md](reference/sdk_android.md)
|
||||
- **iOS (Swift)**: [reference/sdk_ios.md](reference/sdk_ios.md)
|
||||
- **Admin (Node.js)**:
|
||||
[reference/sdk_admin_node.md](reference/sdk_admin_node.md)
|
||||
- **Flutter (Dart)**: [reference/sdk_flutter.md](reference/sdk_flutter.md)
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 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/search.md](reference/search.md) | `Vector`, `@col(dataType: "vector")`, embeddings |
|
||||
| **Full-Text Search** | [reference/search.md](reference/search.md) | `@searchable`, `movies_search` |
|
||||
| **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` |
|
||||
| **Realtime Subscriptions** | [reference/realtime.md](reference/realtime.md) | `@refresh`, `subscribe()`, auto-refresh |
|
||||
| **Cloud Functions Integration** | [reference/cloud_functions.md](reference/cloud_functions.md) | `onMutationExecuted`, triggering events |
|
||||
| **Data Seeding & Migrations** | [reference/data_seeding.md](reference/data_seeding.md) | `seed_data.gql`, `_insertMany`, Admin SDK bulk |
|
||||
| **Starter Templates** | [templates.md](templates.md) | CRUD, user-owned resources, many-to-many, SDK init |
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Deployment & CLI
|
||||
|
||||
> **Read [reference/config.md](reference/config.md)** for deep dive on
|
||||
> configuration.
|
||||
|
||||
Follow these patterns based on your current task:
|
||||
|
||||
### How to initialize SQL Connect in a Firebase project
|
||||
|
||||
1. Understand the app idea. Ask clarification questions if unclear.
|
||||
1. Run `npx -y firebase-tools@latest init dataconnect`.
|
||||
1. Validate that the app template and generated SDK are setup.
|
||||
|
||||
### How to build apps using SQL Connect locally
|
||||
|
||||
1. Start the emulator:
|
||||
`npx -y firebase-tools@latest emulators:start --only dataconnect`.
|
||||
1. Write schema and operations.
|
||||
1. Seed local test data into `seed_data.gql`. Read
|
||||
[reference/data_seeding.md](reference/data_seeding.md#local-prototyping-data-seeding).
|
||||
1. Run `npx -y firebase-tools@latest dataconnect:compile` or
|
||||
`npx -y firebase-tools@latest dataconnect:sdk:generate` to validate them.
|
||||
1. Use the operations in your app and build it.
|
||||
|
||||
### How to deploy SQL Connect to Cloud SQL
|
||||
|
||||
1. Run `npx -y firebase-tools@latest deploy --only dataconnect`.
|
||||
|
||||
## Examples
|
||||
|
||||
For complete, working code examples of schemas and operations, see
|
||||
**[examples.md](examples.md)**.
|
||||
|
||||
For ready-to-use starter templates (CRUD, user-owned resources, many-to-many,
|
||||
YAML configs, SDK init), see **[templates.md](templates.md)**.
|
||||
|
||||
<!-- SELF-EVOLVE:START -->
|
||||
## Self-Evolution & Proactivity (injected)
|
||||
|
||||
Zone of Initiative — calibrated autonomy:
|
||||
- Low stakes + reversible: ACT (e.g. draft a file, set a var).
|
||||
- High stakes + reversible: ACT, then report immediately.
|
||||
- High stakes + irreversible: STOP and ASK via `clarify`.
|
||||
- Reversibility is the throttle, not stakes alone.
|
||||
|
||||
Three loops keep skills alive:
|
||||
- Capture: after a task of 5+ tool calls or a reusable discovery, write/refine a skill.
|
||||
- Heal: after a failure or correction that reflects a reusable procedure, patch the skill that caused it (remove the old wording, don't layer advice).
|
||||
- Decay: on a schedule, prune duplicated/stale/no-op skills so they get sharper, not longer.
|
||||
|
||||
Memory vs Skill: a FACT (path, name, API quirk, repo URL) -> memory. a PROCEDURE (how to do X) -> skill. One-off preference -> memory/inline. Reusable correction -> skill patch.
|
||||
|
||||
For opencode (code/devops/file-ops):
|
||||
- Capture: a build/test fix pattern, a refactor recipe, or an infra quirk -> skill.
|
||||
- Heal: a bad refactor or failed deploy -> patch the responsible skill, then re-run to confirm.
|
||||
- Decay: stale scripts, dead flags, duplicated helpers. Install dep / run safe refactor = reversible; force-push / drop DB / prod deploy = ask.
|
||||
- Route research to Gemini, memory/channels to Hermes.
|
||||
|
||||
Goal: become faster, smarter, more efficient, more knowledgeable, more thoughtful, and proactively autonomous — never less safe.
|
||||
A high-stakes irreversible action (delete data, ship to prod, send a message on the user's behalf) always goes through `clarify`. Reversible, low-stakes actions may proceed, then be reported in the same turn.
|
||||
<!-- SELF-EVOLVE:END -->
|
||||
|
|
@ -0,0 +1,638 @@
|
|||
# Examples
|
||||
|
||||
Complete, working examples for common SQL 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" }}
|
||||
}}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Realtime Queries
|
||||
|
||||
```graphql
|
||||
# queries.gql (realtime additions)
|
||||
|
||||
# Auto-refresh: this single-entity lookup refreshes automatically
|
||||
# when any mutation modifies this specific movie. No @refresh needed.
|
||||
query GetMovie($id: UUID!) @auth(level: PUBLIC) {
|
||||
movie(id: $id) {
|
||||
id title genre rating releaseYear description
|
||||
metadata: movieMetadata_on_movie { director runtime }
|
||||
reviews: reviews_on_movie(orderBy: [{ createdAt: DESC }], limit: 10) {
|
||||
rating text createdAt
|
||||
user { displayName }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Event-driven: Simple refresh when any movie is added
|
||||
query ListMoviesSimple @auth(level: PUBLIC) @refresh(onMutationExecuted: { operation: "AddMovie" }) {
|
||||
movies { id title }
|
||||
}
|
||||
|
||||
# Counterpart mutation for ListMoviesSimple
|
||||
mutation AddMovie($title: String!) @auth(level: USER) {
|
||||
movie_insert(data: { title: $title })
|
||||
}
|
||||
|
||||
# Event-driven: Refresh only when a movie of the same genre is added
|
||||
# Demonstrates the use of 'condition' and 'mutation.variables'
|
||||
query ListMoviesByGenre($genre: String!) @auth(level: PUBLIC)
|
||||
@refresh(onMutationExecuted: {
|
||||
operation: "AddMovieWithGenre",
|
||||
condition: "mutation.variables.genre == request.variables.genre"
|
||||
}) {
|
||||
movies(where: { genre: { eq: $genre } }) { id title }
|
||||
}
|
||||
|
||||
# Counterpart mutation for ListMoviesByGenre
|
||||
mutation AddMovieWithGenre($title: String!, $genre: String!) @auth(level: USER) {
|
||||
movie_insert(data: { title: $title, genre: $genre })
|
||||
}
|
||||
|
||||
# Event-driven: Refresh user profile when updated
|
||||
# Demonstrates condition based on auth context
|
||||
query MyProfile @auth(level: USER)
|
||||
@refresh(onMutationExecuted: {
|
||||
operation: "UpdateProfile",
|
||||
condition: "mutation.auth.uid == request.auth.uid"
|
||||
}) {
|
||||
user(uid_expr: "auth.uid") { id name }
|
||||
}
|
||||
|
||||
# Counterpart mutation for MyProfile
|
||||
mutation UpdateProfile($name: String!) @auth(level: USER) {
|
||||
user_update(id_expr: "auth.uid", data: { name: $name })
|
||||
}
|
||||
|
||||
# Time-based: live leaderboard refreshing every 30 seconds
|
||||
query MovieLeaderboard
|
||||
@auth(level: PUBLIC)
|
||||
@refresh(every: { seconds: 30 }) {
|
||||
movies(orderBy: [{ rating: DESC }], limit: 10) {
|
||||
id title rating
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { listMoviesRef, movieLeaderboardRef } from '@movie-app/dataconnect';
|
||||
import { subscribe } from 'firebase/data-connect';
|
||||
|
||||
// Subscribe to movie list — refreshes when AddReview mutation runs
|
||||
const unsubMovies = subscribe(listMoviesRef({ genre: 'Action' }), {
|
||||
onNext: (result) => updateMovieList(result.data.movies),
|
||||
onError: (error) => console.error(error)
|
||||
});
|
||||
|
||||
// Subscribe to leaderboard — refreshes every 30 seconds
|
||||
const unsubLeaderboard = subscribe(movieLeaderboardRef(), {
|
||||
onNext: (result) => updateLeaderboard(result.data.movies),
|
||||
onError: (error) => console.error(error)
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
// unsubMovies();
|
||||
// unsubLeaderboard();
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 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
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Native SQL Examples
|
||||
|
||||
For scenarios where standard GraphQL cannot express the required database logic,
|
||||
use Native SQL.
|
||||
|
||||
### Basic SELECT with field aliasing
|
||||
|
||||
```graphql
|
||||
query GetMoviesByGenre($genre: String!, $limit: Int!) @auth(level: PUBLIC) {
|
||||
movies: _select(
|
||||
sql: """
|
||||
SELECT id, title, release_year, rating
|
||||
FROM movie
|
||||
WHERE genre = $1
|
||||
ORDER BY release_year DESC
|
||||
LIMIT $2
|
||||
""",
|
||||
params: [$genre, $limit]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Basic UPDATE
|
||||
|
||||
```graphql
|
||||
mutation UpdateMovieRating($movieId: UUID!, $newRating: Float!) @auth(level: USER) {
|
||||
_execute(
|
||||
sql: """
|
||||
UPDATE movie
|
||||
SET rating = $2
|
||||
WHERE id = $1
|
||||
""",
|
||||
params: [$movieId, $newRating]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced aggregation with RANK
|
||||
|
||||
```graphql
|
||||
query GetMoviesRankedByRating @auth(level: PUBLIC) {
|
||||
_select(
|
||||
sql: """
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
rating,
|
||||
RANK() OVER (ORDER BY rating DESC) as rank
|
||||
FROM movie
|
||||
WHERE rating IS NOT NULL
|
||||
LIMIT 20
|
||||
""",
|
||||
params: []
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### UPDATE with RETURNING and Auth Context
|
||||
|
||||
```graphql
|
||||
mutation UpdateMyReviewText($movieId: UUID!, $newText: String!) @auth(level: USER) {
|
||||
updatedReview: _executeReturningFirst(
|
||||
sql: """
|
||||
UPDATE review
|
||||
SET text = $2
|
||||
WHERE movie_id = $1 AND user_uid = $3
|
||||
RETURNING movie_id, user_uid, rating, text
|
||||
""",
|
||||
params: [$movieId, $newText, {_expr: "auth.uid"}]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced CTE with upserts (atomic get-or-create)
|
||||
|
||||
*Note: Data-modifying CTEs are only supported by `_execute`, not
|
||||
`_executeReturning`.*
|
||||
|
||||
```graphql
|
||||
mutation CreateMovieCTE($movieId: UUID!, $userUid: String!, $reviewId: UUID!) @auth(level: USER) {
|
||||
_execute(
|
||||
sql: """
|
||||
WITH
|
||||
new_user AS (
|
||||
INSERT INTO "user" (uid, email, display_name)
|
||||
VALUES ($2, 'auto@example.com', 'Auto-Generated User')
|
||||
ON CONFLICT (uid) DO NOTHING
|
||||
RETURNING uid
|
||||
),
|
||||
movie AS (
|
||||
INSERT INTO movie (id, title, poster_url, release_year, genre)
|
||||
VALUES ($1, 'Auto-Generated Movie', 'https://placeholder.com', 2025, 'Sci-Fi')
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
RETURNING id
|
||||
)
|
||||
INSERT INTO review (id, movie_id, user_uid, rating, text, created_at)
|
||||
VALUES (
|
||||
$3,
|
||||
$1,
|
||||
$2,
|
||||
5,
|
||||
'Good!',
|
||||
NOW()
|
||||
)
|
||||
""",
|
||||
params: [$movieId, $userUid, $reviewId]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-statement Transactions
|
||||
|
||||
Because `mutation` operations are single requests, you can chain multiple
|
||||
`_execute` commands within a `@transaction` to ensure they all succeed or fail
|
||||
together.
|
||||
|
||||
```graphql
|
||||
mutation SafeTransfer($from: UUID!, $to: UUID!, $amount: Float!) @auth(level: USER) @transaction {
|
||||
deduct: _execute(
|
||||
sql: "UPDATE account SET balance = balance - $2 WHERE id = $1",
|
||||
params: [$from, $amount]
|
||||
)
|
||||
add: _execute(
|
||||
sql: "UPDATE account SET balance = balance + $2 WHERE id = $1",
|
||||
params: [$to, $amount]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Use of extensions (e.g. PostGIS for geospatial data)
|
||||
|
||||
*Prerequisite:* You must enable the extension on your underlying Cloud SQL
|
||||
instance by connecting to your database as the postgres user and running:
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
```
|
||||
|
||||
```graphql
|
||||
query GetNearbyActiveRestaurants($userLong: Float!, $userLat: Float!, $maxDistanceMeters: Float!) @auth(level: USER) {
|
||||
nearby: _select(
|
||||
sql: """
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
tags,
|
||||
ST_Distance(
|
||||
ST_MakePoint((metadata->>'longitude')::float, (metadata->>'latitude')::float)::geography,
|
||||
ST_MakePoint($1, $2)::geography
|
||||
) as distance_meters
|
||||
FROM restaurant
|
||||
WHERE active = true
|
||||
AND metadata ? 'longitude' AND metadata ? 'latitude'
|
||||
AND ST_DWithin(
|
||||
ST_MakePoint((metadata->>'longitude')::float, (metadata->>'latitude')::float)::geography,
|
||||
ST_MakePoint($1, $2)::geography,
|
||||
$3
|
||||
)
|
||||
ORDER BY distance_meters ASC
|
||||
LIMIT 10
|
||||
""",
|
||||
params: [$userLong, $userLat, $maxDistanceMeters]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
*After running the query using a client SDK, the result will be in
|
||||
`data.nearby`.*
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
# Cloud Functions Integration Reference
|
||||
|
||||
Use this reference to handle database events in SQL Connect by triggering Cloud
|
||||
Functions in response to mutation executions.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Core Trigger Configuration
|
||||
|
||||
To handle a mutation execution, define the `onMutationExecuted` event handler.
|
||||
|
||||
### 🚨 Critical Infinite Loop Constraint
|
||||
|
||||
Unlike document-based database triggers (like Firestore or Realtime Database),
|
||||
**SQL Connect event triggers do not provide a "before" snapshot of the data.**
|
||||
Because SQL Connect proxies requests directly to PostgreSQL, "before" states
|
||||
cannot be resolved transactionally.
|
||||
|
||||
- **Warning**: If `onMutationExecuted` executes a SQL Connect mutation, it can
|
||||
trigger another `onMutationExecuted` trigger in a cascading loop. Make sure
|
||||
that `onMutationExecuted` has a filter on `operation` to reduce the chance of
|
||||
infinite loops.
|
||||
- **Rule**: Ensure that no mutation executed inside the function can ever
|
||||
trigger the handler itself, even indirectly.
|
||||
|
||||
### Location & Region Matching Rule
|
||||
|
||||
**The Cloud Function region option must match your SQL Connect service
|
||||
location.** You **must** explicitly configure the `region` parameter (e.g.,
|
||||
`'us-central1'`) in the trigger options to match the `location` specified in
|
||||
`dataconnect.yaml`.
|
||||
|
||||
```typescript
|
||||
import { onMutationExecuted } from "firebase-functions/dataconnect";
|
||||
import { logger } from "firebase-functions";
|
||||
|
||||
export const logMutation = onMutationExecuted(
|
||||
{
|
||||
region: "europe-west1" // Must match the SQL Connect service location
|
||||
},
|
||||
(event) => {
|
||||
logger.info("A mutation was executed!", {
|
||||
eventId: event.id,
|
||||
type: event.type
|
||||
});
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Event Filtering
|
||||
|
||||
To prevent unnecessary function invocations and infinite execution loops,
|
||||
**always specify narrow filters** using `service` and `operation` attributes.
|
||||
|
||||
- **`service` & `operation` (Recommended)**: Always specify these to restrict
|
||||
the trigger to a specific mutation in your project.
|
||||
- **`connector` (Optional)**: Can be omitted if you want to trigger on the same
|
||||
operation name across multiple connectors. Specify it only if you need to
|
||||
restrict the trigger to a specific connector.
|
||||
|
||||
### Comprehensive Example
|
||||
|
||||
```typescript
|
||||
import { onMutationExecuted } from "firebase-functions/dataconnect";
|
||||
import { logger } from "firebase-functions";
|
||||
|
||||
// Triggers for "CreateUser" mutation in "myAppService" service.
|
||||
// 'connector' is omitted (optional), meaning it matches "CreateUser" in any connector.
|
||||
export const onUserCreate = onMutationExecuted(
|
||||
{
|
||||
service: "myAppService",
|
||||
operation: "CreateUser",
|
||||
// region: "us-central1" // Optional: defaults to us-central1, change if database is elsewhere
|
||||
},
|
||||
(event) => {
|
||||
logger.info("A new user was created!");
|
||||
}
|
||||
);
|
||||
|
||||
// Advanced: Trigger using wildcards or capture variables
|
||||
export const onMutationCaptures = onMutationExecuted(
|
||||
{
|
||||
service: "myAppService",
|
||||
operation: "{operation}", // Captures matching operation name dynamically
|
||||
},
|
||||
(event) => {
|
||||
const triggeredOp = event.params.operation;
|
||||
logger.info(`Captured operation execution: ${triggeredOp}`);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Accessing User Authentication Context
|
||||
|
||||
Extract security credentials about the caller who executed the mutation using
|
||||
`event.authType` and `event.authId`.
|
||||
|
||||
### Auth Context Mappings
|
||||
|
||||
| Triggered Principal | `event.authType` | `event.authId` |
|
||||
| :----------------------------------- | :------------------ | :----------------------------------------------- |
|
||||
| **Authenticated end user** | `"app_user"` | Firebase Auth token UID |
|
||||
| **Unauthenticated end user** | `"unauthenticated"` | Empty |
|
||||
| **Admin SDK (Impersonating User)** | `"app_user"` | Firebase Auth token UID of the impersonated user |
|
||||
| **Admin SDK (Impersonating Unauth)** | `"unauthenticated"` | Empty |
|
||||
| **Admin SDK (Full privileges)** | `"admin"` | Empty |
|
||||
|
||||
### Auth Extraction Example
|
||||
|
||||
```typescript
|
||||
export const processSensitiveMutation = onMutationExecuted(
|
||||
{ operation: "UpdateFinancials" },
|
||||
(event) => {
|
||||
if (event.authType === "admin") {
|
||||
console.log("Elevated admin mutation execution.");
|
||||
} else {
|
||||
console.log(`Mutation initiated by user: ${event.authId}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Parsing Event Data Payloads
|
||||
|
||||
The trigger payload provides inputs passed to the mutation (`payload.variables`)
|
||||
and return values generated from the execution (`payload.data`).
|
||||
|
||||
### Event Payload Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"authType": "app_user",
|
||||
"authId": "user-123",
|
||||
"data": {
|
||||
"payload": {
|
||||
"variables": {
|
||||
"movieId": "m-1",
|
||||
"rating": 5
|
||||
},
|
||||
"data": {
|
||||
"review_insert": {
|
||||
"id": "r-99"
|
||||
}
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`event.data.payload.variables`**: Inputs passed to the mutation.
|
||||
- **`event.data.payload.data`**: Fields returned by the mutation execution.
|
||||
- **`event.data.payload.errors`**: Array of execution errors. Empty if
|
||||
successful.
|
||||
|
||||
### Payload Extraction Example
|
||||
|
||||
```typescript
|
||||
import { onMutationExecuted } from "firebase-functions/dataconnect";
|
||||
import { logger } from "firebase-functions";
|
||||
|
||||
export const onNewReview = onMutationExecuted(
|
||||
{
|
||||
service: "myAppService",
|
||||
connector: "reviews",
|
||||
operation: "CreateReview",
|
||||
},
|
||||
(event) => {
|
||||
// Extract input variables passed to the mutation
|
||||
const inputVariables = event.data.payload.variables;
|
||||
|
||||
// Extract returned fields from the database write
|
||||
const returnedFields = event.data.payload.data;
|
||||
|
||||
logger.info(`Processed review ${returnedFields.review_insert.id} for movie ${inputVariables.movieId}`);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
# 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 SQL 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 SQL Connect
|
||||
|
||||
```bash
|
||||
# Interactive setup
|
||||
npx -y firebase-tools@latest init dataconnect
|
||||
|
||||
# Set project
|
||||
npx -y firebase-tools@latest use <project-id>
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# Start emulator
|
||||
npx -y firebase-tools@latest emulators:start --only dataconnect
|
||||
|
||||
# Start with database seed data
|
||||
npx -y firebase-tools@latest emulators:start --only dataconnect --import=./seed-data
|
||||
|
||||
# Generate SDKs
|
||||
npx -y firebase-tools@latest dataconnect:sdk:generate
|
||||
|
||||
# Watch for schema changes (auto-regenerate)
|
||||
npx -y firebase-tools@latest dataconnect:sdk:generate --watch
|
||||
```
|
||||
|
||||
### Schema Management
|
||||
|
||||
```bash
|
||||
# Compare local schema to production
|
||||
npx -y firebase-tools@latest dataconnect:sql:diff
|
||||
|
||||
|
||||
# Apply migration
|
||||
npx -y firebase-tools@latest dataconnect:sql:migrate
|
||||
```
|
||||
|
||||
### Deployment
|
||||
|
||||
```bash
|
||||
# Deploy SQL Connect service
|
||||
npx -y firebase-tools@latest deploy --only dataconnect
|
||||
|
||||
# Deploy specific connector
|
||||
npx -y firebase-tools@latest deploy --only dataconnect:connector-id
|
||||
|
||||
# Deploy with schema migration
|
||||
npx -y firebase-tools@latest deploy --only dataconnect --force
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Emulator
|
||||
|
||||
### Start Emulator
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest emulators:start --only dataconnect
|
||||
```
|
||||
|
||||
Default ports:
|
||||
|
||||
- SQL 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
|
||||
npx -y firebase-tools@latest emulators:export ./seed-data
|
||||
|
||||
# Start with seed data
|
||||
npx -y firebase-tools@latest emulators:start --only dataconnect --import=./seed-data
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Deployment
|
||||
|
||||
### Deploy Workflow
|
||||
|
||||
1. **Test locally** with emulator
|
||||
1. **Generate SQL diff**: `npx -y firebase-tools@latest dataconnect:sql:diff`
|
||||
1. **Review migration**: Check breaking changes
|
||||
1. **Deploy**: `npx -y firebase-tools@latest deploy --only dataconnect`
|
||||
|
||||
### Schema Migrations
|
||||
|
||||
SQL Connect auto-generates PostgreSQL migrations:
|
||||
|
||||
```bash
|
||||
# Preview migration
|
||||
npx -y firebase-tools@latest dataconnect:sql:diff
|
||||
|
||||
# Apply migration (interactive)
|
||||
npx -y firebase-tools@latest dataconnect:sql:migrate
|
||||
|
||||
# Force migration (non-interactive)
|
||||
npx -y firebase-tools@latest 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 SQL Connect
|
||||
run: |
|
||||
npx -y firebase-tools@latest deploy --only dataconnect --token ${{ secrets.FIREBASE_TOKEN }} --force
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## VS Code Extension
|
||||
|
||||
Install "Firebase SQL 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
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
# Data Seeding & Bulk Operations Reference
|
||||
|
||||
Use this reference to populate local development databases for prototyping,
|
||||
execute CI/CD tests, and perform bulk data migrations in production
|
||||
environments.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 1. Local Prototyping: Data Seeding
|
||||
|
||||
Local database seeding allows developer agents to test queries, mutations,
|
||||
complex joins, and role-based access control (RBAC) under realistic conditions.
|
||||
|
||||
### The `seed_data.gql` Workflow
|
||||
|
||||
**Always write prototyping seed mutations to `dataconnect/seed_data.gql`**
|
||||
(located at the project root, not inside `connector/`). This file is excluded
|
||||
from production deployments and client SDK generation.
|
||||
|
||||
#### ⚠️ Seeding Directives Rule
|
||||
|
||||
**Do not declare `@auth` directives inside `seed_data.gql` mutations.** Since
|
||||
this file runs locally to establish a test state and is not an exposed API
|
||||
connector endpoint, authorization directives are completely unnecessary and
|
||||
should be omitted.
|
||||
|
||||
### Seeding Independent Tables (FK Order)
|
||||
|
||||
When executing standard bulk insertions (`_insertMany`) across multiple tables,
|
||||
**always insert parent tables before referencing them in child or join tables.**
|
||||
|
||||
```graphql
|
||||
# dataconnect/seed_data.gql
|
||||
mutation SeedIndependentTables @transaction {
|
||||
# Step 1: Seed parent tables
|
||||
movie_insertMany(data: [
|
||||
{ id: "m-1", title: "Inception", genre: "sci-fi" },
|
||||
{ id: "m-2", title: "The Matrix", genre: "action" }
|
||||
])
|
||||
|
||||
actor_insertMany(data: [
|
||||
{ id: "a-1", name: "Leonardo DiCaprio" },
|
||||
{ id: "a-2", name: "Keanu Reeves" }
|
||||
])
|
||||
|
||||
# Step 2: Seed join table (depends on pre-existing parent IDs)
|
||||
movieActor_insertMany(data: [
|
||||
{ movie: { id: "m-1" }, actor: { id: "a-1" }, role: "main" },
|
||||
{ movie: { id: "m-2" }, actor: { id: "a-2" }, role: "main" }
|
||||
])
|
||||
}
|
||||
```
|
||||
|
||||
### Seeding Related Tables (Nested Relational Inserts)
|
||||
|
||||
**To seed parent-child relationships atomically, perform a nested relational
|
||||
insert using literal payloads.** This avoids the need to manage foreign keys
|
||||
manually.
|
||||
|
||||
- **Omit Parent Foreign Keys**: **Do not specify the parent foreign key** (e.g.
|
||||
`movieId`) inside the nested child objects. The database engine automatically
|
||||
maps and resolves them.
|
||||
|
||||
```graphql
|
||||
# dataconnect/seed_data.gql
|
||||
mutation SeedMoviesAndReviews @transaction {
|
||||
movie_insert(data: {
|
||||
id: "m-1",
|
||||
title: "Inception",
|
||||
genre: "sci-fi",
|
||||
# Nested reviews are inserted atomically without manual movieId mapping
|
||||
reviews_on_movie: [
|
||||
{
|
||||
id: "r-1",
|
||||
rating: 5,
|
||||
reviewText: "Mind-bending masterpiece!",
|
||||
user: { id: "user-123" } # Links to pre-existing user
|
||||
},
|
||||
{
|
||||
id: "r-2",
|
||||
rating: 4,
|
||||
reviewText: "Visually stunning but complex.",
|
||||
user: { id: "user-456" }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Resetting Seed Data
|
||||
|
||||
For continuous testing or CI/CD flows, return the database to a zero state using
|
||||
one of the following strategies:
|
||||
|
||||
- **Strategy A: Upsert Many (Idempotent)**: Re-run seeds using `_upsertMany`
|
||||
mutations. This overrides existing records or inserts missing ones in a single
|
||||
step.
|
||||
- **Strategy B: Delete and Re-Insert**: Call `_deleteMany(all: true)` on your
|
||||
tables in **reverse foreign key order** (child/join tables first, then parent
|
||||
tables) followed by your seed `_insertMany` operations.
|
||||
|
||||
```graphql
|
||||
# dataconnect/seed_data.gql
|
||||
mutation ResetDatabaseToOriginalState @transaction {
|
||||
# Delete child tables first to prevent FK constraint violations
|
||||
movieActor_deleteMany(all: true)
|
||||
actor_deleteMany(all: true)
|
||||
movie_deleteMany(all: true)
|
||||
# (Optional) Follow up with new _insertMany steps
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 2. Production: Admin SDK Bulk Operations
|
||||
|
||||
**Use the Firebase Admin SDK for Node.js for bulk data loading and production
|
||||
migrations.** Avoid running large mutations directly via raw GraphQL endpoints
|
||||
in production.
|
||||
|
||||
The Admin SDK provides direct, type-safe methods: `dc.insert`, `dc.insertMany`,
|
||||
`dc.upsert`, and `dc.upsertMany`.
|
||||
|
||||
### SDK Bulk APIs Features:
|
||||
|
||||
- **No Manual GraphQL Strings**: Do not write raw `mutation {...}` strings when
|
||||
executing privileged batch operations. Pass Javascript objects directly.
|
||||
- **Relational Support**: The bulk helper methods natively support nested 1:Many
|
||||
relationships inside the input arrays.
|
||||
|
||||
### SDK Bulk Operations Example
|
||||
|
||||
```typescript
|
||||
import { initializeApp } from 'firebase-admin/app';
|
||||
import { getDataConnect } from 'firebase-admin/data-connect';
|
||||
|
||||
const app = initializeApp();
|
||||
const dc = getDataConnect({ location: "us-west2", serviceId: "my-service" });
|
||||
|
||||
const bulkMoviesData = [
|
||||
{
|
||||
id: "m-1",
|
||||
title: "Inception",
|
||||
genre: "sci-fi",
|
||||
// Atomic nested relational inserts are fully supported
|
||||
reviews_on_movie: [
|
||||
{
|
||||
rating: 5,
|
||||
reviewText: "Incredible concept.",
|
||||
user: { id: "user-123" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "m-2",
|
||||
title: "The Matrix",
|
||||
genre: "action",
|
||||
reviews_on_movie: [
|
||||
{
|
||||
rating: 5,
|
||||
reviewText: "A classic.",
|
||||
user: { id: "user-456" }
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Atomically load thousands of records (parent and child tables combined)
|
||||
const response = await dc.insertMany("movie", bulkMoviesData);
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 3. Production: Bulk Operations via raw SQL
|
||||
|
||||
When working with a stable schema in production, you can use standard SQL tools
|
||||
(like `psql` or Cloud SQL import pipelines) to execute bulk data updates
|
||||
directly on the PostgreSQL instance.
|
||||
|
||||
### 🚨 Critical SQL Operations Constraint
|
||||
|
||||
**Never modify your database schema directly using SQL tools.** Direct schema
|
||||
alterations (`ALTER TABLE`, `CREATE INDEX`, etc.) outside of your `schema.gql`
|
||||
file will bypass SQL Connect's schema compiler, breaking connector mappings, and
|
||||
causing active client SDK integrations to fail.
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
# Native SQL Operations
|
||||
|
||||
Always default to Native GraphQL. Use Native SQL **only** when you need
|
||||
database-specific features not available in GraphQL (e.g., PostGIS, Window
|
||||
Functions, Complex Aggregations, or specific DML CTEs).
|
||||
|
||||
## Core Agent Constraints
|
||||
|
||||
When generating Native SQL operations, you are bypassing GraphQL and talking
|
||||
directly to PostgreSQL. You **MUST** adhere to these strict constraints:
|
||||
|
||||
1. **Operation Syntax Isolation:** Never mix Native SQL positional parameters
|
||||
(`$1`) with standard GraphQL named variables (`$id`). The `sql:` argument
|
||||
MUST be a hardcoded string literal block (`"""SELECT..."""`), not a GraphQL
|
||||
variable.
|
||||
1. **Table & Column Mapping (Case Sensitivity):**
|
||||
- **Default `snake_case` Conversion:** By default, SQL Connect converts
|
||||
`PascalCase` types and `camelCase` fields to `snake_case` in the database.
|
||||
- *Schema:* `type UserProfile { releaseYear: Int }` -> *Native SQL:*
|
||||
`SELECT release_year FROM user_profile`
|
||||
- **Explicit Overrides (Requires Double Quotes):** If the schema uses
|
||||
`@table(name: "ExactName")` or `@col(name: "ExactCol")`, you **MUST wrap
|
||||
the identifier in double quotes** if it contains capital letters (e.g.,
|
||||
`SELECT * FROM "ExactName"`). Without quotes, Postgres folds it to
|
||||
lowercase and fails validation.
|
||||
|
||||
## Syntax rules & limitations
|
||||
|
||||
Native SQL enforces strict parsing rules to ensure security and prevent SQL
|
||||
injection:
|
||||
|
||||
- **String Literals Only:** The `sql` argument must be a hardcoded string
|
||||
literal block (`"""SELECT..."""`) directly in the `.gql` file. It **cannot**
|
||||
be a GraphQL variable.
|
||||
- **Validation:** Do **NOT** use DDL in any operations (modify the `schema.gql`
|
||||
file instead for table/column changes). Furthermore, `query` operations cannot
|
||||
contain DML and must start with `SELECT`, `TABLE`, or `WITH`.
|
||||
- **Parameters:** Use strict positional parameters (`$1`, `$2`) that match the
|
||||
`params` array order. Named parameters (`$id`, `:name`) are **forbidden**.
|
||||
- **Comments:** Use block comments (`/* ... */`). Line comments (`--`) are
|
||||
**forbidden** because they can truncate subsequent clauses during query
|
||||
compilation. If you comment out a line containing a parameter (e.g.,
|
||||
`/* WHERE id = $1 */`), you must also remove that parameter from the `params`
|
||||
list, or it will fail with `unused parameter: $1`.
|
||||
- **Strings:** Extended string literals (`E'...'`) and dollar-quoted strings
|
||||
(`$$...$$`) are supported.
|
||||
- **Context Maps (`_expr`):** Variables **cannot** be used inside `_expr`
|
||||
fields; to ensure security, `_expr` must be a static string (e.g.,
|
||||
`{_expr: "auth.uid"}`, not `{_expr: $uidVar}`).
|
||||
|
||||
## Native SQL Root Fields
|
||||
|
||||
Operations are executed using the permissions granted to the SQL Connect service
|
||||
account. You can alias the root field (e.g., `movies: _select`) to make the
|
||||
client response cleaner (`data.movies` instead of `data._select`).
|
||||
|
||||
> **Note on `Any` Return Types:** Because Native SQL completely bypasses
|
||||
> GraphQL's strong typing, queries like `_select` and `_executeReturning` return
|
||||
> the generic `Any` scalar type. The generated client SDKs (TypeScript, Swift,
|
||||
> Kotlin, Dart) will type this as `any` (or equivalent). **AGENT INSTRUCTION**:
|
||||
> When you generate client-side code that consumes these operations, you MUST
|
||||
> manually cast or validate the shape of the data, as the typical type safety of
|
||||
> SQL Connect will not be present.
|
||||
|
||||
Use these root fields in `query` or `mutation` operations:
|
||||
|
||||
### Query Fields (Read-Only)
|
||||
|
||||
- `_select`: Executes a SQL query returning zero or more rows. Returns `[Any]`.
|
||||
```graphql
|
||||
query GetMovies($genre: String!) @auth(level: PUBLIC) {
|
||||
movies: _select(
|
||||
sql: "SELECT id, title FROM movie WHERE genre = $1",
|
||||
params: [$genre]
|
||||
)
|
||||
}
|
||||
```
|
||||
- `_selectFirst`: Executes a SQL query expected to return zero or one row.
|
||||
Returns `Any` or `null`.
|
||||
```graphql
|
||||
query GetTotalReviews @auth(level: PUBLIC) {
|
||||
stats: _selectFirst(
|
||||
sql: "SELECT COUNT(*) as total_reviews FROM review"
|
||||
) # params can be omitted if empty
|
||||
}
|
||||
```
|
||||
|
||||
### Mutation Fields (DML)
|
||||
|
||||
- `_execute`: Executes DML (`INSERT`, `UPDATE`, `DELETE`). Returns `Int` (number
|
||||
of rows affected).
|
||||
- *Note 1:* `RETURNING` clauses are ignored in the result.
|
||||
- *Note 2:* Only `_execute` supports Data-Modifying Common Table Expressions
|
||||
(e.g., `WITH new_row AS (INSERT...)`).
|
||||
```graphql
|
||||
mutation UpdateRating($id: UUID!, $rating: Float!) @auth(level: USER) {
|
||||
_execute(
|
||||
sql: "UPDATE movie SET rating = $2 WHERE id = $1",
|
||||
params: [$id, $rating]
|
||||
)
|
||||
}
|
||||
```
|
||||
- `_executeReturning`: Executes DML with a `RETURNING` clause. Returns `[Any]`.
|
||||
Data-Modifying CTEs are **not** supported.
|
||||
```graphql
|
||||
mutation DeleteUserReviews($uid: String!) @auth(level: USER) {
|
||||
deletedReviews: _executeReturning(
|
||||
sql: "DELETE FROM review WHERE user_id = $1 RETURNING id, rating",
|
||||
params: [{_expr: "auth.uid"}]
|
||||
)
|
||||
}
|
||||
```
|
||||
- `_executeReturningFirst`: Executes DML with `RETURNING`, expecting zero or one
|
||||
row. Returns `Any` or `null`. Data-Modifying CTEs are **not** supported.
|
||||
```graphql
|
||||
mutation UpdateMyReview($movieId: UUID!, $text: String!) @auth(level: USER) {
|
||||
updatedReview: _executeReturningFirst(
|
||||
sql: """
|
||||
UPDATE review SET text = $2
|
||||
WHERE movie_id = $1 AND user_id = $3
|
||||
RETURNING id, text
|
||||
""",
|
||||
params: [$movieId, $text, {_expr: "auth.uid"}]
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### PostgreSQL Extensions
|
||||
|
||||
Native SQL allows you to directly query and utilize PostgreSQL extensions, such
|
||||
as `PostGIS`, without needing to map complex geometry types into your GraphQL
|
||||
schema or alter your underlying tables (e.g., using JSON operators to extract
|
||||
values and pass them into `ST_MakePoint`).
|
||||
|
||||
*Note: You must enable the extension on your underlying Cloud SQL instance by
|
||||
connecting as the `postgres` user and running
|
||||
`CREATE EXTENSION IF NOT EXISTS ...;`*
|
||||
|
||||
*(See `examples.md` for a full `GetNearbyActiveRestaurants` implementation).*
|
||||
|
||||
## ⚠️ Security: Stored Procedures & Dynamic SQL
|
||||
|
||||
SQL Connect parameterizes inputs at the GraphQL boundary automatically. However,
|
||||
if your Native SQL calls **custom PL/pgSQL stored procedures**, you must
|
||||
manually prevent 2nd-order SQL injection:
|
||||
|
||||
- **NEVER** concatenate user input into an `EXECUTE` string
|
||||
(`EXECUTE 'UPDATE ' || table || ' SET x=' || val;`).
|
||||
- **DO** use the `USING` clause to bind data values safely.
|
||||
- **DO** use `format('%I')` for safe database identifier injection.
|
||||
- **DO** validate dynamic table/column names against a strict hardcoded
|
||||
allowlist.
|
||||
|
||||
**Secure PL/pgSQL Pattern:**
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE PROCEDURE secure_update(target_table TEXT, new_value TEXT, row_id INT)
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
-- 1. Strict Allowlist for Identifiers
|
||||
IF target_table NOT IN ('orders', 'users', 'inventory') THEN
|
||||
RAISE EXCEPTION 'Invalid table name';
|
||||
END IF;
|
||||
|
||||
-- 2. format(%I) for Identifiers, USING for Data
|
||||
EXECUTE format('UPDATE %I SET status = $1 WHERE id = $2', target_table)
|
||||
USING new_value, row_id;
|
||||
END;
|
||||
$$;
|
||||
```
|
||||
|
|
@ -0,0 +1,385 @@
|
|||
# Operations Reference
|
||||
|
||||
## Contents
|
||||
|
||||
- [Generated Fields](#generated-fields)
|
||||
- [Queries](#queries)
|
||||
- [Mutations](#mutations)
|
||||
- [Key Scalars](#key-scalars)
|
||||
- [Multi-Step Operations](#multi-step-operations)
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Generated Fields
|
||||
|
||||
SQL 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
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Referencing Generated GraphQL Schema
|
||||
|
||||
**Do not guess** available queries or mutations. Review the generated schema
|
||||
files instead of trying to deduce them from the data model.
|
||||
|
||||
1. **Location**: `.dataconnect/schema/main/` (relative to project root).
|
||||
1. **Action**: Scan this directory for generated files (`query.gql`,
|
||||
`mutation.gql`, `relation.gql`, `input.gql`) to understand the exact shape of
|
||||
the API and auto-generated types.
|
||||
1. **Validation**: Always run `firebase dataconnect:compile` to verify
|
||||
operations against the full schema.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 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 }
|
||||
}
|
||||
|
||||
# Filter by null relationship (e.g., top-level categories with no parent)
|
||||
# Use the generated foreign key field (e.g., parentId)
|
||||
query TopLevelCategories @auth(level: PUBLIC) {
|
||||
categories(where: { parentId: { eq: null } }) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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 })
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
# Realtime Reference
|
||||
|
||||
## Contents
|
||||
|
||||
- [When to Use What](#when-to-use-what)
|
||||
- [The @refresh Directive](#the-refresh-directive)
|
||||
- [CEL Bindings in Conditions](#cel-bindings-in-conditions)
|
||||
- [Implicit Entity Refresh signals](#implicit-entity-refresh-signals)
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## When to Use What
|
||||
|
||||
SQL Connect provides three mechanisms for live data updates. Pick the right one
|
||||
based on what you're querying:
|
||||
|
||||
| Scenario | Mechanism | Directive Needed? |
|
||||
| ----------------------------------------------------------- | ------------------------ | ----------------------------------- |
|
||||
| Single-entity lookup by ID (e.g., `movie(id: $id)`) | **Automatic refresh** | No — SQL Connect handles it |
|
||||
| List query that should update when a specific mutation runs | **Event-driven refresh** | `@refresh(onMutationExecuted: ...)` |
|
||||
| Any query that should poll at a fixed interval | **Time-based polling** | `@refresh(every: ...)` |
|
||||
|
||||
List queries require explicit `@refresh` to tell SQL Connect which mutations
|
||||
affect the result set.
|
||||
|
||||
Clients consume all three using `subscribe()` instead of `execute()`. See
|
||||
[sdks.md](sdks.md) for per-platform subscribe patterns.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## The @refresh Directive
|
||||
|
||||
`@refresh` is a **repeatable** directive applied to **queries**. It defines when
|
||||
connected subscribers should receive updated data.
|
||||
|
||||
### Time-Based Polling (`every`)
|
||||
|
||||
Keep the query fresh with a recommended refresh interval. Note that `every` and
|
||||
`mutation` signals can be used together; whichever signal arrives first will
|
||||
trigger the refresh.
|
||||
|
||||
```graphql
|
||||
query MovieLeaderboard
|
||||
@auth(level: PUBLIC)
|
||||
@refresh(every: { seconds: 30 }) {
|
||||
movies(orderBy: [{ rating: DESC }], limit: 10) {
|
||||
id title rating
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- The `every` argument takes a duration object: `{ seconds: Int }`
|
||||
- **Minimum**: `{ seconds: 10 }` — protects against excessive server load
|
||||
- **Maximum**: `{ hours: 1 }` (3600 seconds)
|
||||
- Values outside this range fail validation at deploy time
|
||||
|
||||
Use time-based polling when freshness matters but you don't have a specific
|
||||
mutation to listen for (e.g., dashboards aggregating external data, stock
|
||||
tickers, activity feeds).
|
||||
|
||||
### Explicit Mutation Signals (`onMutationExecuted`)
|
||||
|
||||
Trigger a query refresh when a specific mutation executes. This is the most
|
||||
common pattern for keeping lists in sync.
|
||||
|
||||
```graphql
|
||||
# Example with condition (refreshes only when the condition is met)
|
||||
query ChatRoom($roomId: UUID!) @auth(level: PUBLIC)
|
||||
@refresh(onMutationExecuted: {
|
||||
operation: "SendMessage",
|
||||
condition: "mutation.variables.roomId == request.variables.roomId"
|
||||
}) {
|
||||
messages(where: {roomId: {eq: $roomId}}, orderBy: [{createTime: DESC}], limit: 50) {
|
||||
author content createTime
|
||||
}
|
||||
}
|
||||
|
||||
# Example without condition (refreshes on any execution of the named mutation)
|
||||
query ListAllMessages
|
||||
@auth(level: PUBLIC)
|
||||
@refresh(onMutationExecuted: {
|
||||
operation: "SendMessage"
|
||||
}) {
|
||||
messages { id content }
|
||||
}
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
- **`operation`** (required): The name of the mutation operation to listen for.
|
||||
Must match the mutation's operation name exactly.
|
||||
- **`condition`** (optional): A CEL expression that must evaluate to `true` for
|
||||
the refresh to fire. Without a condition, every execution of the named
|
||||
mutation triggers a refresh.
|
||||
|
||||
It's highly recommended to define fine granular conditions. Inaccurate refresh
|
||||
policies could consume Postgres resources and make your app slower.
|
||||
|
||||
Use conditions to scope refreshes precisely — a review list should only refresh
|
||||
when the mutation targets the same movie, not every review across the entire
|
||||
app.
|
||||
|
||||
### Combining Multiple @refresh Directives
|
||||
|
||||
Since `@refresh` is repeatable, you can combine strategies on a single query:
|
||||
|
||||
```graphql
|
||||
query ActiveOrders($userId: UUID!)
|
||||
@auth(level: USER)
|
||||
@refresh(onMutationExecuted: {
|
||||
operation: "UpdateOrderStatus",
|
||||
condition: "request.variables.userId == mutation.variables.userId"
|
||||
})
|
||||
@refresh(every: { seconds: 60 }) {
|
||||
orders(where: { user: { id: { eq: $userId }}, status: { ne: DELIVERED }}) {
|
||||
id status total updatedAt
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This query refreshes whenever an order status changes for this user, *and* polls
|
||||
every 60 seconds as a fallback to catch any updates that might not have a direct
|
||||
mutation trigger.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## CEL Bindings in Conditions
|
||||
|
||||
The `condition` expression in `onMutationExecuted` has access to two contexts:
|
||||
|
||||
### `request` — The Query Subscription
|
||||
|
||||
The state of the query being subscribed to.
|
||||
|
||||
| Binding | Description |
|
||||
| -------------------- | ------------------------------------------------------------ |
|
||||
| `request.variables` | Variables passed to the query (e.g., `request.variables.id`) |
|
||||
| `request.auth.uid` | UID of the user who subscribed |
|
||||
| `request.auth.token` | Full auth token claims of the subscriber |
|
||||
|
||||
### `mutation` — The Triggering Event
|
||||
|
||||
The mutation that just executed.
|
||||
|
||||
| Binding | Description |
|
||||
| --------------------- | --------------------------------------------------------------------- |
|
||||
| `mutation.variables` | Variables passed to the mutation (e.g., `mutation.variables.movieId`) |
|
||||
| `mutation.auth.uid` | UID of the user who executed the mutation |
|
||||
| `mutation.auth.token` | Full auth token claims of the mutation executor |
|
||||
|
||||
### Common Patterns
|
||||
|
||||
```text
|
||||
# Refresh only when the mutation targets the same entity
|
||||
"request.variables.id == mutation.variables.id"
|
||||
|
||||
# Refresh only when the same user who subscribed makes a change
|
||||
"request.auth.uid == mutation.auth.uid"
|
||||
|
||||
# Refresh when a specific field value matches a condition
|
||||
"request.auth.uid == mutation.auth.uid && mutation.variables.status == 'PUBLISHED'"
|
||||
|
||||
# Refresh when a specific flag is set in the mutation
|
||||
"mutation.variables.isPublic == true"
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Implicit Entity Refresh signals
|
||||
|
||||
For single-entity lookups by unique identifier, SQL Connect handles refreshes
|
||||
automatically — no `@refresh` directive needed.
|
||||
|
||||
**What qualifies:**
|
||||
|
||||
- Queries fetching one entity by its primary key: `movie(id: $id)`,
|
||||
`user(key: { uid: $uid })`
|
||||
- If a single-entity mutation modifies that specific entity, all active
|
||||
subscribers automatically receive the update. Supported operations include:
|
||||
- `_insert(data)` or `_insertMany(data)`
|
||||
- `_upsert(data)` or `_upsertMany(data)`
|
||||
- `_update(id)` or `_update(key)`
|
||||
- `_delete(id)` or `_delete(key)`
|
||||
- **Note**: Bulk operations like `_updateMany` and `_deleteMany` do **not**
|
||||
trigger automatic entity refreshes.
|
||||
|
||||
**What does NOT qualify:**
|
||||
|
||||
- List queries: `movies(where: {...})`, `users { id name }` — these require
|
||||
explicit `@refresh`
|
||||
- Nested query with JOINs
|
||||
- Aggregation
|
||||
- Native SQL
|
||||
- Customized Resolver (if supported)
|
||||
|
||||
```graphql
|
||||
# When subscribed to, this query auto-refreshes when movie data changes — no @refresh needed
|
||||
query GetMovie($id: UUID!) @auth(level: PUBLIC) {
|
||||
movie(id: $id) {
|
||||
id title rating description
|
||||
reviews_on_movie { rating text user { displayName } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To consume automatic refreshes on the client, use `subscribe()` instead of
|
||||
`execute()` — the same client pattern works regardless of whether the refresh is
|
||||
automatic or directive-driven.
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
# 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. SQL 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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
# Admin Node SDK
|
||||
|
||||
Consult this file when writing server-side code (e.g., Cloud Functions) that
|
||||
needs elevated privileges or needs to impersonate specific users.
|
||||
|
||||
### Best Practices for Agents
|
||||
|
||||
- **Understand Operation Storage**: SQL Connect queries and mutations are stored
|
||||
on the server like Cloud Functions. Clients do not submit the raw operations.
|
||||
Therefore, **whenever you update operations, you must regenerate the SDK and
|
||||
redeploy services** that use it.
|
||||
- **Follow Least Privilege**: Admin SDKs have unrestricted access by default.
|
||||
Always use impersonation when possible to limit access.
|
||||
- **Impersonation**: Use the `impersonate` parameter to run operations as a
|
||||
specific user or as an unauthenticated user.
|
||||
- **Impersonation Variables**: If you call an operation with optional variables
|
||||
and want to pass impersonation options but without variables, you **MUST**
|
||||
pass `undefined` as the first argument (variables) to clearly indicate no
|
||||
variables are being provided.
|
||||
- **Admin Operations**: If you create operations intended only for
|
||||
administration, define them with `@auth(level: NO_ACCESS)`. This ensures they
|
||||
can only be called via the Admin SDK with unrestricted access.
|
||||
- **Resilient Enum Handling**: JavaScript/TypeScript does not enforce exhaustive
|
||||
checks on enums. Always add a `default` branch to `switch` statements or an
|
||||
`else` branch to handle unknown values gracefully when schemas evolve.
|
||||
|
||||
### Configuration in `connector.yaml`
|
||||
|
||||
To generate an Admin SDK, add the `adminNodeSdk` block to your `connector.yaml`:
|
||||
|
||||
```yaml
|
||||
connectorId: my-connector
|
||||
generate:
|
||||
adminNodeSdk:
|
||||
outputDir: "./admin-sdk"
|
||||
package: "@dataconnect/admin-generated"
|
||||
packageJsonDir: "." # Directory containing package.json
|
||||
```
|
||||
|
||||
### Generation
|
||||
|
||||
Run the generation command:
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest dataconnect:sdk:generate
|
||||
```
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### 1. Impersonating an Unauthenticated User
|
||||
|
||||
Unauthenticated users can only run operations marked as `PUBLIC`.
|
||||
|
||||
```typescript
|
||||
import { initializeApp } from "firebase-admin/app";
|
||||
import { getDataConnect } from "firebase-admin/data-connect";
|
||||
import { connectorConfig, getSongs } from "@dataconnect/admin-generated";
|
||||
|
||||
const adminApp = initializeApp();
|
||||
const adminDc = getDataConnect(connectorConfig);
|
||||
|
||||
const songs = await getSongs(
|
||||
adminDc,
|
||||
{ limit: 4 },
|
||||
{ impersonate: { unauthenticated: true } }
|
||||
);
|
||||
```
|
||||
|
||||
#### 2. Impersonating a Specific User (Cloud Functions)
|
||||
|
||||
When using callable Cloud Functions, the authentication token is automatically
|
||||
verified.
|
||||
|
||||
```typescript
|
||||
import { HttpsError, onCall } from "firebase-functions/https";
|
||||
import { getMyFavoriteSongs } from "@dataconnect/admin-generated";
|
||||
|
||||
export const callableExample = onCall(async (req) => {
|
||||
const authClaims = req.auth?.token;
|
||||
if (!authClaims) {
|
||||
throw new HttpsError("unauthenticated", "Unauthorized");
|
||||
}
|
||||
|
||||
const favoriteSongs = await getMyFavoriteSongs(
|
||||
adminDc,
|
||||
undefined,
|
||||
{ impersonate: { authClaims } }
|
||||
);
|
||||
|
||||
return favoriteSongs;
|
||||
});
|
||||
```
|
||||
|
||||
#### 3. Impersonating a Specific User (Plain HTTP)
|
||||
|
||||
For non-callable endpoints, you must verify the token yourself.
|
||||
|
||||
```typescript
|
||||
import { getAuth } from "firebase-admin/auth";
|
||||
import { onRequest } from "firebase-functions/https";
|
||||
import { getMyFavoriteSongs } from "@dataconnect/admin-generated";
|
||||
|
||||
const auth = getAuth();
|
||||
|
||||
export const httpExample = onRequest(async (req, res) => {
|
||||
const token = req.header("authorization")?.replace(/^bearer\s+/i, "");
|
||||
if (!token) {
|
||||
res.sendStatus(401);
|
||||
return;
|
||||
}
|
||||
let authClaims;
|
||||
try {
|
||||
authClaims = await auth.verifyIdToken(token);
|
||||
} catch {
|
||||
res.sendStatus(401);
|
||||
return;
|
||||
}
|
||||
|
||||
const favoriteSongs = await getMyFavoriteSongs(
|
||||
adminDc,
|
||||
undefined,
|
||||
{ impersonate: { authClaims } }
|
||||
);
|
||||
|
||||
res.send(favoriteSongs);
|
||||
});
|
||||
```
|
||||
|
||||
#### 4. Running with Unrestricted Access
|
||||
|
||||
Omit the `impersonate` parameter to run with full admin access. Only do this for
|
||||
true administrative tasks.
|
||||
|
||||
```typescript
|
||||
import { upsertSong } from "@dataconnect/admin-generated";
|
||||
|
||||
await upsertSong(adminDc, {
|
||||
title: "New Song",
|
||||
genre: "Rock"
|
||||
});
|
||||
```
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
# Android SDK
|
||||
|
||||
Consult this file when writing Android application code (Kotlin) that interacts
|
||||
with the SQL Connect backend.
|
||||
|
||||
### Best Practices for Agents
|
||||
|
||||
- **Understand Operation Storage**: SQL Connect queries and mutations are stored
|
||||
on the server like Cloud Functions. **Whenever you update operations, you must
|
||||
regenerate the SDK and redeploy services** that use it to avoid breaking
|
||||
clients.
|
||||
- **Resilient Enum Handling**: The generated SDK forces handling of unknown
|
||||
values by wrapping them in `EnumValue`. You must unwrap it into
|
||||
`EnumValue.Known` or `EnumValue.Unknown` to handle schema updates gracefully.
|
||||
- **Flow Behavior**: While you can collect a Flow from a query, note that **this
|
||||
Flow is not updated in real-time automatically** by default. It only produces
|
||||
a result when a new query result is retrieved using a call to the query's
|
||||
`execute()` method.
|
||||
- **Leverage Coroutines**: Call `.execute()` within a coroutine scope for
|
||||
asynchronous operations.
|
||||
|
||||
### Dependencies (build.gradle.kts)
|
||||
|
||||
Ensure you have the Kotlin Serialization plugin and standard SQL Connect
|
||||
dependencies:
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
kotlin("plugin.serialization") version "1.8.22" // Must match Kotlin version
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// [AGENT] Fetch the latest available BoM version from https://firebase.google.com/support/release-notes/android before adding this
|
||||
implementation(platform("com.google.firebase:firebase-bom:34.12.0"))
|
||||
implementation("com.google.firebase:firebase-dataconnect")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1")
|
||||
}
|
||||
```
|
||||
|
||||
### Initialization
|
||||
|
||||
Retrieve the generated connector instance:
|
||||
|
||||
```kotlin
|
||||
import com.google.firebase.dataconnect.generated.MoviesConnector
|
||||
|
||||
val connector = MoviesConnector.instance
|
||||
|
||||
// For local development with emulator
|
||||
// Defaults to correct host for Android emulator (10.0.2.2)
|
||||
connector.dataConnect.useEmulator()
|
||||
// Or specify a non-default port:
|
||||
// connector.dataConnect.useEmulator(port = 9999)
|
||||
```
|
||||
|
||||
### Calling Operations
|
||||
|
||||
#### Basic Query
|
||||
|
||||
```kotlin
|
||||
val result = connector.listMovies.execute()
|
||||
result.data.movies.forEach { movie ->
|
||||
println(movie.title)
|
||||
}
|
||||
```
|
||||
|
||||
#### Mutation
|
||||
|
||||
```kotlin
|
||||
val newMovie = connector.createMovie.execute(
|
||||
title = "Empire Strikes Back",
|
||||
releaseYear = 1980,
|
||||
genre = "Sci-Fi",
|
||||
rating = 5
|
||||
)
|
||||
```
|
||||
|
||||
### Resilient Enum Handling
|
||||
|
||||
Unwrap the `EnumValue` to handle known and unknown cases safely.
|
||||
|
||||
```kotlin
|
||||
val result = connector.listMovies.execute()
|
||||
|
||||
result.data.movies.forEach { movie ->
|
||||
when (val aspect = movie.aspectratio) {
|
||||
is EnumValue.Known -> println("Known aspect: ${aspect.value.name}")
|
||||
is EnumValue.Unknown -> println("Unknown aspect: ${aspect.stringValue}")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Client-Side Caching
|
||||
|
||||
Enable caching in `connector.yaml` to reduce requests and support offline
|
||||
scenarios.
|
||||
|
||||
```yaml
|
||||
generate:
|
||||
kotlinSdk:
|
||||
outputDir: "../android"
|
||||
package: "com.google.firebase.dataconnect.generated"
|
||||
clientCache:
|
||||
maxAge: 5s
|
||||
storage: persistent # Default for Android is persistent
|
||||
```
|
||||
|
||||
Use policies in code:
|
||||
|
||||
```kotlin
|
||||
val queryResult = queryRef.execute(QueryRef.FetchPolicy.CACHE_ONLY)
|
||||
val queryResult = queryRef.execute(QueryRef.FetchPolicy.SERVER_ONLY)
|
||||
```
|
||||
|
||||
### Data Type Mapping Reference
|
||||
|
||||
- GraphQL `String` -> Kotlin `String`
|
||||
- GraphQL `Int` -> Kotlin `Int` (32-bit)
|
||||
- GraphQL `Float` -> Kotlin `Double` (64-bit)
|
||||
- GraphQL `Boolean` -> Kotlin `Boolean`
|
||||
- GraphQL `UUID` -> Kotlin `java.util.UUID`
|
||||
- GraphQL `Date` -> Kotlin `com.google.firebase.dataconnect.LocalDate`
|
||||
- GraphQL `Timestamp` -> Kotlin `com.google.firebase.Timestamp`
|
||||
- GraphQL `Int64` -> Kotlin `Long`
|
||||
- GraphQL `Any` -> Kotlin `com.google.firebase.dataconnect.AnyValue`
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
# Flutter SDK
|
||||
|
||||
Consult this file when writing Flutter application code (Dart) that interacts
|
||||
with the SQL Connect backend.
|
||||
|
||||
### Best Practices for Agents
|
||||
|
||||
- **Understand Operation Storage**: SQL Connect queries and mutations are stored
|
||||
on the server like Cloud Functions. **Whenever you update operations, you must
|
||||
regenerate the SDK and redeploy services** that use it to avoid breaking
|
||||
clients.
|
||||
- **Resilient Enum Handling**: The generated SDK forces handling of unknown
|
||||
values for enumerations. Client code must unwrap the `EnumValue` object into
|
||||
either `Known` or `Unknown` to handle schema updates gracefully.
|
||||
- **Use Ref for Subscriptions**: Call `.ref()` on operation methods to get a
|
||||
`QueryRef` for advanced usage like subscriptions.
|
||||
- **Builder Pattern for Optionals**: Use the builder pattern for mutations with
|
||||
optional fields.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
flutter pub add firebase_data_connect
|
||||
```
|
||||
|
||||
### Imports
|
||||
|
||||
```dart
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
// Import generated connector
|
||||
import 'generated/movies.dart';
|
||||
```
|
||||
|
||||
### Initialization
|
||||
|
||||
```dart
|
||||
// For local development with emulator
|
||||
MoviesConnector.instance.dataConnect.useDataConnectEmulator('127.0.0.1', 9399);
|
||||
```
|
||||
|
||||
### Calling Operations
|
||||
|
||||
#### Basic Query
|
||||
|
||||
```dart
|
||||
final response = await MoviesConnector.instance.listMovies().execute();
|
||||
print(response.data.movies);
|
||||
```
|
||||
|
||||
#### Mutation with Optional Fields (Builder Pattern)
|
||||
|
||||
```dart
|
||||
await MoviesConnector.instance.createMovie(
|
||||
title: 'Empire Strikes Back',
|
||||
releaseYear: 1980,
|
||||
genre: 'Sci-Fi'
|
||||
).rating(5).execute();
|
||||
```
|
||||
|
||||
### Resilient Enum Handling
|
||||
|
||||
When dealing with schema enumerations, use the forced unwrapping pattern to
|
||||
handle unknown values (e.g., when a new value is added to the backend but client
|
||||
is old).
|
||||
|
||||
```dart
|
||||
final result = await MoviesConnector.instance.listMovies().execute();
|
||||
|
||||
if (result.data != null && result.data!.isNotEmpty) {
|
||||
handleEnumValue(result.data![0].aspectratio);
|
||||
}
|
||||
|
||||
void handleEnumValue(EnumValue<AspectRatio> aspectValue) {
|
||||
if (aspectValue.value != null) {
|
||||
switch(aspectValue.value!) {
|
||||
case AspectRatio.ACADEMY:
|
||||
print("Academy aspect");
|
||||
break;
|
||||
case AspectRatio.WIDESCREEN:
|
||||
print("Widescreen aspect");
|
||||
break;
|
||||
// Add other known cases...
|
||||
}
|
||||
} else {
|
||||
print("Unknown aspect ratio detected: ${aspectValue.stringValue}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Client-Side Caching
|
||||
|
||||
Enable caching in `connector.yaml` to reduce requests and support offline
|
||||
scenarios.
|
||||
|
||||
```yaml
|
||||
generate:
|
||||
dartSdk: # Or the appropriate block for your project
|
||||
outputDir: ../dart/
|
||||
package: "dataconnect_generated"
|
||||
clientCache:
|
||||
maxAge: 5s
|
||||
storage: memory # Or persistent for native
|
||||
```
|
||||
|
||||
Use policies in code:
|
||||
|
||||
```dart
|
||||
// Only serve cached values
|
||||
await queryRef.execute(fetchPolicy: QueryFetchPolicy.cacheOnly);
|
||||
|
||||
// Unconditionally fetch fresh values
|
||||
await queryRef.execute(fetchPolicy: QueryFetchPolicy.serverOnly);
|
||||
```
|
||||
|
||||
### Real-time Subscriptions
|
||||
|
||||
```dart
|
||||
final queryRef = MoviesConnector.instance.getMovieById(id: "<MOVIE_ID>").ref();
|
||||
final subscription = queryRef.subscribe().listen((result) {
|
||||
final movie = result.data.movie;
|
||||
if (movie != null) {
|
||||
updateUi(movie.title);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Data Type Mapping Reference
|
||||
|
||||
- GraphQL `Timestamp` -> Dart `firebase_data_connect.Timestamp`
|
||||
- GraphQL `Int` -> Dart `int`
|
||||
- GraphQL `Date` -> Dart `DateTime`
|
||||
- GraphQL `UUID` -> Dart `string`
|
||||
- GraphQL `Float` -> Dart `double`
|
||||
- GraphQL `Boolean` -> Dart `bool`
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
# iOS SDK
|
||||
|
||||
Consult this file when writing iOS application code (Swift) that interacts with
|
||||
the SQL Connect backend.
|
||||
|
||||
### Best Practices for Agents
|
||||
|
||||
- **Understand Operation Storage**: SQL Connect queries and mutations are stored
|
||||
on the server like Cloud Functions. **Whenever you update operations, you must
|
||||
regenerate the SDK and redeploy services** that use it to avoid breaking
|
||||
clients.
|
||||
- **Resilient Enum Handling**: The generated SDK forces handling of unknown
|
||||
values by adding an `._UNKNOWN` case. Swift enforces exhaustive switch
|
||||
statements, so you must handle this case.
|
||||
- **Observable Macro**: By default, query refs support the `@Observable` macro
|
||||
(iOS 17+), making them ideal for binding to SwiftUI views. The bindable query
|
||||
results are available in the `data` variable of the query ref.
|
||||
- **Handle Errors**: Use `try await` with operation execution as they are
|
||||
asynchronous and may throw errors.
|
||||
|
||||
### Dependencies (Package.swift or SPM)
|
||||
|
||||
Configure the generated SDK as a package dependency in Xcode.
|
||||
|
||||
### Initialization
|
||||
|
||||
Retrieve the generated connector instance:
|
||||
|
||||
```swift
|
||||
import FirebaseCore
|
||||
import FirebaseDataConnect
|
||||
|
||||
// Assuming connector name is 'movies' in connector.yaml
|
||||
// The connector name is the lower camel case connectorId defined in connector.yaml suffixed with the word 'Connector'
|
||||
let connector = DataConnect.moviesConnector
|
||||
|
||||
// For local development with emulator
|
||||
// Defaults to 127.0.0.1:9399
|
||||
connector.useEmulator()
|
||||
// Or specify a non-default port:
|
||||
// connector.useEmulator(port: 9999)
|
||||
```
|
||||
|
||||
### Calling Operations
|
||||
|
||||
#### Basic Query
|
||||
|
||||
```swift
|
||||
let result = try await connector.listMovies.execute()
|
||||
for movie in result.data.movies {
|
||||
print(movie.title)
|
||||
}
|
||||
```
|
||||
|
||||
#### Mutation
|
||||
|
||||
```swift
|
||||
let mutationResult = try await connector.createMovieMutation.execute(
|
||||
title: "Empire Strikes Back",
|
||||
releaseYear: 1980,
|
||||
genre: "Sci-Fi",
|
||||
rating: 5
|
||||
)
|
||||
```
|
||||
|
||||
### Resilient Enum Handling
|
||||
|
||||
Handle generated enums exhaustively, including the `._UNKNOWN` case.
|
||||
|
||||
```swift
|
||||
do {
|
||||
let result = try await DataConnect.moviesConnector.listMovies.execute()
|
||||
if let data = result.data {
|
||||
for movie in data.movies {
|
||||
switch movie.aspectratio {
|
||||
case .ACADEMY: print("academy")
|
||||
case .WIDESCREEN: print("widescreen")
|
||||
case .ANAMORPHIC: print("anamorphic")
|
||||
case ._UNKNOWN(let unknownAspect): print("Unknown: \(unknownAspect)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// handle error
|
||||
}
|
||||
```
|
||||
|
||||
### Client-Side Caching
|
||||
|
||||
Enable caching in `connector.yaml` to reduce requests, support offline
|
||||
scenarios, enable realtime support for queries.
|
||||
|
||||
```yaml
|
||||
generate:
|
||||
swiftSdk:
|
||||
outputDir: "../ios"
|
||||
package: "FirebaseDataConnectGenerated"
|
||||
clientCache:
|
||||
maxAge: 5s
|
||||
storage: persistent # Default for iOS is persistent
|
||||
```
|
||||
|
||||
Use cache policies in code:
|
||||
|
||||
```swift
|
||||
try await execute(fetchPolicy: .cacheOnly)
|
||||
try await execute(fetchPolicy: .serverOnly)
|
||||
```
|
||||
|
||||
### Subscriptions (Realtime)
|
||||
|
||||
#### SwiftUI Example
|
||||
|
||||
```swift
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct ListMovieView: View {
|
||||
// QueryRef has the Observable attribute, so its properties will
|
||||
// automatically trigger updates on changes.
|
||||
private var queryRef = connector.listMoviesByGenreQuery.ref(genre: "Sci-Fi")
|
||||
|
||||
// Store the handle to unsubscribe from query updates.
|
||||
@State private var querySub: AnyCancellable?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
// Use the query results in a View.
|
||||
ForEach(queryRef.data?.movies ?? [], id: \.id) { movie in
|
||||
Text(movie.title)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Subscribe to the query for updates using the Observable macro.
|
||||
Task {
|
||||
do {
|
||||
querySub = try await queryRef.subscribe().sink { _ in }
|
||||
} catch {
|
||||
print("Error subscribing to query: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
querySub?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Data Type Mapping Reference
|
||||
|
||||
- GraphQL `UUID` -> Swift `UUID`
|
||||
- GraphQL `Date` -> Swift `FirebaseDataConnect.LocalDate`
|
||||
- GraphQL `Timestamp` -> Swift `FirebaseCore.Timestamp`
|
||||
- GraphQL `Int` -> Swift `Int`
|
||||
- GraphQL `Float` -> Swift `Double`
|
||||
- GraphQL `Boolean` -> Swift `Bool`
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
# Web SDK
|
||||
|
||||
Consult this file when writing client-side web code (TypeScript/JavaScript) that
|
||||
interacts with the SQL Connect backend.
|
||||
|
||||
### Best Practices for Agents
|
||||
|
||||
- **Understand Operation Storage**: SQL Connect queries and mutations are stored
|
||||
on the server like Cloud Functions. **Whenever you update operations, you must
|
||||
regenerate the SDK and redeploy services** that use it to avoid breaking
|
||||
clients.
|
||||
- **Resilient Enum Handling**: JavaScript/TypeScript does not enforce exhaustive
|
||||
checks on enums. Always add a `default` branch to `switch` statements or an
|
||||
`else` branch to handle unknown values gracefully when schemas evolve.
|
||||
- **TanStack Query vs. Native**: You can generate hooks for React/Angular using
|
||||
TanStack Query. Choose either TanStack or SQL Connect's built-in real-time and
|
||||
caching support, but do not use both in the same project. SQL Connect offers
|
||||
normalized caching and remote invalidation.
|
||||
- **Emulator Connection**: `connectDataConnectEmulator` is only required if
|
||||
connecting to the emulator. Otherwise, the generated SDK auto-creates the
|
||||
instance.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install firebase
|
||||
firebase init dataconnect:sdk
|
||||
```
|
||||
|
||||
### Initialization
|
||||
|
||||
```typescript
|
||||
import { connectDataConnectEmulator, getDataConnect } from 'firebase/data-connect';
|
||||
import { connectorConfig } from '@dataconnect/generated';
|
||||
|
||||
const dataConnect = getDataConnect(connectorConfig);
|
||||
// Configure the SDK to use local emulator
|
||||
connectDataConnectEmulator(dataConnect, 'localhost', 9399);
|
||||
```
|
||||
|
||||
### Calling Operations
|
||||
|
||||
#### Using `executeQuery` (Preferred for clarity)
|
||||
|
||||
```typescript
|
||||
import { executeQuery } from 'firebase/data-connect';
|
||||
import { listMoviesRef } from '@dataconnect/generated';
|
||||
|
||||
const ref = listMoviesRef();
|
||||
const { data } = await executeQuery(ref);
|
||||
console.log(data.movies);
|
||||
```
|
||||
|
||||
#### Using Action Shortcuts
|
||||
|
||||
```typescript
|
||||
import { listMovies } from '@dataconnect/generated';
|
||||
|
||||
listMovies().then(data => showInUI(data));
|
||||
```
|
||||
|
||||
### Resilient Enum Handling
|
||||
|
||||
Use a `default` case or check against `Object.values`.
|
||||
|
||||
```typescript
|
||||
import { getOldestMovie } from '@dataconnect/generated';
|
||||
|
||||
const queryResult = await getOldestMovie();
|
||||
|
||||
if (queryResult.data) {
|
||||
const oldestMovieAspectRatio = queryResult.data.originalAspectRatio;
|
||||
switch (oldestMovieAspectRatio) {
|
||||
case AspectRatio.ACADEMY:
|
||||
case AspectRatio.WIDESCREEN:
|
||||
console.log('Filmed in Academy or Widescreen!');
|
||||
break;
|
||||
default:
|
||||
// The default case will catch FULLSCREEN, etc.
|
||||
console.log('Not filmed in Academy or Widescreen.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Client-Side Caching
|
||||
|
||||
Enable caching in `connector.yaml`:
|
||||
|
||||
```yaml
|
||||
generate:
|
||||
javascriptSdk:
|
||||
outputDir: ../web/
|
||||
package: "@dataconnect/generated"
|
||||
clientCache:
|
||||
maxAge: 5s
|
||||
storage: memory # Only memory is supported on Web
|
||||
```
|
||||
|
||||
Use policies in code:
|
||||
|
||||
```typescript
|
||||
await executeQuery(queryRef, QueryFetchPolicy.CACHE_ONLY);
|
||||
await executeQuery(queryRef, QueryFetchPolicy.SERVER_ONLY);
|
||||
```
|
||||
|
||||
### Subscriptions (Realtime)
|
||||
|
||||
Use `subscribe()` to receive live updates.
|
||||
|
||||
#### Web (Vanilla JS)
|
||||
|
||||
```typescript
|
||||
import { subscribe } from 'firebase/data-connect';
|
||||
import { getMovieByIdRef } from '@dataconnect/generated';
|
||||
|
||||
const queryRef = getMovieByIdRef({ id: "<MOVIE_ID>" });
|
||||
|
||||
const unsubscribe = subscribe(queryRef, (result) => {
|
||||
console.log("Updated result:", result);
|
||||
});
|
||||
```
|
||||
|
||||
### TanStack Query Support (React)
|
||||
|
||||
To use React hooks, re-run `firebase init dataconnect:sdk` after adding React.
|
||||
|
||||
#### Usage
|
||||
|
||||
```typescript
|
||||
import { useListAllMovies } from "@dataconnect/generated/react";
|
||||
|
||||
function MyComponent() {
|
||||
const { isLoading, data, error } = useListAllMovies();
|
||||
// handle loading, error, and data
|
||||
}
|
||||
```
|
||||
|
||||
### Data Type Mapping Reference
|
||||
|
||||
- GraphQL `Timestamp` -> TypeScript `string`
|
||||
- GraphQL `Date` -> TypeScript `string`
|
||||
- GraphQL `UUID` -> TypeScript `string`
|
||||
- GraphQL `Int64` -> TypeScript `string`
|
||||
- GraphQL `Double` -> TypeScript `number`
|
||||
- GraphQL `Float` -> TypeScript `number`
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
# Search Solutions Reference (Vector & Full-Text Search)
|
||||
|
||||
Use this reference to design, configure, and implement search capabilities in
|
||||
SQL Connect. SQL Connect supports three types of search:
|
||||
|
||||
1. **Vector Similarity Search (Semantic)**: Best for finding
|
||||
conceptually/semantically similar rows (e.g., recommendations, "more like
|
||||
this"). Requires Vertex AI.
|
||||
1. **Full-Text Search (Lexical)**: Best for keyword and phrase search across
|
||||
single or multiple columns. Supports lexical stemming.
|
||||
1. **String Pattern Filters (Exact/Regex)**: Best for simple prefix, exact
|
||||
match, or basic wildcard queries (uses standard Postgres indexing).
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Search Selection Guide
|
||||
|
||||
Use this comparative guide to choose the optimal search strategy for the user's
|
||||
task:
|
||||
|
||||
| Feature / Capability | Vector Similarity Search | Full-Text Search | String Pattern Filters |
|
||||
| :------------------- | :-------------------------------------------------- | :--------------------------------------------- | :----------------------------------------------------- |
|
||||
| **Use Case** | Semantic search, recommendations, RAG pipelines. | Keyword search, parsing large text fields. | Exact matches, regular expressions, simple wildcards. |
|
||||
| **Engine Support** | Vertex AI Embeddings + `pgvector` extension. | Native PostgreSQL full-text engine. | Native PostgreSQL indexing (`LIKE`, `ILIKE`). |
|
||||
| **Matching Style** | Semantic/concept proximity. | Lexical stemming (tenses, root words). | Exact character sequence. |
|
||||
| **Column Support** | Single column per query. | Multiple columns combined. | Multiple columns via standard logical filters (`_or`). |
|
||||
| **Overhead** | High (API execution costs & vector column storage). | Medium (generates indices & tsvector columns). | Low (uses standard index / minimal storage). |
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 1. Vector Similarity Search (Semantic)
|
||||
|
||||
Perform semantic matching by generating vector embeddings representing the
|
||||
semantic meaning of text.
|
||||
|
||||
### Schema Setup
|
||||
|
||||
- **Configure Column Dimensions**: Define the column dimension size using the
|
||||
`@col(size: X)` directive — SQL Connect requires an explicit size for Vector
|
||||
fields to allocate storage.
|
||||
- **Match Model Specifications**: Ensure the column size matches the output
|
||||
dimension of your chosen embedding model (e.g., **768** for Google Vertex AI's
|
||||
`textembedding-gecko` models) to prevent runtime type mismatches.
|
||||
|
||||
```graphql
|
||||
type Movie @table {
|
||||
id: UUID! @default(expr: "uuidV4()")
|
||||
title: String!
|
||||
description: String
|
||||
# Vector field for description embeddings (Vertex AI gecko size is 768)
|
||||
descriptionEmbedding: Vector! @col(size: 768)
|
||||
}
|
||||
```
|
||||
|
||||
### Automatic Embedding Generation (`_embed` server value)
|
||||
|
||||
Ensure you use the exact same embedding model across all queries and mutations
|
||||
on a given vector field — vector embeddings generated from different model
|
||||
versions are incompatible and will result in poor search relevance or errors.
|
||||
|
||||
#### A. Generation on Insert
|
||||
|
||||
Use the `${vectorFieldName}_embed` input parameter to automatically generate and
|
||||
store embeddings on creation.
|
||||
|
||||
```graphql
|
||||
# connector/mutations.gql
|
||||
mutation CreateMovieWithEmbedding($title: String!, $description: String!) @auth(level: USER) {
|
||||
movie_insert(data: {
|
||||
title: $title,
|
||||
description: $description,
|
||||
descriptionEmbedding_embed: {
|
||||
model: "textembedding-gecko@003",
|
||||
text: $description
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### B. Generation on Update
|
||||
|
||||
```graphql
|
||||
# connector/mutations.gql
|
||||
mutation UpdateMovieDescription($id: UUID!, $description: String!) @auth(level: USER) {
|
||||
movie_update(
|
||||
id: $id,
|
||||
data: {
|
||||
description: $description,
|
||||
descriptionEmbedding_embed: {
|
||||
model: "textembedding-gecko@003",
|
||||
text: $description
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Similarity Search Queries
|
||||
|
||||
SQL Connect automatically generates a similarity query function for every
|
||||
`Vector` field in the format: `${pluralType}_${vectorFieldName}_similarity`
|
||||
|
||||
#### A. Auto-Embedding Search
|
||||
|
||||
Use `compare_embed` to automatically convert the search query string into an
|
||||
embedding on the fly using Vertex AI.
|
||||
|
||||
```graphql
|
||||
# connector/queries.gql
|
||||
query SearchMoviesByDescription($query: String!) @auth(level: PUBLIC) {
|
||||
movies_descriptionEmbedding_similarity(
|
||||
compare_embed: { model: "textembedding-gecko@003", text: $query },
|
||||
limit: 5
|
||||
) {
|
||||
id
|
||||
title
|
||||
description
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### B. Custom Vector Search
|
||||
|
||||
Use `compare` to pass raw pre-computed float arrays (cast as a `Vector!`)
|
||||
directly to the search without calling Vertex AI.
|
||||
|
||||
```graphql
|
||||
# connector/queries.gql
|
||||
query SearchMoviesByCustomVector($vector: Vector!, $limit: Int!) @auth(level: PUBLIC) {
|
||||
movies_descriptionEmbedding_similarity(
|
||||
compare: $vector,
|
||||
method: L2,
|
||||
limit: $limit
|
||||
) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tuning Vector Proximity
|
||||
|
||||
- **Distance Thresholding**: Select the `_metadata { distance }` field to
|
||||
evaluate how close the results are, then define a tight threshold using the
|
||||
`within` parameter.
|
||||
- **Distance Metric Gotcha**: `L2` and `COSINE` return different distance
|
||||
scales. Re-tune your `within` threshold if you change the `method` parameter,
|
||||
as their distance ranges are not compatible.
|
||||
|
||||
```graphql
|
||||
# connector/queries.gql
|
||||
query SearchMoviesCosineSimilarity($query: String!) @auth(level: PUBLIC) {
|
||||
movies_descriptionEmbedding_similarity(
|
||||
compare_embed: { model: "textembedding-gecko@003", text: $query },
|
||||
method: COSINE,
|
||||
within: 0.5, # Maximum distance threshold
|
||||
limit: 5
|
||||
) {
|
||||
id
|
||||
title
|
||||
_metadata { distance }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 2. Full-Text Search (Lexical)
|
||||
|
||||
Perform fast, stemmed keyword/phrase searches over single or multiple text
|
||||
columns in your table.
|
||||
|
||||
### Schema Setup
|
||||
|
||||
To index columns for full-text search, declare the `@searchable` directive on
|
||||
the string fields inside your table schema.
|
||||
|
||||
```graphql
|
||||
type Movie @table {
|
||||
id: UUID! @default(expr: "uuidV4()")
|
||||
title: String! @searchable # Default language (English)
|
||||
genre: String @searchable
|
||||
description: String @searchable(language: "french") # Custom language
|
||||
rating: Float
|
||||
}
|
||||
```
|
||||
|
||||
- **Stemming Language**: By default, parsing uses English stemming. Configure
|
||||
custom stemming using `@searchable(language: "languagename")`.
|
||||
- **Multi-Column Stemming Gotcha**: Ensure all indexed columns use the exact
|
||||
same language when searching over multiple columns in a single query —
|
||||
PostgreSQL requires matching text search configurations for multi-column
|
||||
queries.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### Full-Text Search Queries
|
||||
|
||||
SQL Connect automatically generates a full-text query function for each `@table`
|
||||
containing `@searchable` fields in the format: `${pluralType}_search`
|
||||
|
||||
```graphql
|
||||
# connector/queries.gql
|
||||
query SearchMoviesLexical($query: String!) @auth(level: PUBLIC) {
|
||||
movies_search(query: $query, limit: 10) {
|
||||
id
|
||||
title
|
||||
genre
|
||||
description
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### Tuning Full-Text Queries
|
||||
|
||||
Configuring query arguments optimizes match relevance and search styles.
|
||||
|
||||
#### 1. Query Formats (`queryFormat` argument)
|
||||
|
||||
Configure the search interpretation using the `queryFormat` parameter:
|
||||
|
||||
- **`QUERY` (Default)**: Web-style search (e.g., `inception OR matrix`,
|
||||
`-"space-travel"`, quotes for exact matches).
|
||||
- **`PLAIN`**: Matches all words in the query string in any lexical order (e.g.,
|
||||
`"brown dog"` matches `"the dog was brown"`).
|
||||
- **`PHRASE`**: Matches the exact, contiguous phrase sequence (e.g.,
|
||||
`"brown dog"` matches `"the brown dog"`, but NOT `"dog is brown"`).
|
||||
- **`ADVANCED`**: Allows standard, complex PostgreSQL `tsquery` operators (e.g.
|
||||
`inception & (matrix | sci-fi)`).
|
||||
|
||||
```graphql
|
||||
# connector/queries.gql
|
||||
query SearchMoviesExactPhrase($query: String!) @auth(level: PUBLIC) {
|
||||
movies_search(query: $query, queryFormat: PHRASE) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Relevance Thresholding (`relevanceThreshold` and `_metadata.relevance`)
|
||||
|
||||
Results default to sorting by descending relevance rank. Select
|
||||
`_metadata { relevance }` to inspect match rankings, then set a minimum
|
||||
`relevanceThreshold` value to prune loose or irrelevant matches.
|
||||
|
||||
```graphql
|
||||
# connector/queries.gql
|
||||
query SearchMoviesHighRelevance($query: String!, $threshold: Float!) @auth(level: PUBLIC) {
|
||||
movies_search(
|
||||
query: $query,
|
||||
relevanceThreshold: $threshold, # E.g., 0.05
|
||||
limit: 5
|
||||
) {
|
||||
id
|
||||
title
|
||||
_metadata {
|
||||
relevance
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
# 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_claim>` | 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.
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
# Templates
|
||||
|
||||
Ready-to-use templates for common Firebase SQL 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 SQL Connect in project
|
||||
npx -y firebase-tools@latest init dataconnect
|
||||
|
||||
# Initialize with specific project
|
||||
npx -y firebase-tools@latest use <project-id>
|
||||
npx -y firebase-tools@latest init dataconnect
|
||||
|
||||
# Start emulator for development
|
||||
npx -y firebase-tools@latest emulators:start --only dataconnect
|
||||
|
||||
# Generate SDKs
|
||||
npx -y firebase-tools@latest dataconnect:sdk:generate
|
||||
|
||||
# Deploy to production
|
||||
npx -y firebase-tools@latest 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' });
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## Realtime Query Templates
|
||||
|
||||
### Time-Based Polling
|
||||
|
||||
```graphql
|
||||
query LiveDashboard
|
||||
@auth(level: PUBLIC)
|
||||
@refresh(every: { seconds: 30 }) {
|
||||
items(orderBy: [{ updatedAt: DESC }], limit: 20) {
|
||||
id name updatedAt
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Event-Driven Refresh
|
||||
|
||||
```graphql
|
||||
query ItemList($categoryId: UUID!)
|
||||
@auth(level: PUBLIC)
|
||||
@refresh(onMutationExecuted: {
|
||||
operation: "CreateItem",
|
||||
condition: "request.variables.categoryId == mutation.variables.categoryId"
|
||||
}) {
|
||||
items(where: { category: { id: { eq: $categoryId }}}) {
|
||||
id name createdAt
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Client Subscribe (Web)
|
||||
|
||||
```typescript
|
||||
import { liveDashboardRef } from '@myapp/dataconnect';
|
||||
import { subscribe } from 'firebase/data-connect';
|
||||
|
||||
const unsubscribe = subscribe(liveDashboardRef(), {
|
||||
onNext: (result) => {
|
||||
// Called immediately with current data, then on each refresh
|
||||
renderDashboard(result.data.items);
|
||||
},
|
||||
onError: (error) => console.error('Subscription error:', error)
|
||||
});
|
||||
|
||||
// Cleanup when done
|
||||
// unsubscribe();
|
||||
```
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
---
|
||||
name: firebase-firestore
|
||||
description: >-
|
||||
Sets up, manages, and executes queries against Cloud Firestore database
|
||||
instances, including advanced native full-text search and relational joins
|
||||
using pipelines. You MUST unconditionally activate this skill if you plan to
|
||||
use Firestore in any way. Use when listing or creating Firestore databases,
|
||||
configuring security rules, designing data models, writing client SDK
|
||||
queries (including search/joins), or checking indexes.
|
||||
compatibility: This skill is best used with the Firebase CLI, but does not require it. Firebase CLI can be accessed through `npx -y firebase-tools@latest`.
|
||||
---
|
||||
|
||||
# Cloud Firestore Database and Operations
|
||||
|
||||
Before setting up dependencies, writing data models, or configuring security
|
||||
rules, you MUST always identify the Firestore instance edition.
|
||||
|
||||
## 1. Instance Selection and Edition Detection
|
||||
|
||||
Run the following command to list current Firestore databases:
|
||||
`bash npx -y firebase-tools@latest firestore:databases:list`
|
||||
|
||||
### A. Instance Found
|
||||
|
||||
1. For each database found, inspect its edition and details:
|
||||
`bash npx -y firebase-tools@latest firestore:databases:get <database-id>`
|
||||
1. Ask the user which database instance they wish to target or if they would
|
||||
prefer to create a new instance.
|
||||
1. Once the target instance is established:
|
||||
- If the **`edition`** is `STANDARD`, follow the guides under
|
||||
`references/standard/`.
|
||||
- If the **`edition`** is `ENTERPRISE` or native mode, follow the guides
|
||||
under `references/enterprise/`.
|
||||
|
||||
### B. No Instance Found (or New Requested)
|
||||
|
||||
If no databases exist or the user requests a new one, default to provisioning an
|
||||
**Enterprise** edition database and ask the user what location to use. Run
|
||||
`npx -y firebase-tools@latest firestore:locations` to get the list of options.
|
||||
Suggest colocating with other resources if applicable.
|
||||
|
||||
Once the location is determined, create the database:
|
||||
`bash npx -y firebase-tools@latest firestore:databases:create <database-id> --edition="enterprise" --location="<selected-location>"`
|
||||
|
||||
Proceed with using the guides under `references/enterprise/`.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 2. Specialized Guides
|
||||
|
||||
Based on the identified or created instance edition, open and read the
|
||||
corresponding reference guides:
|
||||
|
||||
### Standard Edition (`references/standard/`)
|
||||
|
||||
- **Provisioning**: Read [provisioning.md](references/standard/provisioning.md)
|
||||
- **Security Rules**: Read
|
||||
[security_rules.md](references/standard/security_rules.md)
|
||||
- **SDK Usage**: Read [web_sdk_usage.md](references/standard/web_sdk_usage.md),
|
||||
[android_sdk_usage.md](references/standard/android_sdk_usage.md),
|
||||
[ios_setup.md](references/standard/ios_setup.md), or
|
||||
[flutter_setup.md](references/standard/flutter_setup.md)
|
||||
- **Indexes**: Read [indexes.md](references/standard/indexes.md)
|
||||
|
||||
### Enterprise Edition / Native Mode (`references/enterprise/`)
|
||||
|
||||
- **Provisioning**: Read
|
||||
[provisioning.md](references/enterprise/provisioning.md)
|
||||
|
||||
- **Data Model**: Read [data_model.md](references/enterprise/data_model.md)
|
||||
|
||||
- **Security Rules**: Read
|
||||
[security_rules.md](references/enterprise/security_rules.md)
|
||||
|
||||
- **SDK Usage**:
|
||||
|
||||
> [!CRITICAL] **Mandatory Reference Reading** Before writing or modifying any
|
||||
> application code for Firestore Enterprise Edition, you **MUST** read at
|
||||
> least one of the relevant reference documents below for the target
|
||||
> platform/language to understand specific architectural requirements and
|
||||
> pipeline initialization patterns.
|
||||
|
||||
Read [web_sdk_usage.md](references/enterprise/web_sdk_usage.md),
|
||||
[python_sdk_usage.md](references/enterprise/python_sdk_usage.md),
|
||||
[android_sdk_usage.md](references/enterprise/android_sdk_usage.md),
|
||||
[ios_setup.md](references/enterprise/ios_setup.md), or
|
||||
[flutter_setup.md](references/enterprise/flutter_setup.md)
|
||||
|
||||
- **Indexes**: Read [indexes.md](references/enterprise/indexes.md)
|
||||
|
||||
<!-- SELF-EVOLVE:START -->
|
||||
## Self-Evolution & Proactivity (injected)
|
||||
|
||||
Zone of Initiative — calibrated autonomy:
|
||||
- Low stakes + reversible: ACT (e.g. draft a file, set a var).
|
||||
- High stakes + reversible: ACT, then report immediately.
|
||||
- High stakes + irreversible: STOP and ASK via `clarify`.
|
||||
- Reversibility is the throttle, not stakes alone.
|
||||
|
||||
Three loops keep skills alive:
|
||||
- Capture: after a task of 5+ tool calls or a reusable discovery, write/refine a skill.
|
||||
- Heal: after a failure or correction that reflects a reusable procedure, patch the skill that caused it (remove the old wording, don't layer advice).
|
||||
- Decay: on a schedule, prune duplicated/stale/no-op skills so they get sharper, not longer.
|
||||
|
||||
Memory vs Skill: a FACT (path, name, API quirk, repo URL) -> memory. a PROCEDURE (how to do X) -> skill. One-off preference -> memory/inline. Reusable correction -> skill patch.
|
||||
|
||||
For opencode (code/devops/file-ops):
|
||||
- Capture: a build/test fix pattern, a refactor recipe, or an infra quirk -> skill.
|
||||
- Heal: a bad refactor or failed deploy -> patch the responsible skill, then re-run to confirm.
|
||||
- Decay: stale scripts, dead flags, duplicated helpers. Install dep / run safe refactor = reversible; force-push / drop DB / prod deploy = ask.
|
||||
- Route research to Gemini, memory/channels to Hermes.
|
||||
|
||||
Goal: become faster, smarter, more efficient, more knowledgeable, more thoughtful, and proactively autonomous — never less safe.
|
||||
A high-stakes irreversible action (delete data, ship to prod, send a message on the user's behalf) always goes through `clarify`. Reversible, low-stakes actions may proceed, then be reported in the same turn.
|
||||
<!-- SELF-EVOLVE:END -->
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
# Android SDK Usage (Enterprise Native Mode)
|
||||
|
||||
This guide covers the Firestore Android SDK (Kotlin) setup and usage patterns
|
||||
optimized for Firestore Enterprise edition in Native mode.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 1. Initialization
|
||||
|
||||
### Add Dependencies
|
||||
|
||||
In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add
|
||||
the Firebase Kotlin Bill of Materials (BoM) and the dependency for Cloud
|
||||
Firestore:
|
||||
|
||||
> [!IMPORTANT] **[AGENT] RESOLVING THE RESOLVED SDK VERSION DETERMINISTICALLY**
|
||||
> Never guess or hardcode a memorized out-of-date SDK version. Before adding
|
||||
> dependencies, you MUST query the latest available versions directly from the
|
||||
> Google Maven Repository:
|
||||
>
|
||||
> ```bash
|
||||
> # Find the latest Firebase BoM version
|
||||
> curl -s https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-bom/maven-metadata.xml | grep -oE '<latest>[^<]+' | cut -d'>' -f2
|
||||
>
|
||||
> # Find the latest firebase-firestore version
|
||||
> curl -s https://dl.google.com/dl/android/maven2/com/google/firebase/firebase-firestore/maven-metadata.xml | grep -oE '<latest>[^<]+' | cut -d'>' -f2
|
||||
> ```
|
||||
>
|
||||
> If the dependency is already declared, fetch the exact resolved version
|
||||
> currently active in the workspace's build graph:
|
||||
>
|
||||
> ```bash
|
||||
> ./gradlew -q :app:dependencyInsight --dependency firebase-firestore --configuration releaseRuntimeClasspath
|
||||
> ```
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// [AGENT] Fetch the latest resolved version using the Gradle command above
|
||||
implementation(platform("com.google.firebase:firebase-bom:<latest_bom_version>"))
|
||||
|
||||
// Add the dependency for the Cloud Firestore library
|
||||
// When using the BoM, you don't specify versions in Firebase library dependencies
|
||||
implementation("com.google.firebase:firebase-firestore")
|
||||
}
|
||||
```
|
||||
|
||||
### Initialize Firestore
|
||||
|
||||
Initialize the `FirebaseFirestore` instance in your ComponentActivity (Jetpack
|
||||
Compose) or standard Activity/Fragment:
|
||||
|
||||
```kotlin
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// Initialize the Firestore instance
|
||||
val db = FirebaseFirestore.getInstance("<database-name>")
|
||||
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
Text("Firestore initialized!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 2. Decision Framework: Mandatory Pipeline Architecture
|
||||
|
||||
> [!CRITICAL] **MANDATORY PIPELINE ARCHITECTURE (ZERO TOLERANCE)** You are
|
||||
> strictly forbidden from using standard collection queries (e.g.,
|
||||
> `db.collection(...).where(...)` or client-side mapping loops) as the primary
|
||||
> implementation for relational joins, aggregations, or search algorithms.
|
||||
>
|
||||
> Server-side execution via pipelines is the superior, non-negotiable core
|
||||
> architecture for Enterprise instances.
|
||||
|
||||
### Strict Rules & Accountability
|
||||
|
||||
1. **Mandatory Execution Engine**: Every repository lookup requiring related
|
||||
data mapping, textual relevance scoring, or multi-document counting MUST be
|
||||
authored natively using the server-side `db.pipeline()` API.
|
||||
1. **Required Proof of Use**: In your task execution summaries, you MUST
|
||||
explicitly highlight the exact pipeline stages (`.define()`, `.addFields()`,
|
||||
`.search()`, `.aggregate()`) integrated into your primary data layer return
|
||||
paths. Failure to implement pipelines natively constitutes an immediate
|
||||
violation of instructions.
|
||||
1. **Check API References**:
|
||||
- **Locate**: Locate the standard Maven sources JAR (`-sources.jar`)
|
||||
corresponding to the resolved `firebase-firestore` dependency version
|
||||
inside your local Gradle or Maven dependency cache.
|
||||
- **Extract Once**: Extract the documentation files `pipeline.docs.txt` and
|
||||
`expressions.docs.txt` from the root directory of that `-sources.jar`
|
||||
archive into a temporary workspace scratch directory of your choice.
|
||||
- **Read & Reference**:
|
||||
- **Read** the extracted `pipeline.docs.txt` once fully to understand core
|
||||
pipeline structure and stage capabilities.
|
||||
- **Reference** the extracted `expressions.docs.txt` on-demand for specific
|
||||
function overloads and parameters.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 3. Pipeline Examples
|
||||
|
||||
### Relational Joins Pattern
|
||||
|
||||
When querying related data (e.g., articles and their author profiles), perform
|
||||
the join at the database level via pipeline stages instead of executing multiple
|
||||
sequential lookups on the client-side.
|
||||
|
||||
- Use `.define()` to bind parameters or document properties as variables.
|
||||
- Use `.addFields()` and a nested subquery with a matching filter.
|
||||
- Use `.toScalarExpression()` to convert a nested pipeline subquery to a single
|
||||
field value.
|
||||
- Assign variable and field aliases using `.alias(...)` (note: while the Web SDK
|
||||
uses `.as()`, the Kotlin SDK uses `.alias()` to avoid keyword conflicts with
|
||||
Kotlin's `as` operator).
|
||||
|
||||
```kotlin
|
||||
import com.google.firebase.firestore.pipeline.Expression.field
|
||||
import com.google.firebase.firestore.pipeline.Expression.variable
|
||||
|
||||
// Fetch articles and join the associated author Profile side-by-side
|
||||
val articlesWithAuthProfile = db.pipeline().collection("articles")
|
||||
.define(field("authorUid").alias("author_id"))
|
||||
.addFields(
|
||||
db.pipeline().collection("users")
|
||||
.where(field("__name__").documentId().equalTo(variable("author_id")))
|
||||
.select(field("displayName"), field("avatarUrl"), field("handle"))
|
||||
.toScalarExpression()
|
||||
.alias("author")
|
||||
)
|
||||
```
|
||||
|
||||
### Full-Text Search
|
||||
|
||||
Leverage the database-native `.search()` stage within your pipelines to run
|
||||
high-performance text query matches on the database level.
|
||||
|
||||
```kotlin
|
||||
import com.google.firebase.firestore.pipeline.Expression.documentMatches
|
||||
import com.google.firebase.firestore.pipeline.Expression.score
|
||||
|
||||
// Execute full-text search inside a pipeline, sorted by relevance score descending
|
||||
val searchPipeline = db.pipeline()
|
||||
.collection("articles")
|
||||
.search(
|
||||
query = documentMatches("machine learning"),
|
||||
sort = score().descending()
|
||||
)
|
||||
.limit(5)
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 4. Real-Time Listener & Document Operations
|
||||
|
||||
When real-time data sync or transaction-based document mutations are strictly
|
||||
required by application specifications, write clean operations as shown in this
|
||||
comprehensive example.
|
||||
|
||||
```kotlin
|
||||
import android.util.Log
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.firestore.DocumentChange
|
||||
import com.google.firebase.firestore.firestore
|
||||
|
||||
val db = Firebase.firestore
|
||||
// 1. Add a new document to a collection
|
||||
val taskData = hashMapOf(
|
||||
"title" to "Refactor Android SDK Usage Guide",
|
||||
"status" to "pending"
|
||||
)
|
||||
|
||||
db.collection("tasks")
|
||||
.add(taskData)
|
||||
.addOnSuccessListener { documentReference ->
|
||||
val taskId = documentReference.id
|
||||
Log.d("Firestore", "Document added with ID: $taskId")
|
||||
|
||||
// 2. Update specific fields of an existing document without replacing it
|
||||
db.collection("tasks").document(taskId)
|
||||
.update("priority", "high")
|
||||
.addOnSuccessListener {
|
||||
Log.d("Firestore", "Document successfully updated!")
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.w("Firestore", "Error updating document", e)
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.w("Firestore", "Error adding document", e)
|
||||
}
|
||||
|
||||
// 3. Establish a real-time listener on a collection query
|
||||
db.collection("tasks")
|
||||
.whereEqualTo("status", "pending")
|
||||
.addSnapshotListener { snapshot, error ->
|
||||
if (error != null) {
|
||||
Log.w("Firestore", "Listen failed.", error)
|
||||
return@addSnapshotListener
|
||||
}
|
||||
|
||||
snapshot?.documentChanges?.forEach { change ->
|
||||
val docId = change.document.id
|
||||
val docData = change.document.data
|
||||
when (change.type) {
|
||||
DocumentChange.Type.ADDED -> {
|
||||
Log.d("Firestore", "Added Task: $docId => $docData")
|
||||
}
|
||||
DocumentChange.Type.MODIFIED -> {
|
||||
Log.d("Firestore", "Updated Task: $docId => $docData")
|
||||
}
|
||||
DocumentChange.Type.REMOVED -> {
|
||||
Log.d("Firestore", "Removed Task: $docId => $docData")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# Firestore Data Model Reference
|
||||
|
||||
Firestore is a NoSQL, document-oriented database. Unlike a SQL database, there
|
||||
are no tables or rows. Instead, you store data in **documents**, which are
|
||||
organized into **collections**.
|
||||
|
||||
## Document Data Model
|
||||
|
||||
Data in Firestore is organized into documents, collections, and subcollections.
|
||||
|
||||
### Documents
|
||||
|
||||
A **document** is a lightweight record that contains fields, which map to
|
||||
values. Each document is identified by a name. A document can contain complex
|
||||
nested objects in addition to basic data types like strings, numbers, and
|
||||
booleans. Documents are limited to a maximum size of 1 MiB.
|
||||
|
||||
Example document (e.g., in a `users` collection):
|
||||
`json { "first": "Ada", "last": "Lovelace", "born": 1815 }`
|
||||
|
||||
### Collections
|
||||
|
||||
Documents live in **collections**, which are containers for your documents. For
|
||||
example, you could have a `users` collection to contain your various users, each
|
||||
represented by a document. * Collections can only contain documents. They cannot
|
||||
directly contain raw fields with values, and they cannot contain other
|
||||
collections. * Documents within a collection can contain different fields. * You
|
||||
don't need to "create" or "delete" collections explicitly. After you create the
|
||||
first document in a collection, the collection exists. If you delete all of the
|
||||
documents in a collection, the collection no longer exists.
|
||||
|
||||
### Subcollections
|
||||
|
||||
Documents can contain subcollections natively. A subcollection is a collection
|
||||
associated with a specific document. For example, a user document in the `users`
|
||||
collection could have a `messages` subcollection containing message documents
|
||||
exclusively for that user. This creates a powerful hierarchical data structure.
|
||||
|
||||
Data path example: `users/user1/messages/message1`
|
||||
|
||||
## Collection Group Support
|
||||
|
||||
A **collection group** consists of all collections with the same ID. By default,
|
||||
queries retrieve results from a single collection in your database. Use a
|
||||
collection group query to retrieve documents from a collection group instead of
|
||||
from a single collection.
|
||||
|
||||
### Use Cases
|
||||
|
||||
Collection group queries are useful when you want to query across multiple
|
||||
subcollections that share the same organizational structure.
|
||||
|
||||
For example, imagine an app with a `landmarks` collection where each landmark
|
||||
has a `reviews` subcollection. If you want to find all 5-star reviews across
|
||||
*all* landmarks, it would involve checking many separate `reviews`
|
||||
subcollections. With a collection group, you can perform a single query against
|
||||
the `reviews` collection group.
|
||||
|
||||
### Examples
|
||||
|
||||
**Standard Query** (Single Collection): Find all 5-star reviews for a specific
|
||||
landmark.
|
||||
`javascript db.collection('landmarks/golden_gate_bridge/reviews').where('rating', '==', 5)`
|
||||
|
||||
**Collection Group Query**: Find all 5-star reviews across *all* landmarks.
|
||||
`javascript db.collectionGroup('reviews').where('rating', '==', 5)`
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
# Cloud Firestore in Flutter
|
||||
|
||||
This guide covers basic CRUD operations, type-safe data modeling, and real-time
|
||||
streams when using Cloud Firestore in a Flutter application via the
|
||||
`cloud_firestore` package.
|
||||
|
||||
## 1. Setup
|
||||
|
||||
Ensure you have added the required dependency:
|
||||
|
||||
```bash
|
||||
flutter pub add cloud_firestore
|
||||
```
|
||||
|
||||
Also, ensure FlutterFire is configured properly for your target platforms.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 2. Best Practices: Type-Safe Models
|
||||
|
||||
Instead of passing raw `Map<String, dynamic>` maps throughout your UI layer,
|
||||
define a domain model class with `fromFirestore` and `toFirestore` converters to
|
||||
maintain type safety.
|
||||
|
||||
```dart
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class Item {
|
||||
final String id;
|
||||
final String name;
|
||||
final String ownerId;
|
||||
final DateTime createdAt;
|
||||
|
||||
Item({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.ownerId,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory Item.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>? ?? {};
|
||||
return Item(
|
||||
id: doc.id,
|
||||
name: data['name'] as String? ?? '',
|
||||
ownerId: data['ownerId'] as String? ?? '',
|
||||
createdAt: data['createdAt'] is Timestamp
|
||||
? (data['createdAt'] as Timestamp).toDate()
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'name': name,
|
||||
'ownerId': ownerId,
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 3. The Service Layer
|
||||
|
||||
Encapsulate all database interactions within a dedicated service class to keep
|
||||
your UI code clean and testable.
|
||||
|
||||
### Initialization & References
|
||||
|
||||
```dart
|
||||
class ItemService {
|
||||
// For Enterprise Native Mode, you often need to specify a non-default database ID:
|
||||
final FirebaseFirestore _db = FirebaseFirestore.instanceFor(
|
||||
app: Firebase.app(),
|
||||
databaseId: 'my-database-id',
|
||||
);
|
||||
|
||||
// Define your collection reference
|
||||
CollectionReference get _itemsRef => _db.collection('items');
|
||||
|
||||
// 1. Create Data
|
||||
Future<void> createItem(Item item) async {
|
||||
try {
|
||||
await _itemsRef.add(item.toFirestore());
|
||||
} catch (e) {
|
||||
print("Error creating document: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Read Data (One-Time Fetch)
|
||||
Future<List<Item>> fetchItems(String ownerId) async {
|
||||
try {
|
||||
final querySnapshot = await _itemsRef
|
||||
.where('ownerId', isEqualTo: ownerId)
|
||||
.orderBy('createdAt', descending: true)
|
||||
.get();
|
||||
|
||||
return querySnapshot.docs.map((doc) => Item.fromFirestore(doc)).toList();
|
||||
} catch (e) {
|
||||
print("Error fetching documents: $e");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Read Data (Real-Time Stream)
|
||||
Stream<List<Item>> streamItems(String ownerId) {
|
||||
return _itemsRef
|
||||
.where('ownerId', isEqualTo: ownerId)
|
||||
.snapshots()
|
||||
.map((snapshot) {
|
||||
// If a custom composite index is missing during prototyping, apply sorting client-side:
|
||||
final items = snapshot.docs.map((doc) => Item.fromFirestore(doc)).toList();
|
||||
items.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return items;
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Update Data
|
||||
Future<void> updateItemName(String id, String newName) async {
|
||||
try {
|
||||
await _itemsRef.doc(id).update({'name': newName});
|
||||
} catch (e) {
|
||||
print("Error updating document: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Delete Data
|
||||
Future<void> deleteItem(String id) async {
|
||||
try {
|
||||
await _itemsRef.doc(id).delete();
|
||||
} catch (e) {
|
||||
print("Error deleting document: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 4. Listening to Streams in the UI (`StreamBuilder`)
|
||||
|
||||
Use Flutter's `StreamBuilder` to rebuild the interface reactively whenever data
|
||||
changes in your database collection.
|
||||
|
||||
```dart
|
||||
StreamBuilder<List<Item>>(
|
||||
stream: itemService.streamItems(currentUser.uid),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return const Center(child: Text('Failed to load data'));
|
||||
}
|
||||
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final items = snapshot.data ?? [];
|
||||
|
||||
if (items.isEmpty) {
|
||||
return const Center(child: Text('No items found.'));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return ListTile(
|
||||
title: Text(item.name),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => itemService.deleteItem(item.id),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
# Firestore Indexes Reference
|
||||
|
||||
Indexes helps to improve query performance. Firestore Enterprise edition does
|
||||
not create any indexes by default. By default, Firestore Enterprise performs a
|
||||
full collection scan to find documents that match a query, which can be slow and
|
||||
expensive for large collections. To avoid this, you can create indexes to
|
||||
optimize your queries.
|
||||
|
||||
## Index Structure
|
||||
|
||||
An index consists of the following:
|
||||
|
||||
- a collection ID.
|
||||
- a list of fields in the given collection.
|
||||
- an order, either ascending or descending, for each field.
|
||||
|
||||
### Index Ordering
|
||||
|
||||
The order and sort direction of each field uniquely defines the index. For
|
||||
example, the following indexes are two distinct indexes and not interchangeable:
|
||||
|
||||
- Field name `name` (ascending) and `population` (descending)
|
||||
- Field name `name` (descending) and `population` (ascending)
|
||||
|
||||
### Index Density
|
||||
|
||||
Dense indexes: By default, Firestore indexes store data from all documents in a
|
||||
collection. An index entry will be added for a document regardless of whether
|
||||
the document contains any of the fields specified in the index. Non-existent
|
||||
fields are treated as having a NULL value when generating index entries.
|
||||
|
||||
Sparse indexes: To change this behavior, you can define the index as a sparse
|
||||
index. A sparse index indexes only the documents in the collection that contain
|
||||
a value (including null) for at least one of the indexed fields. A sparse index
|
||||
reduces storage costs and can improve performance.
|
||||
|
||||
### Unique Indexes
|
||||
|
||||
You can use unique index option to enforce unique values for the indexed fields.
|
||||
For indexes on multiple fields, each combination of values must be unique across
|
||||
the index. The database rejects any update and insert operations that attempt to
|
||||
create index entries with duplicate values.
|
||||
|
||||
## Query Support Examples
|
||||
|
||||
| Query Type | Index Required |
|
||||
| :--------------------------------------------------------- | :----------------------------------- |
|
||||
| **Simple Equality**<br>\`where("a", | Single-Field Index on field `a` |
|
||||
| : "==", 1)\` : : | |
|
||||
| **Simple Range/Sort**<br>\`where("a", | Single-Field Index on field `a` |
|
||||
| : ">", 1).orderBy("a")\` : : | |
|
||||
| **Multiple Equality**<br>\`where("a", | Single-Field Index on field `a` and |
|
||||
| : "==", 1).where("b", "==", 2)` :`b\` : | |
|
||||
| \*\*Equality + | **Composite Index** on field `a` and |
|
||||
| : Range/Sort\*\*<br>`where("a", "==", : `b\` : | |
|
||||
| : 1).where("b", ">", 2)\` : : | |
|
||||
| **Multiple Ranges**<br>\`where("a", | **Composite Index** on field `a` and |
|
||||
| : ">", 1).where("b", ">", 2)` :`b\` : | |
|
||||
| \*\*Array Contains + | **Composite Index** on field `tags` |
|
||||
| : Equality\*\*<br>`where("tags", : and `active\` : | |
|
||||
| : "array-contains", : : | |
|
||||
| : "news").where("active", "==", true)\` : : | |
|
||||
|
||||
If no indexes is present, Firestore Enterprise will perform a full collection
|
||||
scan to find documents that match a query.
|
||||
|
||||
## Management
|
||||
|
||||
### Config files
|
||||
|
||||
Your indexes should be defined in `firestore.indexes.json` (pointed to by
|
||||
`firebase.json`).
|
||||
|
||||
Define a dense index:
|
||||
|
||||
```json
|
||||
{
|
||||
"indexes": [
|
||||
{
|
||||
"collectionGroup": "cities",
|
||||
"queryScope": "COLLECTION",
|
||||
"density": "DENSE",
|
||||
"fields": [
|
||||
{ "fieldPath": "country", "order": "ASCENDING" },
|
||||
{ "fieldPath": "population", "order": "DESCENDING" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"fieldOverrides": []
|
||||
}
|
||||
```
|
||||
|
||||
Define a sparse-any index:
|
||||
|
||||
```json
|
||||
{
|
||||
"indexes": [
|
||||
{
|
||||
"collectionGroup": "cities",
|
||||
"queryScope": "COLLECTION",
|
||||
"density": "SPARSE_ANY",
|
||||
"fields": [
|
||||
{ "fieldPath": "country", "order": "ASCENDING" },
|
||||
{ "fieldPath": "population", "order": "DESCENDING" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"fieldOverrides": []
|
||||
}
|
||||
```
|
||||
|
||||
Define a unique index:
|
||||
|
||||
```json
|
||||
{
|
||||
"indexes": [
|
||||
{
|
||||
"collectionGroup": "cities",
|
||||
"queryScope": "COLLECTION",
|
||||
"density": "SPARSE_ANY",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
{ "fieldPath": "country", "order": "ASCENDING" },
|
||||
{ "fieldPath": "population", "order": "DESCENDING" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"fieldOverrides": []
|
||||
}
|
||||
```
|
||||
|
||||
### CLI Commands
|
||||
|
||||
Deploy indexes only:
|
||||
`bash npx firebase-tools@latest -y deploy --only firestore:indexes`
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
# Firestore Enterprise Native Mode on iOS
|
||||
|
||||
This guide walks you through using the Cloud Firestore SDK in your iOS app using
|
||||
Swift.
|
||||
|
||||
# ⛔️ CRITICAL RULE: NO FirebaseFirestoreSwift ⛔️
|
||||
|
||||
UNDER NO CIRCUMSTANCES should you import, link against, or configure a project
|
||||
to use `FirebaseFirestoreSwift`.
|
||||
|
||||
As of Firebase SDK v11+, all Swift-specific features (including `@DocumentID`,
|
||||
`@ServerTimestamp`, and `Codable` support) have been fully merged into the main
|
||||
`FirebaseFirestore` module.
|
||||
|
||||
- NEVER add `.external(name: "FirebaseFirestoreSwift")` or similar to SPM or
|
||||
Xcode configurations.
|
||||
- NEVER write `import FirebaseFirestoreSwift` in any Swift file.
|
||||
- ONLY use `import FirebaseFirestore`.
|
||||
|
||||
This is a zero-tolerance constraint. Using `FirebaseFirestoreSwift` is
|
||||
fundamentally incorrect and unacceptable.
|
||||
|
||||
# ⛔️ CRITICAL RULE: NO INLINE INITIALIZATION ⛔️
|
||||
|
||||
NEVER write `let db = Firestore.firestore()` or `Firestore.firestore(database:)`
|
||||
as an inline class or struct property if there is ANY chance the object is
|
||||
instantiated before `FirebaseApp.configure()` executes in the app root.
|
||||
|
||||
- **FATAL CRASH:**
|
||||
`@Observable class DataManager { let db = Firestore.firestore() }` initialized
|
||||
as a `@State` in the App root.
|
||||
- **SAFE PATTERN:** Initialize `Firestore.firestore()` lazily
|
||||
(`lazy var db = Firestore.firestore()`) OR explicitly initialize the manager
|
||||
*after* `FirebaseApp.configure()` finishes.
|
||||
|
||||
## 1. Import and Initialize
|
||||
|
||||
Ensure you have installed the `FirebaseFirestore` SDK. Use the
|
||||
`xcode-project-setup` skill to automate adding the SPM dependency to the Xcode
|
||||
project.
|
||||
|
||||
```swift
|
||||
import FirebaseFirestore
|
||||
```
|
||||
|
||||
Initialize an instance of Cloud Firestore. **CRITICAL**: Enterprise databases
|
||||
require a custom database ID and cannot use the `(default)` instance.
|
||||
|
||||
```swift
|
||||
// Replace "your-enterprise-database-id" with your actual database ID
|
||||
let db = Firestore.firestore(database: "your-enterprise-database-id")
|
||||
```
|
||||
|
||||
## 2. Type-Safe Data Models (Codable)
|
||||
|
||||
To leverage modern Swift data modeling, define your data as `Codable` structs.
|
||||
The main `FirebaseFirestore` module automatically supports mapping these types.
|
||||
|
||||
```swift
|
||||
struct User: Codable {
|
||||
@DocumentID var id: String?
|
||||
var firstName: String
|
||||
var lastName: String
|
||||
var born: Int
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Basic CRUD Operations
|
||||
|
||||
The operations are identical to standard Firestore, but ensure you use the `db`
|
||||
instance initialized with your Enterprise database ID.
|
||||
|
||||
### Writing Data (Modern Concurrency & Codable)
|
||||
|
||||
```swift
|
||||
let user = User(firstName: "Ada", lastName: "Lovelace", born: 1815)
|
||||
|
||||
do {
|
||||
// Add a new document with a generated ID using Codable
|
||||
let ref = try db.collection("users").addDocument(from: user)
|
||||
print("Document added with ID: \(ref.documentID)")
|
||||
} catch {
|
||||
print("Error adding document: \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
### Reading Data (Modern Concurrency & Codable)
|
||||
|
||||
```swift
|
||||
do {
|
||||
let querySnapshot = try await db.collection("users").getDocuments()
|
||||
|
||||
// Map documents to the User struct automatically
|
||||
let users = querySnapshot.documents.compactMap { document in
|
||||
try? document.data(as: User.self)
|
||||
}
|
||||
|
||||
for user in users {
|
||||
print("Found user: \(user.firstName) \(user.lastName)")
|
||||
}
|
||||
} catch {
|
||||
print("Error getting documents: \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Pipeline Queries
|
||||
|
||||
Firestore Enterprise supports Pipeline operations for complex queries.
|
||||
|
||||
### Initialization
|
||||
|
||||
```swift
|
||||
let pipeline = db.pipeline()
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```swift
|
||||
// Return all documents across all collections in the database
|
||||
let results = try await db.pipeline().database().execute()
|
||||
|
||||
// Filtered query
|
||||
let results = try await db.pipeline()
|
||||
.collection("cities")
|
||||
.where(Field("name").equal(Constant("Toronto")))
|
||||
.execute()
|
||||
|
||||
// Compound query
|
||||
let results = try await db.pipeline()
|
||||
.collection("books")
|
||||
.where(Field("rating").equal(5) && Field("published").lessThan(1900))
|
||||
.execute()
|
||||
```
|
||||
|
||||
## 5. Realtime Listeners in SwiftUI (Lifecycle Best Practices)
|
||||
|
||||
When implementing Firestore realtime listeners (`addSnapshotListener`) within a
|
||||
SwiftUI application, you **MUST** tie the listener lifecycle to the view's
|
||||
identity using `.task(id:)`, NOT `.onDisappear`.
|
||||
|
||||
### ⛔️ UNSAFE PATTERN (.onDisappear)
|
||||
|
||||
Presenting a `.sheet` or `.fullScreenCover` can trigger the underlying view's
|
||||
`onDisappear` method. If you stop your listener here, the feed will stop
|
||||
updating while the sheet is open, and won't resume when it's dismissed.
|
||||
|
||||
### ✅ SAFE PATTERN (.task with deinit)
|
||||
|
||||
Because `addSnapshotListener` is a synchronous call, placing it inside a `.task`
|
||||
means the task completes immediately. This breaks SwiftUI's automatic
|
||||
cancellation mechanism.
|
||||
|
||||
To safely manage traditional Firebase listeners in SwiftUI, you must use
|
||||
**`deinit`** to handle memory cleanup when the view is destroyed, and
|
||||
**`.task(id:)`** to handle data identity changes while the view is active.
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
import FirebaseFirestore
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class DataManager {
|
||||
private var listenerHandle: ListenerRegistration?
|
||||
var data: [String] = []
|
||||
|
||||
func startListening(for userId: String) {
|
||||
// 1. Clean up any existing listener to prevent duplicates if the ID changes
|
||||
stopListening()
|
||||
|
||||
// 2. Start the regular listener and capture the handle
|
||||
// Note: Using the global default instance here, make sure to use your enterprise instance if applicable
|
||||
// For enterprise, you might need to pass the db instance or use a shared manager.
|
||||
listenerHandle = Firestore.firestore(database: "your-enterprise-database-id").collection("users").document(userId).addSnapshotListener { snapshot, error in
|
||||
// Handle updates
|
||||
}
|
||||
}
|
||||
|
||||
func stopListening() {
|
||||
listenerHandle?.remove()
|
||||
listenerHandle = nil
|
||||
}
|
||||
|
||||
// 3. Guarantee cleanup when the View is destroyed and this object is deallocated
|
||||
isolated deinit {
|
||||
stopListening()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
# Provisioning Firestore Enterprise Native Mode
|
||||
|
||||
## Manual Initialization
|
||||
|
||||
Initialize the following firebase configuration files manually. Do not use
|
||||
`npx -y firebase-tools@latest init`, as it expects interactive inputs.
|
||||
|
||||
1. **Create a Firestore Enterprise Database**: Create a Firestore Enterprise
|
||||
database using the Firebase CLI.
|
||||
1. **Create `firebase.json`**: This file contains database configuration for the
|
||||
Firebase CLI.
|
||||
1. **Create `firestore.rules`**: This file contains your security rules.
|
||||
1. **Create `firestore.indexes.json`**: This file contains your index
|
||||
definitions.
|
||||
|
||||
### 1. Create a Firestore Enterprise Database
|
||||
|
||||
If the user needs to create a new database, ask the user what location to use.
|
||||
Run `npx -y firebase-tools@latest firestore:locations` to get the list of
|
||||
options. Suggest colocating with other resources if applicable.
|
||||
|
||||
Use the following command to create a Firestore Enterprise database:
|
||||
|
||||
```bash
|
||||
firebase firestore:databases:create my-database-id \
|
||||
--location="<selected-location>" \
|
||||
--edition="enterprise" \
|
||||
--firestore-data-access="ENABLED" \
|
||||
--mongodb-compatible-data-access="DISABLED"
|
||||
```
|
||||
|
||||
This will create an enterprise database in the selected location with native
|
||||
mode enabled. A database id is required to create an enterprise database and the
|
||||
database id must not be `(default)`. To enable realtime-updates feature, use
|
||||
`--realtime-updates` flag.
|
||||
|
||||
```bash
|
||||
firebase firestore:databases:create my-database-id \
|
||||
--location="<selected-location>" \
|
||||
--edition="enterprise" \
|
||||
--firestore-data-access="ENABLED" \
|
||||
--mongodb-compatible-data-access="DISABLED" \
|
||||
--realtime-updates="ENABLED"
|
||||
```
|
||||
|
||||
### 2. Create `firebase.json`
|
||||
|
||||
Create a file named `firebase.json` in your project root with the following
|
||||
content (edit `database` and `location` to match the ones you created above). If
|
||||
this file already exists, instead append to the existing JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"firestore": {
|
||||
"rules": "firestore.rules",
|
||||
"indexes": "firestore.indexes.json",
|
||||
"edition": "enterprise",
|
||||
"database": "my-database-id",
|
||||
"location": "<selected-location>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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).
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
# Python SDK Usage
|
||||
|
||||
The Python Server SDK is used for backend/server environments and utilizes
|
||||
Google Application Default Credentials in most Google Cloud environments.
|
||||
|
||||
### Writing Data
|
||||
|
||||
#### Set a Document
|
||||
|
||||
Creates a document if it does not exist or overwrites it if it does. You can
|
||||
also specify a merge option to only update provided fields.
|
||||
|
||||
```python
|
||||
city_ref = db.collection("cities").document("LA")
|
||||
|
||||
# Create/Overwrite
|
||||
city_ref.set({
|
||||
"name": "Los Angeles",
|
||||
"state": "CA",
|
||||
"country": "USA"
|
||||
})
|
||||
|
||||
# Merge
|
||||
city_ref.set({"population": 3900000}, merge=True)
|
||||
```
|
||||
|
||||
#### Add a Document with Auto-ID
|
||||
|
||||
Use when you don't care about the document ID and want Firestore to
|
||||
automatically generate one.
|
||||
|
||||
```python
|
||||
update_time, city_ref = db.collection("cities").add({
|
||||
"name": "Tokyo",
|
||||
"country": "Japan"
|
||||
})
|
||||
print("Document written with ID: ", city_ref.id)
|
||||
```
|
||||
|
||||
#### Update a Document
|
||||
|
||||
Update some fields of an existing document without overwriting the entire
|
||||
document. Fails if the document doesn't exist.
|
||||
|
||||
```python
|
||||
city_ref = db.collection("cities").document("LA")
|
||||
city_ref.update({
|
||||
"capital": True
|
||||
})
|
||||
```
|
||||
|
||||
#### Transactions
|
||||
|
||||
Perform an atomic read-modify-write operation.
|
||||
|
||||
```python
|
||||
from google.cloud.firestore import Transaction
|
||||
|
||||
transaction = db.transaction()
|
||||
city_ref = db.collection("cities").document("SF")
|
||||
|
||||
@firestore.transactional
|
||||
def update_in_transaction(transaction, city_ref):
|
||||
snapshot = city_ref.get(transaction=transaction)
|
||||
if not snapshot.exists:
|
||||
raise Exception("Document does not exist!")
|
||||
|
||||
new_population = snapshot.get("population") + 1
|
||||
transaction.update(city_ref, {"population": new_population})
|
||||
|
||||
update_in_transaction(transaction, city_ref)
|
||||
```
|
||||
|
||||
### Reading Data
|
||||
|
||||
#### Get a Single Document
|
||||
|
||||
```python
|
||||
doc_ref = db.collection("cities").document("SF")
|
||||
doc = doc_ref.get()
|
||||
|
||||
if doc.exists:
|
||||
print(f"Document data: {doc.to_dict()}")
|
||||
else:
|
||||
print("No such document!")
|
||||
```
|
||||
|
||||
#### Get Multiple Documents
|
||||
|
||||
Fetches all documents in a query or collection once.
|
||||
|
||||
```python
|
||||
docs = db.collection("cities").stream()
|
||||
|
||||
for doc in docs:
|
||||
print(f"{doc.id} => {doc.to_dict()}")
|
||||
```
|
||||
|
||||
### Queries
|
||||
|
||||
#### Simple and Compound Queries
|
||||
|
||||
Use `.where()` to combine filters safely. Stack `.where()` calls for compound
|
||||
queries.
|
||||
|
||||
```python
|
||||
from google.cloud.firestore import FieldFilter
|
||||
|
||||
cities_ref = db.collection("cities")
|
||||
|
||||
# Simple equality
|
||||
query_1 = cities_ref.where(filter=FieldFilter("state", "==", "CA"))
|
||||
|
||||
# Compound (AND)
|
||||
query_2 = cities_ref.where(
|
||||
filter=FieldFilter("state", "==", "CA")
|
||||
).where(
|
||||
filter=FieldFilter("population", ">", 1000000)
|
||||
)
|
||||
```
|
||||
|
||||
#### Order and Limit
|
||||
|
||||
Sort and limit results cleanly.
|
||||
|
||||
```python
|
||||
query = cities_ref.order_by("name").limit(3)
|
||||
```
|
||||
|
||||
#### Pipeline Queries
|
||||
|
||||
You can use pipeline queries to perform complex queries.
|
||||
|
||||
```python
|
||||
pipeline = client.pipeline().collection("users")
|
||||
for result in pipeline.execute():
|
||||
print(f"{result.id} => {result.data()}")
|
||||
```
|
||||
|
|
@ -0,0 +1,577 @@
|
|||
## 1. Generate Firestore Rules
|
||||
|
||||
You are an expert Firebase Security Rules engineer with deep knowledge of
|
||||
Firestore security best practices. Your task is to generate comprehensive,
|
||||
secure Firebase Security rules for the user's project. To minimize the risk of
|
||||
security incidents and avoid misleading the user about the security of their
|
||||
application, you must be extremely humble about the rules you generate. Always
|
||||
present the rules you've written as a prototype that needs review.
|
||||
|
||||
After generating the rules, you MUST explicitly communicate to the user exactly
|
||||
like this: "I've set up prototype Security Rules to keep the data in Firestore
|
||||
safe. They are designed to be secure for <explain reasons here>. However, you
|
||||
should review and verify them before broadly sharing your app. If you'd like, I
|
||||
can help you harden these rules."
|
||||
|
||||
### Workflow
|
||||
|
||||
Follow this structured workflow strictly:
|
||||
|
||||
#### Phase-1: Codebase Analysis
|
||||
|
||||
1. **Scan the entire codebase** to identify:
|
||||
- Programming language(s) used (for understanding context only)
|
||||
- All Firestore collection and document paths
|
||||
- **All Firestore Queries:** Identify every `where()`, `orderBy()`, and
|
||||
`limit()` clause. The security rules **MUST** allow these specific queries.
|
||||
- Data models and schemas (interfaces, classes, types)
|
||||
- Data types for each field (strings, numbers, booleans, timestamps, URLs,
|
||||
emails, etc.)
|
||||
- Required vs. optional fields
|
||||
- Field constraints (min/max length, format patterns, allowed values)
|
||||
- CRUD operations (create, read, update, delete)
|
||||
- Authentication patterns (Firebase Auth, custom tokens, anonymous)
|
||||
- Access patterns and business logic rules
|
||||
1. **Document your findings** in a untracked file. Refer to this file when
|
||||
generating the security rules.
|
||||
|
||||
#### Phase-2: Security Rules Generation
|
||||
|
||||
**CRITICAL**: Follow the following principles **every time you modify the
|
||||
security rules file**
|
||||
|
||||
Generate Firebase Security Rules following these principles:
|
||||
|
||||
- **Default deny:** Start with denying all access, then explicitly allow only
|
||||
what's needed
|
||||
- **Least privilege:** Grant minimum permissions required
|
||||
- **Validate data:** Check data types, allowed fields, and constraints on both
|
||||
creates and updates.
|
||||
- **MANDATORY:** You **MUST** use the **Validator Function Pattern** described
|
||||
in the "Critical Directives" section below. This involves defining a
|
||||
specific validation function (e.g., `isValidUser`) and calling it in
|
||||
**BOTH** `create` and `update` rules.
|
||||
- **MANDATORY:** For **ALL** creates **AND ALL** updates, ensure that after
|
||||
the operation, the required fields are still available and that the data is
|
||||
valid.
|
||||
- **Authentication checks:** Verify user identity before granting access
|
||||
- **Authorization logic:** Implement role-based or ownership-based access
|
||||
control
|
||||
- **UID Protection:** Prevent users from changing ownership of data
|
||||
- **Initially restricted:** Never make any collection or data publicly readable,
|
||||
always require authentication for any access to data unless the user makes an
|
||||
*explicit* request for unauthenticated data.
|
||||
|
||||
This means the first firestore.rules file you generate must never have any
|
||||
"allow read: true" statements.
|
||||
|
||||
**Structure Requirements:**
|
||||
|
||||
1. **Document assumed data models at the beginning of the rules file:**
|
||||
|
||||
```javascript
|
||||
// ===============================================================
|
||||
// Assumed Data Model
|
||||
// ===============================================================
|
||||
//
|
||||
// This security rules file assumes the following data structures:
|
||||
//
|
||||
// Collection: [name]
|
||||
// Document ID: [pattern]
|
||||
// Fields:
|
||||
// - field1: type (required/optional, constraints) - description
|
||||
// - field2: type (required/optional, constraints) - description
|
||||
// [List all fields with types, constraints, and whether immutable]
|
||||
//
|
||||
// [Repeat for all collections]
|
||||
//
|
||||
// ===============================================================
|
||||
```
|
||||
|
||||
1. **Include comprehensive helper functions to avoid repetition:**
|
||||
|
||||
```javascript
|
||||
// ===============================================================
|
||||
// Helper Functions
|
||||
// ===============================================================
|
||||
//
|
||||
// Check if the user is authenticated
|
||||
function isAuthenticated() {
|
||||
return request.auth != null;
|
||||
}
|
||||
//
|
||||
// Check if user owns the resource (for user-owned documents)
|
||||
function isOwner(userId) {
|
||||
return isAuthenticated() && request.auth.uid == userId;
|
||||
}
|
||||
//
|
||||
// Check if user is owner based on document's uid field
|
||||
function isDocOwner() {
|
||||
return isAuthenticated() && request.auth.uid == resource.data.uid;
|
||||
}
|
||||
//
|
||||
// Verify UID hasn't been tampered with on create
|
||||
function uidUnchanged() {
|
||||
return !('uid' in request.resource.data) ||
|
||||
request.resource.data.uid == request.auth.uid;
|
||||
}
|
||||
//
|
||||
// Ensure uid field is not modified on update
|
||||
function uidNotModified() {
|
||||
return !('uid' in request.resource.data) ||
|
||||
request.resource.data.uid == resource.data.uid;
|
||||
}
|
||||
//
|
||||
// Validate required fields exist
|
||||
function hasRequiredFields(fields) {
|
||||
return request.resource.data.keys().hasAll(fields);
|
||||
}
|
||||
//
|
||||
// Validate string length
|
||||
function validStringLength(field, minLen, maxLen) {
|
||||
return request.resource.data[field] is string &&
|
||||
request.resource.data[field].size() >= minLen &&
|
||||
request.resource.data[field].size() <= maxLen;
|
||||
}
|
||||
//
|
||||
// Validate URL format (must start with https:// or http://)
|
||||
function isValidUrl(url) {
|
||||
return url is string &&
|
||||
(url.matches("^https://.*") || url.matches("^http://.*"));
|
||||
}
|
||||
//
|
||||
// Validate email format
|
||||
function isValidEmail(email) {
|
||||
return email is string &&
|
||||
email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
|
||||
}
|
||||
|
||||
//
|
||||
// Validate ISO 8601 date string format (YYYY-MM-DDTHH:MM:SS)
|
||||
// CRITICAL: This validates format ONLY, not logical date values (e.g., month 13).
|
||||
// Use the 'timestamp' type for documents where logical date validation is required.
|
||||
function isValidDateString(dateStr) {
|
||||
return dateStr is string &&
|
||||
dateStr.matches("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*Z?$");
|
||||
}
|
||||
|
||||
//
|
||||
// Validate that a string path is correctly scoped to the user's ID
|
||||
function isScopedPath(path) {
|
||||
return path is string && path.matches("^users/" + request.auth.uid + "/.*");
|
||||
}
|
||||
//
|
||||
// Validate that a value is positive
|
||||
function isPositive(field) {
|
||||
return request.resource.data[field] is number && request.resource.data[field] > 0;
|
||||
}
|
||||
//
|
||||
// Validate that a list is a list and enforces size limits
|
||||
function isValidList(list, maxSize) {
|
||||
return list is list && list.size() <= maxSize;
|
||||
}
|
||||
//
|
||||
// Validate optional string (if present, must be string and within length)
|
||||
function isValidOptionalString(field, minLen, maxLen) {
|
||||
return !('field' in request.resource.data) ||
|
||||
(request.resource.data[field] is string &&
|
||||
request.resource.data[field].size() >= minLen &&
|
||||
request.resource.data[field].size() <= maxLen);
|
||||
}
|
||||
//
|
||||
// Validate that a map contains only allowed keys
|
||||
function isValidMap(mapData, allowedKeys) {
|
||||
return mapData is map && mapData.keys().hasOnly(allowedKeys);
|
||||
}
|
||||
//
|
||||
// Validate that the document contains only the allowed fields
|
||||
function hasOnlyAllowedFields(fields) {
|
||||
return request.resource.data.keys().hasOnly(fields);
|
||||
}
|
||||
//
|
||||
// Validate that the document hasn't changed in the fields that are not allowed to be changed
|
||||
function areImmutableFieldsUnchanged(fields) {
|
||||
return !request.resource.data.diff(resource.data).affectedKeys().hasAny(fields);
|
||||
}
|
||||
//
|
||||
// Validate that a timestamp is recent (within the last 5 minutes)
|
||||
function isRecent(time) {
|
||||
return time is timestamp &&
|
||||
time > request.time - duration.value(5, 'm') &&
|
||||
time <= request.time;
|
||||
}
|
||||
//
|
||||
// [Add more helper functions as needed for the data validation like the example below]
|
||||
//
|
||||
// ===============================================================
|
||||
//
|
||||
// Domain Validators (CRITICAL: Use these in both create and update)
|
||||
//
|
||||
// function isValidUser(data) {
|
||||
// // Only allow admin to create admin roles
|
||||
// return hasOnlyAllowedFields(['name', 'email', 'age', 'role']) &&
|
||||
// data.name is string && data.name.size() > 0 && data.name.size() < 50 &&
|
||||
// data.email is string && isValidEmail(data.email) &&
|
||||
// data.age is number && data.age >= 18 &&
|
||||
// data.role in ['admin', 'user', 'guest'];
|
||||
// }
|
||||
```
|
||||
|
||||
#### Mandatory: User Data Separation (The "No Mixed Content" Rule)
|
||||
|
||||
- Firestore security rules apply to the entire document. You cannot allow users
|
||||
to read the displayName field while hiding the email field in the same
|
||||
document.
|
||||
- If a collection (e.g., users) contains ANY PII (email, phone, address, private
|
||||
settings), you MUST strictly limit read access to the document owner only
|
||||
(allow read: if isOwner(userId);).
|
||||
- If the application requires public profiles (e.g., showing user names/avatars
|
||||
on posts):
|
||||
- 1. Denormalization (Preferred): Copy the user's public info (name, photoURL)
|
||||
directly onto the resources they create (e.g., store authorName and
|
||||
authorPhoto inside the posts document).
|
||||
- 2. Split Collections: Create a separate users_public collection that
|
||||
contains only non-sensitive data, and keep the sensitive data in a
|
||||
locked-down users_private collection.
|
||||
- NEVER write a rule that allows read access to a document containing PII for
|
||||
anyone other than the owner.
|
||||
|
||||
#### **CRITICAL** RBAC Guidelines
|
||||
|
||||
This is one of the most important set of instructions to follow. Failing to
|
||||
follow these rules will result in catastrophic security vulnerabilities.
|
||||
|
||||
- **NEVER** allow users to create their own privileged roles. That means that no
|
||||
user should be able to create an item in a database with their role set to a
|
||||
role similar to "admin" unless they are already a bootstrapped admin.
|
||||
- **NEVER** allow users to update their own roles or permissions.
|
||||
- **NEVER** allow users to grant themselves access to other users' data.
|
||||
- **NEVER** allow users to bypass the role hierarchy.
|
||||
- **ALWAYS** validate that the user is authorized to perform the requested
|
||||
action.
|
||||
- **ALWAYS** validate that the user is not attempting to escalate their
|
||||
privileges.
|
||||
- **ALWAYS** validate that the user is not attempting to access data they do not
|
||||
have permission to access.
|
||||
|
||||
Here's a **bad** example of what **NOT** to do:
|
||||
|
||||
```javascript
|
||||
match /users/{userId} {
|
||||
// BAD: Allows users to create their own roles because a user can create a new user document with a role of 'admin' and the isAdmin() function will return true
|
||||
allow create: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin();
|
||||
// BAD: Allows users to update their own roles because a user can update their own user document with a role of 'admin' and the isAdmin() function will return true
|
||||
allow update: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin();
|
||||
}
|
||||
```
|
||||
|
||||
Here's a **good** example of what **TO** do:
|
||||
|
||||
```javascript
|
||||
match /users/{userId} {
|
||||
// GOOD: Does NOT allow users to create their own roles unless they are an admin or the user is updating their own role to a less privileged role
|
||||
allow create: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == 'client') || isAdmin());
|
||||
// GOOD: Does NOT allow users to update their own roles unless they are an admin
|
||||
allow update: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == resource.data.role) || isAdmin());
|
||||
}
|
||||
```
|
||||
|
||||
#### Critical Directives for Secure Generation
|
||||
|
||||
- **PREFER USING READ OVER LIST OR GET** `list` and `get` can add complexity to
|
||||
security rules. Prefer using `read` over them.
|
||||
|
||||
- **Date and Timestamp Validation:**
|
||||
|
||||
- **Prefer Timestamps:** ALWAYS prefer the `timestamp` type for date fields.
|
||||
Firestore automatically ensures they are logically valid dates.
|
||||
- **String Date Risks:** If using strings for dates (e.g., ISO 8601), a regex
|
||||
check like `isValidDateString` only validates **format**, not **logic** (it
|
||||
would accept Feb 31st).
|
||||
- **Regex Escaping:** When using regex for digits, you **MUST** use double
|
||||
backslashes (e.g., `\\\\d`) in the rules string. Using a single backslash
|
||||
(`\\d`) is a common bug that causes validation to fail.
|
||||
|
||||
- **Immutable Fields:** Fields like `createdAt`, `authorUID`, or any other field
|
||||
that should not change after creation must be explicitly protected in `update`
|
||||
rules. (e.g., `request.resource.data.createdAt == resource.data.createdAt`).
|
||||
**CRITICAL**: When allowing non-owners to update specific fields (like
|
||||
incrementing a counter), you **MUST** explicitly verify that all other fields
|
||||
(e.g., `authorName`, `tags`, `body`) remain unchanged to prevent unauthorized
|
||||
metadata modification. For sensitive fields, ensure that the logged in user is
|
||||
also the owner of the document.
|
||||
|
||||
- **Identity Integrity:** When storing denormalized user identity (e.g.
|
||||
`authorName`, `authorPhoto`), you **MUST** validate this data.
|
||||
|
||||
- **Prefer Auth Token:** If possible, check if
|
||||
`request.resource.data.authorName == request.auth.token.name`.
|
||||
- **Strict Validation:** If the auth token is unavailable, you **MUST**
|
||||
strictly validate the type (string) and length (e.g. < 50 chars) to prevent
|
||||
spoofing with massive or malicious payloads.
|
||||
- **Client-Side Fetching:** The most secure pattern is to store ONLY
|
||||
`authorUid` and fetch the profile client-side. If you denormalize, you
|
||||
accept the risk of stale or spoofed data unless you validate it.
|
||||
|
||||
- **Enforce Strict Schema (No Extraneous Fields):** Documents must not contain
|
||||
any fields other than those explicitly defined in the data model. This
|
||||
prevents users from adding arbitrary data.
|
||||
|
||||
- **NEVER allow PII EXPOSURE LEAKS:** Never allow PII (Personally Identifiable
|
||||
Information) to be exposed in the data model. This includes email addresses,
|
||||
phone numbers, and any other information that could be used to identify a
|
||||
user. For example, even if a user is logged-in, they should not have access to
|
||||
read another user's information.
|
||||
|
||||
- **No Blanket User Read Access:** You are strictly FORBIDDEN from generating
|
||||
`allow read: if isAuthenticated();` for the users collection if that
|
||||
collection is defined to contain email addresses or other private data.
|
||||
|
||||
- **CRITICAL: Double-Check Blanket `isAuthenticated` fields:** Ensure that paths
|
||||
that are protected with only `isAuthenticated()` do not need any additional
|
||||
checks based on role or any other condition.
|
||||
|
||||
- **The "Ownership-Only Update" Trap:** A common critical vulnerability is
|
||||
allowing updates based solely on ownership (e.g.,
|
||||
`allow update: if isOwner(resource.data.uid);`). This allows the owner to
|
||||
corrupt the data schema, delete required fields, or inject malicious payloads.
|
||||
You **MUST** always combine ownership checks with data validation (e.g.,
|
||||
`allow update: if isOwner(...) && isValidEntity(...);`) **AND** validate that
|
||||
self-escalation is not possible.
|
||||
|
||||
- **Deep Array Inspection:** It is insufficient to check if a field `is list`.
|
||||
You **MUST** validate the contents of the array (e.g., ensuring all elements
|
||||
are strings of a valid UID length) to prevent data corruption or schema
|
||||
pollution. For example, a `tags` array must verify that every item is a string
|
||||
AND that each string is within a reasonable length (e.g., < 20 chars).
|
||||
|
||||
- **Permission-Field Lockdown:** Fields that control access (e.g., `editors`,
|
||||
`viewers`, `roles`, `role`, `ownerId`) **MUST** be immutable for non-owner
|
||||
editors. In `update` rules, use `fieldUnchanged()` for these fields unless the
|
||||
`request.auth.uid` matches the document's original owner/creator. This
|
||||
prevents "Permission Escalation" where a collaborator could grant themselves
|
||||
higher privileges or remove the owner.
|
||||
|
||||
### Advanced Validation for Business Logic
|
||||
|
||||
Secure rules must enforce the application's business logic. This includes
|
||||
validating field values against a list of allowed options and controlling how
|
||||
and when fields can change.
|
||||
|
||||
\#### 1. Enforce Enum Values
|
||||
|
||||
If a field should only contain specific values (e.g., a status), validate
|
||||
against a list.
|
||||
|
||||
**Example:**
|
||||
|
||||
```javascript
|
||||
// A 'task' document's status can only be one of three values
|
||||
function isValidStatus() {
|
||||
let validStatuses = ['pending', 'in-progress', 'completed'];
|
||||
return request.resource.data.status in validStatuses;
|
||||
}
|
||||
|
||||
allow create: if isValidStatus() && ...
|
||||
```
|
||||
|
||||
\#### 2. Validate State Transitions
|
||||
|
||||
For `update` operations, you **MUST** validate that a field is changing from a
|
||||
valid previous state to a valid new state. This prevents users from bypassing
|
||||
workflows (e.g., marking a task as 'completed' from 'archived').
|
||||
|
||||
**Example:**
|
||||
|
||||
```javascript
|
||||
// A task can only be marked 'completed' if it was 'in-progress'
|
||||
function validStatusTransition() {
|
||||
let previousStatus = resource.data.status;
|
||||
let newStatus = request.resource.data.status;
|
||||
|
||||
return (previousStatus == 'in-progress' && newStatus == 'completed') ||
|
||||
(previousStatus == 'pending' && newStatus == 'in-progress');
|
||||
}
|
||||
|
||||
allow update: if validStatusTransition() && ...
|
||||
```
|
||||
|
||||
#### 3. Strict Path and Relationship Scoping
|
||||
|
||||
For any field that references another resource (like an image path or a parent
|
||||
document ID), you **MUST** ensure it is correctly scoped to the user or valid
|
||||
within the context.
|
||||
|
||||
**Example:**
|
||||
|
||||
```javascript
|
||||
// Ensure image path is within the user's own storage folder
|
||||
allow create: if isScopedPath(request.resource.data.imageBucket) && ...
|
||||
```
|
||||
|
||||
#### 4. Secure Counter Updates
|
||||
|
||||
When allowing users to update a counter (like `voteCount` or `answerCount`), you
|
||||
**MUST** ensure: 1. **Atomic Increments:** The field is only changing by exactly
|
||||
+1 or -1. 2. **Isolation:** **NO OTHER FIELDS** are being modified. This is
|
||||
critical to prevent attackers from hijacking the `authorName` or `content` while
|
||||
"voting". 3. **Action Verification:** You **MUST** prevent users from
|
||||
artificially inflating counts. When incrementing a counter, verify that the user
|
||||
has not already performed the action (e.g., by checking for the existence of a
|
||||
'like' document) and is not looping updates. * **CRITICAL:** Relying solely on
|
||||
`!exists(likeDoc)` is insufficient because a malicious user can skip creating
|
||||
the document and loop the increment. * **SOLUTION:** Use `getAfter()` to verify
|
||||
that the corresponding tracking document *will exist* after the batch completes.
|
||||
|
||||
**Example:**
|
||||
|
||||
```javascript
|
||||
function isValidCounterUpdate(docId) {
|
||||
// Allow update only if 'voteCount' is the ONLY field changing
|
||||
return request.resource.data.diff(resource.data).affectedKeys().hasOnly(['voteCount']) &&
|
||||
// And the change is exactly +1 or -1
|
||||
math.abs(request.resource.data.voteCount - resource.data.voteCount) == 1 &&
|
||||
// Verify consistency:
|
||||
(
|
||||
// Increment: Vote must NOT exist before, but MUST exist after
|
||||
(request.resource.data.voteCount > resource.data.voteCount &&
|
||||
!exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) &&
|
||||
getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) != null) ||
|
||||
// Decrement: Vote MUST exist before, but must NOT exist after
|
||||
(request.resource.data.voteCount < resource.data.voteCount &&
|
||||
exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) &&
|
||||
getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) == null)
|
||||
);
|
||||
}
|
||||
|
||||
allow update: if isValidCounterUpdate(docId) && ...
|
||||
```
|
||||
|
||||
#### 5. **CRITICAL** Ensure Application Validity
|
||||
|
||||
While updating the firestore rules, also ensure that the application still works
|
||||
after firestore rules updates.
|
||||
|
||||
1. **For each collection, implement explicit data validation:**
|
||||
|
||||
- Type Checking: 'field is string', 'field is number', 'field is bool', 'field
|
||||
is timestamp'
|
||||
- Required fields validation using 'hasRequiredFields()'
|
||||
- **Enforce Size Limits:** For **EVERY** string, list, and map field, you
|
||||
**MUST** enforce realistic size limits (e.g., `text.size() < 1000`,
|
||||
`tags.size() < 20`). **Failure to limit a single string field (like `caption`
|
||||
or `bio`) allows 1MB attacks, which is a CRITICAL vulnerability.**
|
||||
- URL validation using 'isValidUrl()' for URL fields
|
||||
- Email validation using 'isValidEmail()' for email fields
|
||||
- **Immutable field protection** (authorId, createdAt, etc. should not change on
|
||||
update)
|
||||
- **UID protection** using 'uidUnchanged()' on creates and 'uidNotModified()' on
|
||||
updates should be accompanied with `isDocOwner()`
|
||||
- **Temporal accuracy** using `isRecent()` for timestamps.
|
||||
- **Range validation** using `isPositive()` or similar for numbers.
|
||||
- **Path scoping** using `isScopedPath()` for storage paths.
|
||||
|
||||
Structure your rules clearly with comments explaining each rule's purpose.
|
||||
|
||||
#### Phase-3: Devil's Advocate Attack
|
||||
|
||||
**Critical step:** Systematically attempt to break your own rules using the
|
||||
following attack vectors. You MUST document the outcome of each attempt.
|
||||
|
||||
1. **Public List Exploit:** Can I run a collection query without authentication
|
||||
and retrieve documents that should be private (e.g., where
|
||||
`visible == false`)?
|
||||
1. **Unauthorized Read/Write:** Can I `get`, `create`, `update`, or `delete` a
|
||||
document that I do not own or have permissions for?
|
||||
1. **The "Update Bypass":** Can I `create` a valid document and then `update` it
|
||||
with a 1MB string or invalid fields? (Tests if validation logic is missing
|
||||
from `update`).
|
||||
1. **Ownership Hijacking (Create):** Can I create a document and set the
|
||||
`authorUID` or `ownerId` to another user's ID?
|
||||
1. **Ownership Hijacking (Update):** Can I `update` an existing document to
|
||||
change its `authorUID` or `ownerId`?
|
||||
1. **Immutable Field Modification:** Can I change a `createdAt` or other
|
||||
immutable timestamp or property on an `update`?
|
||||
1. **Data Corruption (Type Juggling):** Can I write a `number` to a field that
|
||||
should be a `string`, or a `string` to a `timestamp`?
|
||||
1. **Validation Bypass (Create vs. Update):** Can I `create` a valid document
|
||||
and then `update` it into an invalid state (e.g., remove a required field,
|
||||
write a string that's too long)?
|
||||
1. **Resource Exhaustion / DoS:** Can I write an enormous string (e.g., 1MB) to
|
||||
any field that accepts a string or a massive array to a list field? Every
|
||||
string field (e.g., `bio`, `url`, `name`) MUST have a `.size()` check. If any
|
||||
are missing, it's a "Resource Exhaustion/DoS" risk.
|
||||
1. **Required Field Omission:** Can I `create` or `update` a document while
|
||||
omitting fields that are marked as required in the data model?
|
||||
1. **Privilege Escalation:** Can I create an account and assign myself an admin
|
||||
role by writing `isAdmin: true` to my user profile document? (Tests reliance
|
||||
on document data vs. custom claims).
|
||||
1. **Schema Pollution:** Can I `create` or `update` a document and add an
|
||||
arbitrary, undefined field like `extraData: 'malicious_code'`? (Tests for
|
||||
strict schema enforcement).
|
||||
1. **Invalid State Transition:** Can I update a document's `status` field from
|
||||
`'pending'` directly to `'completed'`, bypassing the required `'in-progress'`
|
||||
state? (Tests business logic enforcement).
|
||||
1. **Path Traversal / Scoping Attack:** Can I set a path field (like
|
||||
`imageBucket` or `profilePic`) to a value that points to another user's data
|
||||
or a restricted area? (Tests for regex path scoping).
|
||||
1. **Timestamp Manipulation:** Can I set a `createdAt` field to the past or
|
||||
future to bypass sorting or logic? (Tests for `request.time` validation).
|
||||
1. **Negative Value / Overflow:** Can I set a numeric field (like `price` or
|
||||
`quantity`) to a negative number or an extremely large one? (Tests for range
|
||||
validation).
|
||||
1. **The "Mixed Content" Leak:** Create a second user. Can User B read User A's
|
||||
users document? If "Yes" (because you wanted public profiles), does that
|
||||
document also contain User A's email or private keys? If both are true, the
|
||||
rules are insecure.
|
||||
1. **Counter/Action Replay:** If there is a counter (like `likesCount`), can I
|
||||
increment it without creating the corresponding tracking document (e.g.,
|
||||
inside `likes/{userId}`)? Can I increment it twice? (Tests for `getAfter()`
|
||||
consistency checks).
|
||||
1. **Orphaned Subcollection Access:** Can I read/write to a subcollection (e.g.,
|
||||
`users/123/posts/456`) if the parent document (`users/123`) does not exist?
|
||||
(Tests for parent existence checks).
|
||||
1. **Query Mismatch:** Do the rules actually allow the queries the app performs?
|
||||
(e.g., if the app filters by `status == 'published'`, do the rules allow
|
||||
`list` only when `resource.data.status == 'published'`?)
|
||||
1. **Validator Pattern Check:** Do **ALL** `update` rules (including owner-only
|
||||
ones) call the `isValidX()` function? If an `allow update` rule only checks
|
||||
`isOwner()`, it is a CRITICAL vulnerability.
|
||||
|
||||
Document each attack attempt and whether it succeeded. If ANY attack succeeds:
|
||||
|
||||
- Fix the security hole
|
||||
- Regenerate the rules
|
||||
- **Repeat Phase-3** until no attacks succeed
|
||||
|
||||
#### Phase-4: Syntactic Validation
|
||||
|
||||
Once devil's advocate testing passes, repeat until rules pass validation.
|
||||
|
||||
**After all phases are complete, create or update the `firestore.rules` file.**
|
||||
|
||||
### Critical Constraints
|
||||
|
||||
1. **Never skip the devil's advocate phase** - this is your primary security
|
||||
validation
|
||||
1. **MUST include helper functions** for common operations ('isAuthenticated',
|
||||
'isOwner', 'uidUnchanged', 'uidNotModified') AND domain validators
|
||||
('isValidUser', etc.)
|
||||
1. **MUST document assumed data models** at the beginning of the rules file
|
||||
1. **Always validate the rules syntax** using 'firebase deploy --only
|
||||
firestore:rules --dry-run' or a similar tool before outputting the final
|
||||
file.
|
||||
1. **Provide complete, runnable code** - no placeholders or TODOs
|
||||
1. **Document all assumptions** about data structure or access patterns
|
||||
1. **Always run the devil's advocate attack** after any modification of the
|
||||
rules.
|
||||
1. **Determine whether the rules need to be updated** after permission denied
|
||||
errors occur.
|
||||
1. **Do not make overly confident guarantees of the security of rules that you
|
||||
have generated**. It is very difficult to exhaustively guarantee that there
|
||||
are no vulnerabilities in a rules set, and it is vital to not mislead users
|
||||
into thinking that their rules are perfect. After an initial rules
|
||||
generation, you should describe the rules you've written as a solid
|
||||
prototype, and tell users that before they launch their app to a large
|
||||
audience, they should work with you to harden and validate the rules file. Be
|
||||
clear that users should carefully review rules to ensure security.
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
# Web SDK Usage (Enterprise Native Mode)
|
||||
|
||||
This guide focuses on the **Modular Web SDK** (v9+) designed for tree-shaking
|
||||
and efficiency.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 1. Initialization
|
||||
|
||||
Initialize the SDK and Firestore instance:
|
||||
|
||||
```javascript
|
||||
import { initializeApp } from "firebase/app";
|
||||
import { getFirestore } from "firebase/firestore";
|
||||
|
||||
const firebaseConfig = {
|
||||
// Retrieve values using 'firebase apps:sdkconfig web <app-id>'
|
||||
};
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const db = getFirestore(app);
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 2. Decision Framework: Pipelines vs. Standard Queries
|
||||
|
||||
> [!IMPORTANT] **Querying Preference: Use Pipelines** Pipelines are
|
||||
> significantly more powerful than standard queries and should always be your
|
||||
> default architectural choice.
|
||||
|
||||
### Rules & Accountability
|
||||
|
||||
1. **Check API References:** Always read `pipeline.d.ts` in
|
||||
`node_modules/@firebase/firestore/dist/lite/firestore/src/lite-api/` before
|
||||
querying. Reference `expressions.d.ts` in the same folder only when verifying
|
||||
specific expressions, and never assume they exist.
|
||||
1. **Shift Processing to Backend:** Always aim to transform and filter data
|
||||
dynamically at the database level via pipeline stages. Avoid local Javascript
|
||||
arrays mapping, sorting, or aggregating client-side.
|
||||
1. **Reverting to Standard Queries:** Standard `collection.where()` queries and
|
||||
real-time listeners are an EXCEPTION. Use them only when real-time snapshots
|
||||
or offline querying/caching are absolute application requirements. When doing
|
||||
so, explicitly document the reason in your response.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 3. Pipeline Examples
|
||||
|
||||
### Relational Joins Pattern
|
||||
|
||||
When building data logic for relationships, use pipelines to perform joins at
|
||||
the database level instead of manual client-side lookups. - Use `.define()` to
|
||||
bind alias parameters. - Invoke `.addFields()` incorporating a new subquery
|
||||
linking the documents.
|
||||
|
||||
```javascript
|
||||
import { field, variable } from "firebase/firestore/pipelines";
|
||||
|
||||
// Fetch articles and join the associated author Profile side-by-side
|
||||
const articlesWithAuthProfile = db.pipeline().collection("articles")
|
||||
.define(field("authorUid").as("author_id"))
|
||||
.addFields(
|
||||
db.pipeline().collection("users")
|
||||
.where(field("__name__").documentId().equal(variable("author_id")))
|
||||
.select(field("displayName"), field("avatarUrl"), field("handle"))
|
||||
.toScalarExpression()
|
||||
.as("author")
|
||||
);
|
||||
```
|
||||
|
||||
### Full-Text Search
|
||||
|
||||
Leverage the database-native `.search()` stage for high-performance text
|
||||
lookups.
|
||||
|
||||
```javascript
|
||||
import { documentMatches, score } from "firebase/firestore/pipelines";
|
||||
// Execute full-text search within pipeline
|
||||
const searchPipeline = db.pipeline()
|
||||
.collection("articles")
|
||||
.search({
|
||||
query: documentMatches("machine learning"),
|
||||
sort: score().descending()
|
||||
})
|
||||
.limit(5);
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 4. Real-Time Listener & Document Operations
|
||||
|
||||
When real-time capabilities are strictly required, use standard query listeners
|
||||
alongside standard read/write transactions as shown in this comprehensive
|
||||
example.
|
||||
|
||||
```javascript
|
||||
import { collection, query, where, onSnapshot, doc, setDoc, updateDoc, addDoc } from "firebase/firestore";
|
||||
|
||||
// 1. Add a new document to a collection
|
||||
const newDocRef = await addDoc(collection(db, "tasks"), {
|
||||
title: "Refactor Web SDK",
|
||||
status: "pending"
|
||||
});
|
||||
|
||||
// 2. Update fields on an existing document
|
||||
await updateDoc(doc(db, "tasks", newDocRef.id), {
|
||||
priority: "high"
|
||||
});
|
||||
|
||||
// 3. Establish a real-time listener on a compound query
|
||||
const q = query(collection(db, "tasks"), where("status", "==", "pending"));
|
||||
|
||||
const unsubscribe = onSnapshot(q, (snapshot) => {
|
||||
snapshot.docChanges().forEach((change) => {
|
||||
if (change.type === "added") {
|
||||
console.log("Added Task: ", change.doc.id, change.doc.data());
|
||||
}
|
||||
if (change.type === "modified") {
|
||||
console.log("Updated Task: ", change.doc.id, change.doc.data());
|
||||
}
|
||||
if (change.type === "removed") {
|
||||
console.log("Removed Task: ", change.doc.id, change.doc.data());
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
# Cloud Firestore on Android (Kotlin)
|
||||
|
||||
This guide walks you through using Cloud Firestore in your Android app using
|
||||
Kotlin.
|
||||
|
||||
### Enable Firestore via CLI
|
||||
|
||||
Before adding dependencies in your app, make sure you enable the Firestore
|
||||
service in your Firebase Project using the Firebase CLI:
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest init firestore
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 1. Add Dependencies
|
||||
|
||||
In your module-level `build.gradle.kts` (usually `app/build.gradle.kts`), add
|
||||
the dependency for Cloud Firestore:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// [AGENT] Fetch the latest available BoM version from https://firebase.google.com/support/release-notes/android before adding this
|
||||
implementation(platform("com.google.firebase:firebase-bom:<latest_bom_version>"))
|
||||
|
||||
// Add the dependency for the Cloud Firestore library
|
||||
// When using the BoM, you don't specify versions in Firebase library dependencies
|
||||
implementation("com.google.firebase:firebase-firestore")
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 2. Initialize Firestore
|
||||
|
||||
In your Activity or Fragment, initialize the `FirebaseFirestore` instance:
|
||||
|
||||
```kotlin
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import com.google.firebase.firestore.ktx.firestore
|
||||
import com.google.firebase.ktx.Firebase
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var db: FirebaseFirestore
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val db = Firebase.firestore
|
||||
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
Text("Firestore initialized!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Jetpack Compose (Modern)
|
||||
|
||||
Initialize inside a `ComponentActivity` using `setContent`:
|
||||
|
||||
```kotlin
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.firestore.firestore
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val db = Firebase.firestore
|
||||
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
Text("Firestore initialized!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 3. Add Data
|
||||
|
||||
Add a new document with a generated ID using `add()`:
|
||||
|
||||
```kotlin
|
||||
// Create a new user with a first and last name
|
||||
val user = hashMapOf(
|
||||
"first" to "Ada",
|
||||
"last" to "Lovelace",
|
||||
"born" to 1815
|
||||
)
|
||||
|
||||
// Add a new document with a generated ID
|
||||
db.collection("users")
|
||||
.add(user)
|
||||
.addOnSuccessListener { documentReference ->
|
||||
Log.d(TAG, "DocumentSnapshot added with ID: ${documentReference.id}")
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Log.w(TAG, "Error adding document", e)
|
||||
}
|
||||
```
|
||||
|
||||
Or set a document with a specific ID using `set()`:
|
||||
|
||||
```kotlin
|
||||
val city = hashMapOf(
|
||||
"name" to "Los Angeles",
|
||||
"state" to "CA",
|
||||
"country" to "USA"
|
||||
)
|
||||
|
||||
db.collection("cities").document("LA")
|
||||
.set(city)
|
||||
.addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully written!") }
|
||||
.addOnFailureListener { e -> Log.w(TAG, "Error writing document", e) }
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 4. Read Data
|
||||
|
||||
Read a single document using `get()`:
|
||||
|
||||
```kotlin
|
||||
val docRef = db.collection("cities").document("SF")
|
||||
docRef.get()
|
||||
.addOnSuccessListener { document ->
|
||||
if (document != null && document.exists()) {
|
||||
Log.d(TAG, "DocumentSnapshot data: ${document.data}")
|
||||
} else {
|
||||
Log.d(TAG, "No such document")
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { exception ->
|
||||
Log.d(TAG, "get failed with ", exception)
|
||||
}
|
||||
```
|
||||
|
||||
Read multiple documents using a query:
|
||||
|
||||
```kotlin
|
||||
db.collection("cities")
|
||||
.whereEqualTo("capital", true)
|
||||
.get()
|
||||
.addOnSuccessListener { documents ->
|
||||
for (document in documents) {
|
||||
Log.d(TAG, "${document.id} => ${document.data}")
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { exception ->
|
||||
Log.w(TAG, "Error getting documents: ", exception)
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 5. Update Data
|
||||
|
||||
Update some fields of a document using `update()` without overwriting the entire
|
||||
document:
|
||||
|
||||
```kotlin
|
||||
val washingtonRef = db.collection("cities").document("DC")
|
||||
|
||||
// Set the "isCapital" field to true
|
||||
washingtonRef
|
||||
.update("capital", true)
|
||||
.addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully updated!") }
|
||||
.addOnFailureListener { e -> Log.w(TAG, "Error updating document", e) }
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
### 6. Delete Data
|
||||
|
||||
Delete a document using `delete()`:
|
||||
|
||||
```kotlin
|
||||
db.collection("cities").document("DC")
|
||||
.delete()
|
||||
.addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully deleted!") }
|
||||
.addOnFailureListener { e -> Log.w(TAG, "Error deleting document", e) }
|
||||
```
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
# Cloud Firestore in Flutter
|
||||
|
||||
This guide covers basic CRUD operations, type-safe data modeling, and real-time
|
||||
streams when using Cloud Firestore in a Flutter application via the
|
||||
`cloud_firestore` package.
|
||||
|
||||
## 1. Setup
|
||||
|
||||
Ensure you have added the required dependency:
|
||||
|
||||
```bash
|
||||
flutter pub add cloud_firestore
|
||||
```
|
||||
|
||||
Also, ensure FlutterFire is configured properly for your target platforms.
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 2. Best Practices: Type-Safe Models
|
||||
|
||||
Instead of passing raw `Map<String, dynamic>` maps throughout your UI layer,
|
||||
define a domain model class with `fromFirestore` and `toFirestore` converters to
|
||||
maintain type safety.
|
||||
|
||||
```dart
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
class Item {
|
||||
final String id;
|
||||
final String name;
|
||||
final String ownerId;
|
||||
final DateTime createdAt;
|
||||
|
||||
Item({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.ownerId,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory Item.fromFirestore(DocumentSnapshot doc) {
|
||||
final data = doc.data() as Map<String, dynamic>? ?? {};
|
||||
return Item(
|
||||
id: doc.id,
|
||||
name: data['name'] as String? ?? '',
|
||||
ownerId: data['ownerId'] as String? ?? '',
|
||||
createdAt: data['createdAt'] is Timestamp
|
||||
? (data['createdAt'] as Timestamp).toDate()
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'name': name,
|
||||
'ownerId': ownerId,
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 3. The Service Layer
|
||||
|
||||
Encapsulate all database interactions within a dedicated service class to keep
|
||||
your UI code clean and testable.
|
||||
|
||||
### Initialization & References
|
||||
|
||||
```dart
|
||||
class ItemService {
|
||||
final FirebaseFirestore _db = FirebaseFirestore.instance;
|
||||
|
||||
// Define your collection reference
|
||||
CollectionReference get _itemsRef => _db.collection('items');
|
||||
|
||||
// 1. Create Data
|
||||
Future<void> createItem(Item item) async {
|
||||
try {
|
||||
await _itemsRef.add(item.toFirestore());
|
||||
} catch (e) {
|
||||
print("Error creating document: \$e");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Read Data (One-Time Fetch)
|
||||
Future<List<Item>> fetchItems(String ownerId) async {
|
||||
try {
|
||||
final querySnapshot = await _itemsRef
|
||||
.where('ownerId', isEqualTo: ownerId)
|
||||
.orderBy('createdAt', descending: true)
|
||||
.get();
|
||||
|
||||
return querySnapshot.docs.map((doc) => Item.fromFirestore(doc)).toList();
|
||||
} catch (e) {
|
||||
print("Error fetching documents: \$e");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Read Data (Real-Time Stream)
|
||||
Stream<List<Item>> streamItems(String ownerId) {
|
||||
return _itemsRef
|
||||
.where('ownerId', isEqualTo: ownerId)
|
||||
.snapshots()
|
||||
.map((snapshot) {
|
||||
// If a custom composite index is missing during prototyping, apply sorting client-side:
|
||||
final items = snapshot.docs.map((doc) => Item.fromFirestore(doc)).toList();
|
||||
items.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return items;
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Update Data
|
||||
Future<void> updateItemName(String id, String newName) async {
|
||||
try {
|
||||
await _itemsRef.doc(id).update({'name': newName});
|
||||
} catch (e) {
|
||||
print("Error updating document: \$e");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Delete Data
|
||||
Future<void> deleteItem(String id) async {
|
||||
try {
|
||||
await _itemsRef.doc(id).delete();
|
||||
} catch (e) {
|
||||
print("Error deleting document: \$e");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
## 4. Listening to Streams in the UI (`StreamBuilder`)
|
||||
|
||||
Use Flutter's `StreamBuilder` to rebuild the interface reactively whenever data
|
||||
changes in your database collection.
|
||||
|
||||
```dart
|
||||
StreamBuilder<List<Item>>(
|
||||
stream: itemService.streamItems(currentUser.uid),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return const Center(child: Text('Failed to load data'));
|
||||
}
|
||||
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final items = snapshot.data ?? [];
|
||||
|
||||
if (items.isEmpty) {
|
||||
return const Center(child: Text('No items found.'));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return ListTile(
|
||||
title: Text(item.name),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => itemService.deleteItem(item.id),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
# 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
|
||||
|
||||
In Standard Edition, Firestore **automatically creates** a single-field index
|
||||
for every field in a document (and subfields in maps). * **Support**: Simple
|
||||
equality queries (`==`) and single-field range/sort queries (`<`, `<=`,
|
||||
`orderBy`). * **Behavior**: You generally don't need to manage these unless you
|
||||
want to *exempt* a field.
|
||||
|
||||
### 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**<br>\`where("a", | Automatic (Single-Field) |
|
||||
| : "==", 1)\` : : | |
|
||||
| **Simple Range/Sort**<br>\`where("a", | Automatic (Single-Field) |
|
||||
| : ">", 1).orderBy("a")\` : : | |
|
||||
| **Multiple Equality**<br>\`where("a", | Automatic (Merged Single-Field) |
|
||||
| : "==", 1).where("b", "==", 2)\` : : | |
|
||||
| \*\*Equality + | **Composite Index** |
|
||||
| : Range/Sort\*\*<br>\`where("a", "==", : : | |
|
||||
| : 1).where("b", ">", 2)\` : : | |
|
||||
| **Multiple Ranges**<br>\`where("a", | **Composite Index** (and technically |
|
||||
| : ">", 1).where("b", ">", 2)\` : limited query support) : | |
|
||||
| \*\*Array Contains + | **Composite Index** |
|
||||
| : Equality\*\*<br>\`where("tags", : : | |
|
||||
| : "array-contains", : : | |
|
||||
| : "news").where("active", "==", true)\` : : | |
|
||||
|
||||
## 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 npx -y firebase-tools@latest deploy --only firestore:indexes`
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
# Firebase Firestore iOS Setup Guide
|
||||
|
||||
# ⛔️ CRITICAL RULE: NO FirebaseFirestoreSwift ⛔️
|
||||
|
||||
UNDER NO CIRCUMSTANCES should you import, link against, or configure a project
|
||||
to use `FirebaseFirestoreSwift`.
|
||||
|
||||
As of Firebase SDK v11+, all Swift-specific features (including `@DocumentID`,
|
||||
`@ServerTimestamp`, and `Codable` support) have been fully merged into the main
|
||||
`FirebaseFirestore` module.
|
||||
|
||||
- NEVER add `.external(name: "FirebaseFirestoreSwift")` or similar to SPM or
|
||||
Xcode configurations.
|
||||
- NEVER write `import FirebaseFirestoreSwift` in any Swift file.
|
||||
- ONLY use `import FirebaseFirestore`.
|
||||
|
||||
This is a zero-tolerance constraint. Using `FirebaseFirestoreSwift` is
|
||||
fundamentally incorrect and unacceptable.
|
||||
|
||||
# ⛔️ CRITICAL RULE: NO INLINE INITIALIZATION ⛔️
|
||||
|
||||
NEVER write `let db = Firestore.firestore()` as an inline class or struct
|
||||
property if there is ANY chance the object is instantiated before
|
||||
`FirebaseApp.configure()` executes in the app root.
|
||||
|
||||
- **FATAL CRASH:**
|
||||
`@Observable class DataManager { let db = Firestore.firestore() }` initialized
|
||||
as a `@State` in the App root.
|
||||
- **SAFE PATTERN:** Initialize `Firestore.firestore()` lazily
|
||||
(`lazy var db = Firestore.firestore()`) OR explicitly initialize the manager
|
||||
*after* `FirebaseApp.configure()` finishes.
|
||||
|
||||
## 1. Import and Initialize
|
||||
|
||||
Ensure you have installed the `FirebaseFirestore` SDK. Use the
|
||||
`xcode-project-setup` skill to automate adding the SPM dependency to the Xcode
|
||||
project.
|
||||
|
||||
```swift
|
||||
import FirebaseFirestore
|
||||
```
|
||||
|
||||
Initialize an instance of Cloud Firestore:
|
||||
|
||||
```swift
|
||||
let db = Firestore.firestore()
|
||||
```
|
||||
|
||||
## 2. Type-Safe Data Models (Codable)
|
||||
|
||||
To leverage modern Swift data modeling, define your data as `Codable` structs.
|
||||
The main `FirebaseFirestore` module automatically supports mapping these types.
|
||||
|
||||
```swift
|
||||
struct User: Codable {
|
||||
@DocumentID var id: String?
|
||||
var firstName: String
|
||||
var lastName: String
|
||||
var born: Int
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Writing Data (Modern Concurrency & Codable)
|
||||
|
||||
Using `async/await` and `Codable` ensures type safety and avoids callback hell.
|
||||
|
||||
```swift
|
||||
let user = User(firstName: "Ada", lastName: "Lovelace", born: 1815)
|
||||
|
||||
do {
|
||||
// Add a new document with a generated ID using Codable
|
||||
let ref = try db.collection("users").addDocument(from: user)
|
||||
print("Document added with ID: \(ref.documentID)")
|
||||
} catch {
|
||||
print("Error adding document: \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Reading Data (Modern Concurrency & Codable)
|
||||
|
||||
```swift
|
||||
do {
|
||||
let querySnapshot = try await db.collection("users").getDocuments()
|
||||
|
||||
// Map documents to the User struct automatically
|
||||
let users = querySnapshot.documents.compactMap { document in
|
||||
try? document.data(as: User.self)
|
||||
}
|
||||
|
||||
for user in users {
|
||||
print("Found user: \(user.firstName) \(user.lastName)")
|
||||
}
|
||||
} catch {
|
||||
print("Error getting documents: \(error)")
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Realtime Listeners in SwiftUI (Lifecycle Best Practices)
|
||||
|
||||
When implementing Firestore realtime listeners (`addSnapshotListener`) within a
|
||||
SwiftUI application, you **MUST** tie the listener lifecycle to the view's
|
||||
identity using `.task(id:)`, NOT `.onDisappear`.
|
||||
|
||||
### ⛔️ UNSAFE PATTERN (.onDisappear)
|
||||
|
||||
Presenting a `.sheet` or `.fullScreenCover` can trigger the underlying view's
|
||||
`onDisappear` method. If you stop your listener here, the feed will stop
|
||||
updating while the sheet is open, and won't resume when it's dismissed.
|
||||
|
||||
### ✅ SAFE PATTERN (.task with deinit)
|
||||
|
||||
Because `addSnapshotListener` is a synchronous call, placing it inside a `.task`
|
||||
means the task completes immediately. This breaks SwiftUI's automatic
|
||||
cancellation mechanism.
|
||||
|
||||
To safely manage traditional Firebase listeners in SwiftUI, you must use
|
||||
**`deinit`** to handle memory cleanup when the view is destroyed, and
|
||||
**`.task(id:)`** to handle data identity changes while the view is active.
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
import FirebaseFirestore
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class DataManager {
|
||||
private var listenerHandle: ListenerRegistration?
|
||||
var data: [String] = []
|
||||
|
||||
func startListening(for userId: String) {
|
||||
// 1. Clean up any existing listener to prevent duplicates if the ID changes
|
||||
stopListening()
|
||||
|
||||
// 2. Start the regular listener and capture the handle
|
||||
listenerHandle = Firestore.firestore().collection("users").document(userId).addSnapshotListener { snapshot, error in
|
||||
// Handle updates
|
||||
}
|
||||
}
|
||||
|
||||
func stopListening() {
|
||||
listenerHandle?.remove()
|
||||
listenerHandle = nil
|
||||
}
|
||||
|
||||
// 3. Guarantee cleanup when the View is destroyed and this object is deallocated
|
||||
isolated deinit {
|
||||
stopListening()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, in your SwiftUI View, trigger the listener using `.task(id:)`.
|
||||
|
||||
```swift
|
||||
struct MyView: View {
|
||||
@State private var manager = DataManager()
|
||||
@Environment(AuthManager.self) var authManager
|
||||
|
||||
var body: some View {
|
||||
List(manager.data, id: \.self) { item in
|
||||
Text(item)
|
||||
}
|
||||
// .task(id:) automatically re-runs if the userId changes.
|
||||
// The view model handles stopping the old listener and starting the new one.
|
||||
.task(id: authManager.userId) {
|
||||
if let userId = authManager.userId {
|
||||
manager.startListening(for: userId)
|
||||
} else {
|
||||
manager.stopListening()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
# Provisioning Cloud Firestore
|
||||
|
||||
## Manual Initialization
|
||||
|
||||
Initialize the following firebase configuration files manually. Do not use
|
||||
`npx -y firebase-tools@latest init`, as it expects interactive inputs.
|
||||
|
||||
1. **Create `firebase.json`**: This file configures the Firebase CLI.
|
||||
1. **Create `firestore.rules`**: This file contains your security rules.
|
||||
1. **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 with the Standard edition. To use a different
|
||||
database, specify the database ID and location:
|
||||
|
||||
1. Run `npx -y firebase-tools@latest firestore:locations` to get the list of
|
||||
locations.
|
||||
1. Ask the user which location to use, suggesting colocation if other parts of
|
||||
the app already have a region selected.
|
||||
|
||||
You can check the list of available databases using
|
||||
`npx -y firebase-tools@latest firestore:databases:list`.
|
||||
|
||||
If the database does not exist, it will be created when you deploy with the
|
||||
specified configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"firestore": {
|
||||
"rules": "firestore.rules",
|
||||
"indexes": "firestore.indexes.json",
|
||||
"database": "my-database-id",
|
||||
"location": "<selected-location>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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 database, rules and indexes
|
||||
|
||||
**CRITICAL**: You MUST deploy the firestore configuration for the database to be
|
||||
provisioned in the cloud and for your rules/indexes to take effect. If you don't
|
||||
run this, your database will not exist.
|
||||
|
||||
```bash
|
||||
# To deploy all rules and indexes
|
||||
npx -y firebase-tools@latest deploy --only firestore
|
||||
|
||||
# To deploy just rules
|
||||
npx -y firebase-tools@latest deploy --only firestore:rules
|
||||
|
||||
# To deploy just indexes
|
||||
npx -y firebase-tools@latest deploy --only firestore:indexes
|
||||
```
|
||||
|
||||
## Local Emulation
|
||||
|
||||
To run Firestore locally for development and testing:
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest emulators:start --only firestore
|
||||
```
|
||||
|
||||
This starts the Firestore emulator, typically on port 8080. You can interact
|
||||
with it using the Emulator UI (usually at http://localhost:4000/firestore).
|
||||
|
|
@ -0,0 +1,577 @@
|
|||
## 1. Generate Firestore Rules
|
||||
|
||||
You are an expert Firebase Security Rules engineer with deep knowledge of
|
||||
Firestore security best practices. Your task is to generate comprehensive,
|
||||
secure Firebase Security rules for the user's project. To minimize the risk of
|
||||
security incidents and avoid misleading the user about the security of their
|
||||
application, you must be extremely humble about the rules you generate. Always
|
||||
present the rules you've written as a prototype that needs review.
|
||||
|
||||
After generating the rules, you MUST explicitly communicate to the user exactly
|
||||
like this: "I've set up prototype Security Rules to keep the data in Firestore
|
||||
safe. They are designed to be secure for <explain reasons here>. However, you
|
||||
should review and verify them before broadly sharing your app. If you'd like, I
|
||||
can help you harden these rules."
|
||||
|
||||
### Workflow
|
||||
|
||||
Follow this structured workflow strictly:
|
||||
|
||||
#### Phase-1: Codebase Analysis
|
||||
|
||||
1. **Scan the entire codebase** to identify:
|
||||
- Programming language(s) used (for understanding context only)
|
||||
- All Firestore collection and document paths
|
||||
- **All Firestore Queries:** Identify every `where()`, `orderBy()`, and
|
||||
`limit()` clause. The security rules **MUST** allow these specific queries.
|
||||
- Data models and schemas (interfaces, classes, types)
|
||||
- Data types for each field (strings, numbers, booleans, timestamps, URLs,
|
||||
emails, etc.)
|
||||
- Required vs. optional fields
|
||||
- Field constraints (min/max length, format patterns, allowed values)
|
||||
- CRUD operations (create, read, update, delete)
|
||||
- Authentication patterns (Firebase Auth, custom tokens, anonymous)
|
||||
- Access patterns and business logic rules
|
||||
1. **Document your findings** in a untracked file. Refer to this file when
|
||||
generating the security rules.
|
||||
|
||||
#### Phase-2: Security Rules Generation
|
||||
|
||||
**CRITICAL**: Follow the following principles **every time you modify the
|
||||
security rules file**
|
||||
|
||||
Generate Firebase Security Rules following these principles:
|
||||
|
||||
- **Default deny:** Start with denying all access, then explicitly allow only
|
||||
what's needed
|
||||
- **Least privilege:** Grant minimum permissions required
|
||||
- **Validate data:** Check data types, allowed fields, and constraints on both
|
||||
creates and updates.
|
||||
- **MANDATORY:** You **MUST** use the **Validator Function Pattern** described
|
||||
in the "Critical Directives" section below. This involves defining a
|
||||
specific validation function (e.g., `isValidUser`) and calling it in
|
||||
**BOTH** `create` and `update` rules.
|
||||
- **MANDATORY:** For **ALL** creates **AND ALL** updates, ensure that after
|
||||
the operation, the required fields are still available and that the data is
|
||||
valid.
|
||||
- **Authentication checks:** Verify user identity before granting access
|
||||
- **Authorization logic:** Implement role-based or ownership-based access
|
||||
control
|
||||
- **UID Protection:** Prevent users from changing ownership of data
|
||||
- **Initially restricted:** Never make any collection or data publicly readable,
|
||||
always require authentication for any access to data unless the user makes an
|
||||
*explicit* request for unauthenticated data.
|
||||
|
||||
This means the first firestore.rules file you generate must never have any
|
||||
"allow read: true" statements.
|
||||
|
||||
**Structure Requirements:**
|
||||
|
||||
1. **Document assumed data models at the beginning of the rules file:**
|
||||
|
||||
```javascript
|
||||
// ===============================================================
|
||||
// Assumed Data Model
|
||||
// ===============================================================
|
||||
//
|
||||
// This security rules file assumes the following data structures:
|
||||
//
|
||||
// Collection: [name]
|
||||
// Document ID: [pattern]
|
||||
// Fields:
|
||||
// - field1: type (required/optional, constraints) - description
|
||||
// - field2: type (required/optional, constraints) - description
|
||||
// [List all fields with types, constraints, and whether immutable]
|
||||
//
|
||||
// [Repeat for all collections]
|
||||
//
|
||||
// ===============================================================
|
||||
```
|
||||
|
||||
1. **Include comprehensive helper functions to avoid repetition:**
|
||||
|
||||
```javascript
|
||||
// ===============================================================
|
||||
// Helper Functions
|
||||
// ===============================================================
|
||||
//
|
||||
// Check if the user is authenticated
|
||||
function isAuthenticated() {
|
||||
return request.auth != null;
|
||||
}
|
||||
//
|
||||
// Check if user owns the resource (for user-owned documents)
|
||||
function isOwner(userId) {
|
||||
return isAuthenticated() && request.auth.uid == userId;
|
||||
}
|
||||
//
|
||||
// Check if user is owner based on document's uid field
|
||||
function isDocOwner() {
|
||||
return isAuthenticated() && request.auth.uid == resource.data.uid;
|
||||
}
|
||||
//
|
||||
// Verify UID hasn't been tampered with on create
|
||||
function uidUnchanged() {
|
||||
return !('uid' in request.resource.data) ||
|
||||
request.resource.data.uid == request.auth.uid;
|
||||
}
|
||||
//
|
||||
// Ensure uid field is not modified on update
|
||||
function uidNotModified() {
|
||||
return !('uid' in request.resource.data) ||
|
||||
request.resource.data.uid == resource.data.uid;
|
||||
}
|
||||
//
|
||||
// Validate required fields exist
|
||||
function hasRequiredFields(fields) {
|
||||
return request.resource.data.keys().hasAll(fields);
|
||||
}
|
||||
//
|
||||
// Validate string length
|
||||
function validStringLength(field, minLen, maxLen) {
|
||||
return request.resource.data[field] is string &&
|
||||
request.resource.data[field].size() >= minLen &&
|
||||
request.resource.data[field].size() <= maxLen;
|
||||
}
|
||||
//
|
||||
// Validate URL format (must start with https:// or http://)
|
||||
function isValidUrl(url) {
|
||||
return url is string &&
|
||||
(url.matches("^https://.*") || url.matches("^http://.*"));
|
||||
}
|
||||
//
|
||||
// Validate email format
|
||||
function isValidEmail(email) {
|
||||
return email is string &&
|
||||
email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
|
||||
}
|
||||
|
||||
//
|
||||
// Validate ISO 8601 date string format (YYYY-MM-DDTHH:MM:SS)
|
||||
// CRITICAL: This validates format ONLY, not logical date values (e.g., month 13).
|
||||
// Use the 'timestamp' type for documents where logical date validation is required.
|
||||
function isValidDateString(dateStr) {
|
||||
return dateStr is string &&
|
||||
dateStr.matches("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*Z?$");
|
||||
}
|
||||
|
||||
//
|
||||
// Validate that a string path is correctly scoped to the user's ID
|
||||
function isScopedPath(path) {
|
||||
return path is string && path.matches("^users/" + request.auth.uid + "/.*");
|
||||
}
|
||||
//
|
||||
// Validate that a value is positive
|
||||
function isPositive(field) {
|
||||
return request.resource.data[field] is number && request.resource.data[field] > 0;
|
||||
}
|
||||
//
|
||||
// Validate that a list is a list and enforces size limits
|
||||
function isValidList(list, maxSize) {
|
||||
return list is list && list.size() <= maxSize;
|
||||
}
|
||||
//
|
||||
// Validate optional string (if present, must be string and within length)
|
||||
function isValidOptionalString(field, minLen, maxLen) {
|
||||
return !('field' in request.resource.data) ||
|
||||
(request.resource.data[field] is string &&
|
||||
request.resource.data[field].size() >= minLen &&
|
||||
request.resource.data[field].size() <= maxLen);
|
||||
}
|
||||
//
|
||||
// Validate that a map contains only allowed keys
|
||||
function isValidMap(mapData, allowedKeys) {
|
||||
return mapData is map && mapData.keys().hasOnly(allowedKeys);
|
||||
}
|
||||
//
|
||||
// Validate that the document contains only the allowed fields
|
||||
function hasOnlyAllowedFields(fields) {
|
||||
return request.resource.data.keys().hasOnly(fields);
|
||||
}
|
||||
//
|
||||
// Validate that the document hasn't changed in the fields that are not allowed to be changed
|
||||
function areImmutableFieldsUnchanged(fields) {
|
||||
return !request.resource.data.diff(resource.data).affectedKeys().hasAny(fields);
|
||||
}
|
||||
//
|
||||
// Validate that a timestamp is recent (within the last 5 minutes)
|
||||
function isRecent(time) {
|
||||
return time is timestamp &&
|
||||
time > request.time - duration.value(5, 'm') &&
|
||||
time <= request.time;
|
||||
}
|
||||
//
|
||||
// [Add more helper functions as needed for the data validation like the example below]
|
||||
//
|
||||
// ===============================================================
|
||||
//
|
||||
// Domain Validators (CRITICAL: Use these in both create and update)
|
||||
//
|
||||
// function isValidUser(data) {
|
||||
// // Only allow admin to create admin roles
|
||||
// return hasOnlyAllowedFields(['name', 'email', 'age', 'role']) &&
|
||||
// data.name is string && data.name.size() > 0 && data.name.size() < 50 &&
|
||||
// data.email is string && isValidEmail(data.email) &&
|
||||
// data.age is number && data.age >= 18 &&
|
||||
// data.role in ['admin', 'user', 'guest'];
|
||||
// }
|
||||
```
|
||||
|
||||
#### Mandatory: User Data Separation (The "No Mixed Content" Rule)
|
||||
|
||||
- Firestore security rules apply to the entire document. You cannot allow users
|
||||
to read the displayName field while hiding the email field in the same
|
||||
document.
|
||||
- If a collection (e.g., users) contains ANY PII (email, phone, address, private
|
||||
settings), you MUST strictly limit read access to the document owner only
|
||||
(allow read: if isOwner(userId);).
|
||||
- If the application requires public profiles (e.g., showing user names/avatars
|
||||
on posts):
|
||||
- 1. Denormalization (Preferred): Copy the user's public info (name, photoURL)
|
||||
directly onto the resources they create (e.g., store authorName and
|
||||
authorPhoto inside the posts document).
|
||||
- 2. Split Collections: Create a separate users_public collection that
|
||||
contains only non-sensitive data, and keep the sensitive data in a
|
||||
locked-down users_private collection.
|
||||
- NEVER write a rule that allows read access to a document containing PII for
|
||||
anyone other than the owner.
|
||||
|
||||
#### **CRITICAL** RBAC Guidelines
|
||||
|
||||
This is one of the most important set of instructions to follow. Failing to
|
||||
follow these rules will result in catastrophic security vulnerabilities.
|
||||
|
||||
- **NEVER** allow users to create their own privileged roles. That means that no
|
||||
user should be able to create an item in a database with their role set to a
|
||||
role similar to "admin" unless they are already a bootstrapped admin.
|
||||
- **NEVER** allow users to update their own roles or permissions.
|
||||
- **NEVER** allow users to grant themselves access to other users' data.
|
||||
- **NEVER** allow users to bypass the role hierarchy.
|
||||
- **ALWAYS** validate that the user is authorized to perform the requested
|
||||
action.
|
||||
- **ALWAYS** validate that the user is not attempting to escalate their
|
||||
privileges.
|
||||
- **ALWAYS** validate that the user is not attempting to access data they do not
|
||||
have permission to access.
|
||||
|
||||
Here's a **bad** example of what **NOT** to do:
|
||||
|
||||
```javascript
|
||||
match /users/{userId} {
|
||||
// BAD: Allows users to create their own roles because a user can create a new user document with a role of 'admin' and the isAdmin() function will return true
|
||||
allow create: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin();
|
||||
// BAD: Allows users to update their own roles because a user can update their own user document with a role of 'admin' and the isAdmin() function will return true
|
||||
allow update: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin();
|
||||
}
|
||||
```
|
||||
|
||||
Here's a **good** example of what **TO** do:
|
||||
|
||||
```javascript
|
||||
match /users/{userId} {
|
||||
// GOOD: Does NOT allow users to create their own roles unless they are an admin or the user is updating their own role to a less privileged role
|
||||
allow create: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == 'client') || isAdmin());
|
||||
// GOOD: Does NOT allow users to update their own roles unless they are an admin
|
||||
allow update: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == resource.data.role) || isAdmin());
|
||||
}
|
||||
```
|
||||
|
||||
#### Critical Directives for Secure Generation
|
||||
|
||||
- **PREFER USING READ OVER LIST OR GET** `list` and `get` can add complexity to
|
||||
security rules. Prefer using `read` over them.
|
||||
|
||||
- **Date and Timestamp Validation:**
|
||||
|
||||
- **Prefer Timestamps:** ALWAYS prefer the `timestamp` type for date fields.
|
||||
Firestore automatically ensures they are logically valid dates.
|
||||
- **String Date Risks:** If using strings for dates (e.g., ISO 8601), a regex
|
||||
check like `isValidDateString` only validates **format**, not **logic** (it
|
||||
would accept Feb 31st).
|
||||
- **Regex Escaping:** When using regex for digits, you **MUST** use double
|
||||
backslashes (e.g., `\\\\d`) in the rules string. Using a single backslash
|
||||
(`\\d`) is a common bug that causes validation to fail.
|
||||
|
||||
- **Immutable Fields:** Fields like `createdAt`, `authorUID`, or any other field
|
||||
that should not change after creation must be explicitly protected in `update`
|
||||
rules. (e.g., `request.resource.data.createdAt == resource.data.createdAt`).
|
||||
**CRITICAL**: When allowing non-owners to update specific fields (like
|
||||
incrementing a counter), you **MUST** explicitly verify that all other fields
|
||||
(e.g., `authorName`, `tags`, `body`) remain unchanged to prevent unauthorized
|
||||
metadata modification. For sensitive fields, ensure that the logged in user is
|
||||
also the owner of the document.
|
||||
|
||||
- **Identity Integrity:** When storing denormalized user identity (e.g.
|
||||
`authorName`, `authorPhoto`), you **MUST** validate this data.
|
||||
|
||||
- **Prefer Auth Token:** If possible, check if
|
||||
`request.resource.data.authorName == request.auth.token.name`.
|
||||
- **Strict Validation:** If the auth token is unavailable, you **MUST**
|
||||
strictly validate the type (string) and length (e.g. < 50 chars) to prevent
|
||||
spoofing with massive or malicious payloads.
|
||||
- **Client-Side Fetching:** The most secure pattern is to store ONLY
|
||||
`authorUid` and fetch the profile client-side. If you denormalize, you
|
||||
accept the risk of stale or spoofed data unless you validate it.
|
||||
|
||||
- **Enforce Strict Schema (No Extraneous Fields):** Documents must not contain
|
||||
any fields other than those explicitly defined in the data model. This
|
||||
prevents users from adding arbitrary data.
|
||||
|
||||
- **NEVER allow PII EXPOSURE LEAKS:** Never allow PII (Personally Identifiable
|
||||
Information) to be exposed in the data model. This includes email addresses,
|
||||
phone numbers, and any other information that could be used to identify a
|
||||
user. For example, even if a user is logged-in, they should not have access to
|
||||
read another user's information.
|
||||
|
||||
- **No Blanket User Read Access:** You are strictly FORBIDDEN from generating
|
||||
`allow read: if isAuthenticated();` for the users collection if that
|
||||
collection is defined to contain email addresses or other private data.
|
||||
|
||||
- **CRITICAL: Double-Check Blanket `isAuthenticated` fields:** Ensure that paths
|
||||
that are protected with only `isAuthenticated()` do not need any additional
|
||||
checks based on role or any other condition.
|
||||
|
||||
- **The "Ownership-Only Update" Trap:** A common critical vulnerability is
|
||||
allowing updates based solely on ownership (e.g.,
|
||||
`allow update: if isOwner(resource.data.uid);`). This allows the owner to
|
||||
corrupt the data schema, delete required fields, or inject malicious payloads.
|
||||
You **MUST** always combine ownership checks with data validation (e.g.,
|
||||
`allow update: if isOwner(...) && isValidEntity(...);`) **AND** validate that
|
||||
self-escalation is not possible.
|
||||
|
||||
- **Deep Array Inspection:** It is insufficient to check if a field `is list`.
|
||||
You **MUST** validate the contents of the array (e.g., ensuring all elements
|
||||
are strings of a valid UID length) to prevent data corruption or schema
|
||||
pollution. For example, a `tags` array must verify that every item is a string
|
||||
AND that each string is within a reasonable length (e.g., < 20 chars).
|
||||
|
||||
- **Permission-Field Lockdown:** Fields that control access (e.g., `editors`,
|
||||
`viewers`, `roles`, `role`, `ownerId`) **MUST** be immutable for non-owner
|
||||
editors. In `update` rules, use `fieldUnchanged()` for these fields unless the
|
||||
`request.auth.uid` matches the document's original owner/creator. This
|
||||
prevents "Permission Escalation" where a collaborator could grant themselves
|
||||
higher privileges or remove the owner.
|
||||
|
||||
### Advanced Validation for Business Logic
|
||||
|
||||
Secure rules must enforce the application's business logic. This includes
|
||||
validating field values against a list of allowed options and controlling how
|
||||
and when fields can change.
|
||||
|
||||
\#### 1. Enforce Enum Values
|
||||
|
||||
If a field should only contain specific values (e.g., a status), validate
|
||||
against a list.
|
||||
|
||||
**Example:**
|
||||
|
||||
```javascript
|
||||
// A 'task' document's status can only be one of three values
|
||||
function isValidStatus() {
|
||||
let validStatuses = ['pending', 'in-progress', 'completed'];
|
||||
return request.resource.data.status in validStatuses;
|
||||
}
|
||||
|
||||
allow create: if isValidStatus() && ...
|
||||
```
|
||||
|
||||
\#### 2. Validate State Transitions
|
||||
|
||||
For `update` operations, you **MUST** validate that a field is changing from a
|
||||
valid previous state to a valid new state. This prevents users from bypassing
|
||||
workflows (e.g., marking a task as 'completed' from 'archived').
|
||||
|
||||
**Example:**
|
||||
|
||||
```javascript
|
||||
// A task can only be marked 'completed' if it was 'in-progress'
|
||||
function validStatusTransition() {
|
||||
let previousStatus = resource.data.status;
|
||||
let newStatus = request.resource.data.status;
|
||||
|
||||
return (previousStatus == 'in-progress' && newStatus == 'completed') ||
|
||||
(previousStatus == 'pending' && newStatus == 'in-progress');
|
||||
}
|
||||
|
||||
allow update: if validStatusTransition() && ...
|
||||
```
|
||||
|
||||
#### 3. Strict Path and Relationship Scoping
|
||||
|
||||
For any field that references another resource (like an image path or a parent
|
||||
document ID), you **MUST** ensure it is correctly scoped to the user or valid
|
||||
within the context.
|
||||
|
||||
**Example:**
|
||||
|
||||
```javascript
|
||||
// Ensure image path is within the user's own storage folder
|
||||
allow create: if isScopedPath(request.resource.data.imageBucket) && ...
|
||||
```
|
||||
|
||||
#### 4. Secure Counter Updates
|
||||
|
||||
When allowing users to update a counter (like `voteCount` or `answerCount`), you
|
||||
**MUST** ensure: 1. **Atomic Increments:** The field is only changing by exactly
|
||||
+1 or -1. 2. **Isolation:** **NO OTHER FIELDS** are being modified. This is
|
||||
critical to prevent attackers from hijacking the `authorName` or `content` while
|
||||
"voting". 3. **Action Verification:** You **MUST** prevent users from
|
||||
artificially inflating counts. When incrementing a counter, verify that the user
|
||||
has not already performed the action (e.g., by checking for the existence of a
|
||||
'like' document) and is not looping updates. * **CRITICAL:** Relying solely on
|
||||
`!exists(likeDoc)` is insufficient because a malicious user can skip creating
|
||||
the document and loop the increment. * **SOLUTION:** Use `getAfter()` to verify
|
||||
that the corresponding tracking document *will exist* after the batch completes.
|
||||
|
||||
**Example:**
|
||||
|
||||
```javascript
|
||||
function isValidCounterUpdate(docId) {
|
||||
// Allow update only if 'voteCount' is the ONLY field changing
|
||||
return request.resource.data.diff(resource.data).affectedKeys().hasOnly(['voteCount']) &&
|
||||
// And the change is exactly +1 or -1
|
||||
math.abs(request.resource.data.voteCount - resource.data.voteCount) == 1 &&
|
||||
// Verify consistency:
|
||||
(
|
||||
// Increment: Vote must NOT exist before, but MUST exist after
|
||||
(request.resource.data.voteCount > resource.data.voteCount &&
|
||||
!exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) &&
|
||||
getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) != null) ||
|
||||
// Decrement: Vote MUST exist before, but must NOT exist after
|
||||
(request.resource.data.voteCount < resource.data.voteCount &&
|
||||
exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) &&
|
||||
getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) == null)
|
||||
);
|
||||
}
|
||||
|
||||
allow update: if isValidCounterUpdate(docId) && ...
|
||||
```
|
||||
|
||||
#### 5. **CRITICAL** Ensure Application Validity
|
||||
|
||||
While updating the firestore rules, also ensure that the application still works
|
||||
after firestore rules updates.
|
||||
|
||||
1. **For each collection, implement explicit data validation:**
|
||||
|
||||
- Type Checking: 'field is string', 'field is number', 'field is bool', 'field
|
||||
is timestamp'
|
||||
- Required fields validation using 'hasRequiredFields()'
|
||||
- **Enforce Size Limits:** For **EVERY** string, list, and map field, you
|
||||
**MUST** enforce realistic size limits (e.g., `text.size() < 1000`,
|
||||
`tags.size() < 20`). **Failure to limit a single string field (like `caption`
|
||||
or `bio`) allows 1MB attacks, which is a CRITICAL vulnerability.**
|
||||
- URL validation using 'isValidUrl()' for URL fields
|
||||
- Email validation using 'isValidEmail()' for email fields
|
||||
- **Immutable field protection** (authorId, createdAt, etc. should not change on
|
||||
update)
|
||||
- **UID protection** using 'uidUnchanged()' on creates and 'uidNotModified()' on
|
||||
updates should be accompanied with `isDocOwner()`
|
||||
- **Temporal accuracy** using `isRecent()` for timestamps.
|
||||
- **Range validation** using `isPositive()` or similar for numbers.
|
||||
- **Path scoping** using `isScopedPath()` for storage paths.
|
||||
|
||||
Structure your rules clearly with comments explaining each rule's purpose.
|
||||
|
||||
#### Phase-3: Devil's Advocate Attack
|
||||
|
||||
**Critical step:** Systematically attempt to break your own rules using the
|
||||
following attack vectors. You MUST document the outcome of each attempt.
|
||||
|
||||
1. **Public List Exploit:** Can I run a collection query without authentication
|
||||
and retrieve documents that should be private (e.g., where
|
||||
`visible == false`)?
|
||||
1. **Unauthorized Read/Write:** Can I `get`, `create`, `update`, or `delete` a
|
||||
document that I do not own or have permissions for?
|
||||
1. **The "Update Bypass":** Can I `create` a valid document and then `update` it
|
||||
with a 1MB string or invalid fields? (Tests if validation logic is missing
|
||||
from `update`).
|
||||
1. **Ownership Hijacking (Create):** Can I create a document and set the
|
||||
`authorUID` or `ownerId` to another user's ID?
|
||||
1. **Ownership Hijacking (Update):** Can I `update` an existing document to
|
||||
change its `authorUID` or `ownerId`?
|
||||
1. **Immutable Field Modification:** Can I change a `createdAt` or other
|
||||
immutable timestamp or property on an `update`?
|
||||
1. **Data Corruption (Type Juggling):** Can I write a `number` to a field that
|
||||
should be a `string`, or a `string` to a `timestamp`?
|
||||
1. **Validation Bypass (Create vs. Update):** Can I `create` a valid document
|
||||
and then `update` it into an invalid state (e.g., remove a required field,
|
||||
write a string that's too long)?
|
||||
1. **Resource Exhaustion / DoS:** Can I write an enormous string (e.g., 1MB) to
|
||||
any field that accepts a string or a massive array to a list field? Every
|
||||
string field (e.g., `bio`, `url`, `name`) MUST have a `.size()` check. If any
|
||||
are missing, it's a "Resource Exhaustion/DoS" risk.
|
||||
1. **Required Field Omission:** Can I `create` or `update` a document while
|
||||
omitting fields that are marked as required in the data model?
|
||||
1. **Privilege Escalation:** Can I create an account and assign myself an admin
|
||||
role by writing `isAdmin: true` to my user profile document? (Tests reliance
|
||||
on document data vs. custom claims).
|
||||
1. **Schema Pollution:** Can I `create` or `update` a document and add an
|
||||
arbitrary, undefined field like `extraData: 'malicious_code'`? (Tests for
|
||||
strict schema enforcement).
|
||||
1. **Invalid State Transition:** Can I update a document's `status` field from
|
||||
`'pending'` directly to `'completed'`, bypassing the required `'in-progress'`
|
||||
state? (Tests business logic enforcement).
|
||||
1. **Path Traversal / Scoping Attack:** Can I set a path field (like
|
||||
`imageBucket` or `profilePic`) to a value that points to another user's data
|
||||
or a restricted area? (Tests for regex path scoping).
|
||||
1. **Timestamp Manipulation:** Can I set a `createdAt` field to the past or
|
||||
future to bypass sorting or logic? (Tests for `request.time` validation).
|
||||
1. **Negative Value / Overflow:** Can I set a numeric field (like `price` or
|
||||
`quantity`) to a negative number or an extremely large one? (Tests for range
|
||||
validation).
|
||||
1. **The "Mixed Content" Leak:** Create a second user. Can User B read User A's
|
||||
users document? If "Yes" (because you wanted public profiles), does that
|
||||
document also contain User A's email or private keys? If both are true, the
|
||||
rules are insecure.
|
||||
1. **Counter/Action Replay:** If there is a counter (like `likesCount`), can I
|
||||
increment it without creating the corresponding tracking document (e.g.,
|
||||
inside `likes/{userId}`)? Can I increment it twice? (Tests for `getAfter()`
|
||||
consistency checks).
|
||||
1. **Orphaned Subcollection Access:** Can I read/write to a subcollection (e.g.,
|
||||
`users/123/posts/456`) if the parent document (`users/123`) does not exist?
|
||||
(Tests for parent existence checks).
|
||||
1. **Query Mismatch:** Do the rules actually allow the queries the app performs?
|
||||
(e.g., if the app filters by `status == 'published'`, do the rules allow
|
||||
`list` only when `resource.data.status == 'published'`?)
|
||||
1. **Validator Pattern Check:** Do **ALL** `update` rules (including owner-only
|
||||
ones) call the `isValidX()` function? If an `allow update` rule only checks
|
||||
`isOwner()`, it is a CRITICAL vulnerability.
|
||||
|
||||
Document each attack attempt and whether it succeeded. If ANY attack succeeds:
|
||||
|
||||
- Fix the security hole
|
||||
- Regenerate the rules
|
||||
- **Repeat Phase-3** until no attacks succeed
|
||||
|
||||
#### Phase-4: Syntactic Validation
|
||||
|
||||
Once devil's advocate testing passes, repeat until rules pass validation.
|
||||
|
||||
**After all phases are complete, create or update the `firestore.rules` file.**
|
||||
|
||||
### Critical Constraints
|
||||
|
||||
1. **Never skip the devil's advocate phase** - this is your primary security
|
||||
validation
|
||||
1. **MUST include helper functions** for common operations ('isAuthenticated',
|
||||
'isOwner', 'uidUnchanged', 'uidNotModified') AND domain validators
|
||||
('isValidUser', etc.)
|
||||
1. **MUST document assumed data models** at the beginning of the rules file
|
||||
1. **Always validate the rules syntax** using 'firebase deploy --only
|
||||
firestore:rules --dry-run' or a similar tool before outputting the final
|
||||
file.
|
||||
1. **Provide complete, runnable code** - no placeholders or TODOs
|
||||
1. **Document all assumptions** about data structure or access patterns
|
||||
1. **Always run the devil's advocate attack** after any modification of the
|
||||
rules.
|
||||
1. **Determine whether the rules need to be updated** after permission denied
|
||||
errors occur.
|
||||
1. **Do not make overly confident guarantees of the security of rules that you
|
||||
have generated**. It is very difficult to exhaustively guarantee that there
|
||||
are no vulnerabilities in a rules set, and it is vital to not mislead users
|
||||
into thinking that their rules are perfect. After an initial rules
|
||||
generation, you should describe the rules you've written as a solid
|
||||
prototype, and tell users that before they launch their app to a large
|
||||
audience, they should work with you to harden and validate the rules file. Be
|
||||
clear that users should carefully review rules to ensure security.
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
# 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 'npx -y firebase-tools@latest apps:sdkconfig <platform> <app-id>'
|
||||
};
|
||||
|
||||
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));
|
||||
```
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
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
|
||||
npx -y firebase-tools@latest emulators:start --only hosting
|
||||
```
|
||||
|
||||
This serves your app at `http://localhost:5000` by default.
|
||||
|
||||
<!-- SELF-EVOLVE:START -->
|
||||
## Self-Evolution & Proactivity (injected)
|
||||
|
||||
Zone of Initiative — calibrated autonomy:
|
||||
- Low stakes + reversible: ACT (e.g. draft a file, set a var).
|
||||
- High stakes + reversible: ACT, then report immediately.
|
||||
- High stakes + irreversible: STOP and ASK via `clarify`.
|
||||
- Reversibility is the throttle, not stakes alone.
|
||||
|
||||
Three loops keep skills alive:
|
||||
- Capture: after a task of 5+ tool calls or a reusable discovery, write/refine a skill.
|
||||
- Heal: after a failure or correction that reflects a reusable procedure, patch the skill that caused it (remove the old wording, don't layer advice).
|
||||
- Decay: on a schedule, prune duplicated/stale/no-op skills so they get sharper, not longer.
|
||||
|
||||
Memory vs Skill: a FACT (path, name, API quirk, repo URL) -> memory. a PROCEDURE (how to do X) -> skill. One-off preference -> memory/inline. Reusable correction -> skill patch.
|
||||
|
||||
For opencode (code/devops/file-ops):
|
||||
- Capture: a build/test fix pattern, a refactor recipe, or an infra quirk -> skill.
|
||||
- Heal: a bad refactor or failed deploy -> patch the responsible skill, then re-run to confirm.
|
||||
- Decay: stale scripts, dead flags, duplicated helpers. Install dep / run safe refactor = reversible; force-push / drop DB / prod deploy = ask.
|
||||
- Route research to Gemini, memory/channels to Hermes.
|
||||
|
||||
Goal: become faster, smarter, more efficient, more knowledgeable, more thoughtful, and proactively autonomous — never less safe.
|
||||
A high-stakes irreversible action (delete data, ship to prod, send a message on the user's behalf) always goes through `clarify`. Reversible, low-stakes actions may proceed, then be reported in the same turn.
|
||||
<!-- SELF-EVOLVE:END -->
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Learnings
|
||||
|
||||
## Initial Setup
|
||||
- Skill created with standard structure
|
||||
|
||||
## 2026-06-28
|
||||
- opencode timed out repeatedly on this skill (non-code task) — route to hermes
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
# 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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# Deploying to Firebase Hosting
|
||||
|
||||
## Standard Deployment
|
||||
|
||||
To deploy your Hosting content and configuration to your live site:
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest 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
|
||||
npx -y firebase-tools@latest hosting:channel:deploy CHANNEL_ID
|
||||
```
|
||||
|
||||
Replace `CHANNEL_ID` with a name (e.g., `feature-beta`). This returns a preview
|
||||
URL like `PROJECT_ID--CHANNEL_ID-RANDOM_HASH.web.app`.
|
||||
|
||||
### Expiration
|
||||
|
||||
Channels expire after 7 days by default. To set a different expiration:
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest hosting:channel:deploy CHANNEL_ID --expires 1d
|
||||
```
|
||||
|
||||
## Cloning to Live
|
||||
|
||||
You can promote a version from a preview channel to your live channel without
|
||||
rebuilding.
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest hosting:clone SOURCE_SITE_ID:SOURCE_CHANNEL_ID TARGET_SITE_ID:live
|
||||
```
|
||||
|
||||
**Example:** Clone the `feature-beta` channel on your default site to live:
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest hosting:clone my-project:feature-beta my-project:live
|
||||
```
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
---
|
||||
name: firebase-remote-config-basics
|
||||
description: Comprehensive guide for Firebase Remote Config, including template management and SDK usage. Use this skill when the user needs help setting up Remote Config, managing feature flags, or updating app behavior dynamically.
|
||||
compatibility: This skill is best used with the Firebase CLI, but does not require it. Firebase CLI can be accessed through `npx -y firebase-tools@latest`.
|
||||
---
|
||||
|
||||
# Remote Config
|
||||
|
||||
This skill provides a complete guide for getting started with Remote Config on
|
||||
Android or iOS. Remote Config allows you to change the behavior and appearance
|
||||
of your app without publishing an app update by maintaining a cloud-based
|
||||
configuration template.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Provisioning Remote Config requires both a Firebase project and a Firebase app,
|
||||
either Android or iOS. To manage the Remote Config template and conditions via
|
||||
the command line, use the Firebase CLI. See the `firebase-basics` skill for
|
||||
references on project initialization.
|
||||
|
||||
## Troubleshooting Execution
|
||||
|
||||
### Handling npx 403 Forbidden Errors
|
||||
|
||||
If `npx -y firebase-tools@latest` fails due to registry permissions (403 error):
|
||||
|
||||
1. **Inform the user**: "I am unable to fetch the latest Firebase tools via npx
|
||||
due to a registry error."
|
||||
1. **Fallback**: Attempt to use the local `firebase` command directly if the
|
||||
user confirms it is installed globally (`npm install -g firebase-tools`).
|
||||
|
||||
### Handling Project Context Issues
|
||||
|
||||
If a command fails because "no active project is selected":
|
||||
|
||||
1. **Check login**: Run `npx -y firebase-tools@latest login:list`.
|
||||
1. **Prompt for ID**: If logged in but no project is active, ask the user:
|
||||
"Please provide your Firebase Project ID to proceed."
|
||||
1. **Use Flag**: Append `--project <PROJECT_ID>` to every subsequent command.
|
||||
|
||||
## SDK Setup
|
||||
|
||||
To learn how to set up Remote Config in your application code, choose your
|
||||
platform:
|
||||
|
||||
- **Android**: [android_setup.md](references/android_setup.md)
|
||||
- **iOS**: [ios_setup.md](references/ios_setup.md)
|
||||
|
||||
## Best Practices and Template Management
|
||||
|
||||
Follow these guidelines and use the associated CLI tools to ensure efficient and
|
||||
safe use of Remote Config.
|
||||
|
||||
### Fetching Strategies
|
||||
|
||||
To optimize app performance and user experience, follow these recommended
|
||||
patterns (see
|
||||
[Loading Strategies](https://firebase.google.com/docs/remote-config/loading)):
|
||||
|
||||
- **Load new values for next startup**: The most effective pattern is to
|
||||
activate previously fetched values immediately on startup and fetch new values
|
||||
in the background to be used next time. This minimizes user wait time.
|
||||
- **Real-time Updates**: Use the SDK's real-time listener to update the app
|
||||
instantly without a refresh when server-side configuration changes.
|
||||
|
||||
### Template Management via CLI
|
||||
|
||||
Use the following commands to manage your Remote Config template and version
|
||||
history through the terminal:
|
||||
|
||||
### Template Management via CLI
|
||||
|
||||
Use the following commands to manage your Remote Config template and version
|
||||
history through the terminal:
|
||||
|
||||
- **Get current template**: Save the remote template to a local JSON file for
|
||||
auditing or modification.
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest remoteconfig:get -o remote_config.json
|
||||
```
|
||||
|
||||
- **Autonomous Editing & Discovery** : Modify the local `remote_config.json`
|
||||
directly. Determine the correct signal (e.g., device.country or percent) and
|
||||
update the "conditions" array and "parameters" map accordingly.
|
||||
|
||||
- **MANDATORY: User Review and Verification** : STOP and ask the user to verify
|
||||
your changes before proceeding to deployment.
|
||||
|
||||
- Action: Inform the user: "I have prepared the changes in remote_config.json.
|
||||
Please review the file for accuracy. Once you are satisfied, tell me to
|
||||
'deploy' to make the changes live."
|
||||
|
||||
- **Deployment Orchestration** : To push changes, you must ensure the
|
||||
environment is configured for deployment.
|
||||
|
||||
- Config Mapping: If a firebase.json file is missing, create one to map the
|
||||
local JSON to the Remote Config service:
|
||||
|
||||
```json
|
||||
{ "remoteconfig": { "template": "remote_config.json" } }
|
||||
```
|
||||
|
||||
- Deploy: Execute the partial deployment command
|
||||
```bash
|
||||
npx -y firebase-tools@latest deploy --only remoteconfig
|
||||
```
|
||||
|
||||
- **Verification**: After deployment, verify the update by listing the version
|
||||
history.
|
||||
|
||||
```bash
|
||||
npx -y firebase-tools@latest remoteconfig:versions:list
|
||||
```
|
||||
|
||||
The SDK provides a number of features to make your application dynamic and
|
||||
responsive to user segments.
|
||||
|
||||
- **Set In-App Defaults**: Define baseline values to ensure the app functions
|
||||
offline or before the first fetch.
|
||||
- **Fetch and Activate**: Retrieve values from the Firebase backend and apply
|
||||
them to the local UI/Logic.
|
||||
- **Template Management**: Use the Firebase CLI to version-control, get, and
|
||||
deploy your config JSON files.
|
||||
|
||||
<!-- SELF-EVOLVE:START -->
|
||||
## Self-Evolution & Proactivity (injected)
|
||||
|
||||
Zone of Initiative — calibrated autonomy:
|
||||
- Low stakes + reversible: ACT (e.g. draft a file, set a var).
|
||||
- High stakes + reversible: ACT, then report immediately.
|
||||
- High stakes + irreversible: STOP and ASK via `clarify`.
|
||||
- Reversibility is the throttle, not stakes alone.
|
||||
|
||||
Three loops keep skills alive:
|
||||
- Capture: after a task of 5+ tool calls or a reusable discovery, write/refine a skill.
|
||||
- Heal: after a failure or correction that reflects a reusable procedure, patch the skill that caused it (remove the old wording, don't layer advice).
|
||||
- Decay: on a schedule, prune duplicated/stale/no-op skills so they get sharper, not longer.
|
||||
|
||||
Memory vs Skill: a FACT (path, name, API quirk, repo URL) -> memory. a PROCEDURE (how to do X) -> skill. One-off preference -> memory/inline. Reusable correction -> skill patch.
|
||||
|
||||
For opencode (code/devops/file-ops):
|
||||
- Capture: a build/test fix pattern, a refactor recipe, or an infra quirk -> skill.
|
||||
- Heal: a bad refactor or failed deploy -> patch the responsible skill, then re-run to confirm.
|
||||
- Decay: stale scripts, dead flags, duplicated helpers. Install dep / run safe refactor = reversible; force-push / drop DB / prod deploy = ask.
|
||||
- Route research to Gemini, memory/channels to Hermes.
|
||||
|
||||
Goal: become faster, smarter, more efficient, more knowledgeable, more thoughtful, and proactively autonomous — never less safe.
|
||||
A high-stakes irreversible action (delete data, ship to prod, send a message on the user's behalf) always goes through `clarify`. Reversible, low-stakes actions may proceed, then be reported in the same turn.
|
||||
<!-- SELF-EVOLVE:END -->
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
# Firebase Remote Config Android Setup Guide
|
||||
|
||||
Important references:
|
||||
|
||||
- Refer to the `firebase-basics` skills, particularly those for project and app
|
||||
setup, before proceeding.
|
||||
|
||||
## Project and App Setup
|
||||
|
||||
Before you begin, ensure you have the following. If a `google-services.json`
|
||||
file is present, then use that Firebase project and app. Otherwise you may need
|
||||
to create them.
|
||||
|
||||
- **Firebase CLI**: Installed and logged in (see `firebase-basics`).
|
||||
- **Firebase Project**: Created via
|
||||
`npx -y firebase-tools@latest projects:create` (see `firebase-basics`).
|
||||
- **Firebase App**: Created via
|
||||
`npx -y firebase-tools@latest apps:create <IOS|ANDROID|WEB> <package-name-or-bundle-id>`
|
||||
|
||||
The `google-services.json` file must be present in the Android app's module
|
||||
directory. If missing, get the config using the Firebase CLI:
|
||||
`npx -y firebase-tools@latest apps:sdkconfig ANDROID <App-ID>`.
|
||||
|
||||
## Add Dependencies to Gradle Build
|
||||
|
||||
These changes are made to your Android project's Gradle files. Google Analytics
|
||||
is highly recommended as it enables conditional targeting based on user
|
||||
properties and audiences.
|
||||
|
||||
### Project-level `build.gradle.kts` (`<project>/build.gradle.kts`)
|
||||
|
||||
Ensure the Google Services plugin is in the `plugins` block:
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
// ... other plugins
|
||||
id("com.google.gms.google-services") version "4.4.0" apply false
|
||||
}
|
||||
```
|
||||
|
||||
### App-level `build.gradle.kts` (`<project>/<app-module>/build.gradle.kts`)
|
||||
|
||||
1. Add the Google Services plugin to the `plugins` block:
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
// ... other plugins
|
||||
id("com.google.gms.google-services")
|
||||
}
|
||||
```
|
||||
|
||||
1. Add the Firebase Remote Config and Analytics dependencies. Using the Firebase
|
||||
Bill of Materials (BoM) is the best practice for version management.
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
// ... other dependencies
|
||||
|
||||
// Import the Firebase BoM
|
||||
implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
|
||||
|
||||
// Add the dependencies for Remote Config and Analytics
|
||||
implementation("com.google.firebase:firebase-config-ktx")
|
||||
implementation("com.google.firebase:firebase-analytics-ktx")
|
||||
}
|
||||
```
|
||||
|
||||
## Follow up Steps
|
||||
|
||||
The following steps cover the essential patterns for using Remote Config
|
||||
effectively.
|
||||
|
||||
### Set In-App Defaults
|
||||
|
||||
Define default values so your app has functional logic before it ever fetches a
|
||||
template from the server. Create an XML file (e.g.,
|
||||
`res/xml/remote_config_defaults.xml`):
|
||||
`xml <!-- Example Remote Config Defaults File --> <?xml version="1.0" encoding="utf-8"?> <defaultsMap> <entry> <key>welcome_message</key> <value>Welcome to the app!</value> </entry> <entry> <key>is_feature_enabled</key> <value>false</value> </entry> </defaultsMap> `
|
||||
Then, initialize the SDK in your Activity or Application class:
|
||||
|
||||
````
|
||||
```kotlin
|
||||
val remoteConfig = Firebase.remoteConfig
|
||||
remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
|
||||
```
|
||||
````
|
||||
|
||||
### Fetch and Activate Values
|
||||
|
||||
To apply values from the cloud, you must fetch them and then activate them.
|
||||
`kotlin remoteConfig.fetchAndActivate() .addOnCompleteListener(this) { task -> if (task.isSuccessful) { val updated = task.result println("Config params updated: $updated") } else { println("Fetch failed") } // Access a value val message = remoteConfig.getString("welcome_message") } `
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
# Firebase Remote Config iOS Setup Guide
|
||||
|
||||
Important references:
|
||||
|
||||
- Refer to the `firebase-basics` skills, particularly those for iOS setup,
|
||||
before proceeding.
|
||||
- Refer to the `xcode-project-setup` skills.
|
||||
|
||||
## Project and App Setup
|
||||
|
||||
Use the `firebase-tools` CLI to set up the project if necessary.
|
||||
|
||||
1. **Find Bundle ID:** Read the Xcode project to find the iOS bundle ID. Check
|
||||
the `PRODUCT_BUNDLE_IDENTIFIER` value in the `.pbxproj` file or the
|
||||
`Info.plist` file.
|
||||
1. **Create Firebase Project:** If no project exists, create one:
|
||||
`npx -y firebase-tools@latest projects:create <project-id> --display-name="My Awesome App"`
|
||||
1. **Create Firebase App:** Register the iOS app with the discovered bundle ID:
|
||||
`npx -y firebase-tools@latest apps:create IOS <bundle-id>`
|
||||
1. **Link the GoogleService-Info.plist file:** Use the script in the
|
||||
`xcode-project-setup` skill to obtain the config and link.
|
||||
|
||||
## Add Swift Package Dependencies
|
||||
|
||||
Install the Remote Config and Analytics SDKs using the Swift package manager.
|
||||
|
||||
Install the `FirebaseRemoteConfig` and `FirebaseAnalytics` packages from the
|
||||
[https://github.com/firebase/firebase-ios-sdk.git](https://github.com/firebase/firebase-ios-sdk.git)
|
||||
repository.
|
||||
|
||||
## Initialize Firebase in App Code
|
||||
|
||||
Modify the application's entry point to initialize Firebase. Refer to the iOS
|
||||
setup reference in the firebase-basics skill.
|
||||
|
||||
## Follow up Steps
|
||||
|
||||
The following steps cover the essential patterns for using Remote Config
|
||||
effectively in your iOS app.
|
||||
|
||||
### Set In-App Defaults
|
||||
|
||||
Define default values so your app behaves as intended before it connects to the
|
||||
backend. Create a property list file (e.g., RemoteConfigDefaults.plist):
|
||||
|
||||
````
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>welcome_message</key>
|
||||
<string>Welcome to the app!</string>
|
||||
<key>is_feature_enabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
````
|
||||
|
||||
Then, initialize the SDK and set the defaults:
|
||||
|
||||
````
|
||||
```swift
|
||||
import FirebaseRemoteConfig
|
||||
|
||||
let remoteConfig = RemoteConfig.remoteConfig()
|
||||
remoteConfig.setDefaults(fromPlist: "RemoteConfigDefaults")
|
||||
```
|
||||
````
|
||||
|
||||
### Fetch and Activate Values
|
||||
|
||||
To retrieve values from the cloud and apply them to your app:
|
||||
|
||||
````
|
||||
```swift
|
||||
remoteConfig.fetchAndActivate { (status, error) in
|
||||
if status == .successFetchedFromRemote || status == .successUsingPreFetchedData {
|
||||
print("Config fetched and activated!")
|
||||
} else {
|
||||
print("Config not fetched")
|
||||
}
|
||||
|
||||
// Access a value
|
||||
let message = remoteConfig.configValue(forKey: "welcome_message").stringValue
|
||||
}
|
||||
```
|
||||
````
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
name: firebase-security-rules-auditor
|
||||
description: A skill to evaluate how secure Firestore security rules are. Use this when Firestore security rules are updated to ensure that the generated rules are extremely secure and robust.
|
||||
---
|
||||
|
||||
# Overview
|
||||
|
||||
This skill acts as an auditor for Firebase Security Rules, evaluating them
|
||||
against a rigorous set of criteria to ensure they are secure, robust, and
|
||||
correctly implemented.
|
||||
|
||||
# Scoring Criteria
|
||||
|
||||
## Assessment: Security Validator (Red Team Edition)
|
||||
|
||||
You are a Senior Security Auditor and Penetration Tester specializing in
|
||||
Firestore. Your goal is to find "the hole in the wall." Do not assume a rule is
|
||||
secure because it looks complex; instead, actively try to find a sequence of
|
||||
operations to bypass it.
|
||||
|
||||
### Mandatory Audit Checklist:
|
||||
|
||||
1. **The Update Bypass:** Compare 'create' and 'update' rules. Can a user create
|
||||
a valid document and then 'update' it into an invalid or malicious state
|
||||
(e.g., changing their role, bypassing size limits, or corrupting data types)?
|
||||
1. **Authority Source:** Does the security rely on user-provided data
|
||||
(request.resource.data) for sensitive fields like 'role', 'isAdmin', or
|
||||
'ownerId'? Carefully consider the source for that authority.
|
||||
1. **Business Logic vs. Rules:** Does the rule set actually support the app's
|
||||
purpose? (e.g., In a collaboration app, can collaborators actually read the
|
||||
data? If not, the rules are "broken" or will force insecure workarounds).
|
||||
1. **Storage Abuse:** Are there string length or array size limits? If not,
|
||||
label it as a "Resource Exhaustion/DoS" risk.
|
||||
1. **Type Safety:** Are fields checked with 'is string', 'is int', or 'is
|
||||
timestamp'?
|
||||
1. **Field-Level vs. Identity-Level Security:** Be careful with rules that use
|
||||
\`hasOnly()\` or \`diff()\`. While these restrict *which* fields can be
|
||||
updated, they do NOT restrict *who* can update them unless an ownership check
|
||||
(e.g., \`resource.data.uid == request.auth.uid\`) is also present. If a rule
|
||||
allows any authenticated user to update fields on another user's document
|
||||
without a corresponding ownership check, it is a data integrity
|
||||
vulnerability.
|
||||
|
||||
### Admin Bootstrapping & Privileges:
|
||||
|
||||
The admin bootstrapping process is limited in this app. If the rules use a
|
||||
single hardcoded admin email (e.g., checking request.auth.token.email ==
|
||||
'admin@example.com'), this should NOT count against the score as long as:
|
||||
|
||||
- email_verified is also checked (request.auth.token.email_verified == true).
|
||||
- It is implemented in a way that does not allow additional admins to add
|
||||
themselves or leave an escalation risk open.
|
||||
|
||||
### Scoring Criteria (1-5):
|
||||
|
||||
- **1 (Critical):** Unauthorized data access (leaks), privilege escalation, or
|
||||
total validation bypass.
|
||||
- **2 (Major):** Broken business logic, self-assigned roles, bypass of controls.
|
||||
- **3 (Moderate):** PII exposure (e.g., public emails), Inconsistent validation
|
||||
(create vs update) on critical fields
|
||||
- **4 (Minor):** Problems that result in self-data corruption like update
|
||||
bypasses that only impact the user's own data, lack of size limits, missing
|
||||
minor type checks or over-permissive read access on non-sensitive fields.
|
||||
- **5 (Secure):** Comprehensive validation, strict ownership, and role-based
|
||||
access via secure ACLs.
|
||||
|
||||
Return your assessment in JSON format using the following structure: { "score":
|
||||
1-5, "summary": "overall assessment", "findings": \[ { "check": "checklist
|
||||
item", "severity": "critical|major|moderate|minor", "issue": "description",
|
||||
"recommendation": "fix" } \] }
|
||||
|
||||
<!-- SELF-EVOLVE:START -->
|
||||
## Self-Evolution & Proactivity (injected)
|
||||
|
||||
Zone of Initiative — calibrated autonomy:
|
||||
- Low stakes + reversible: ACT (e.g. draft a file, set a var).
|
||||
- High stakes + reversible: ACT, then report immediately.
|
||||
- High stakes + irreversible: STOP and ASK via `clarify`.
|
||||
- Reversibility is the throttle, not stakes alone.
|
||||
|
||||
Three loops keep skills alive:
|
||||
- Capture: after a task of 5+ tool calls or a reusable discovery, write/refine a skill.
|
||||
- Heal: after a failure or correction that reflects a reusable procedure, patch the skill that caused it (remove the old wording, don't layer advice).
|
||||
- Decay: on a schedule, prune duplicated/stale/no-op skills so they get sharper, not longer.
|
||||
|
||||
Memory vs Skill: a FACT (path, name, API quirk, repo URL) -> memory. a PROCEDURE (how to do X) -> skill. One-off preference -> memory/inline. Reusable correction -> skill patch.
|
||||
|
||||
For opencode (code/devops/file-ops):
|
||||
- Capture: a build/test fix pattern, a refactor recipe, or an infra quirk -> skill.
|
||||
- Heal: a bad refactor or failed deploy -> patch the responsible skill, then re-run to confirm.
|
||||
- Decay: stale scripts, dead flags, duplicated helpers. Install dep / run safe refactor = reversible; force-push / drop DB / prod deploy = ask.
|
||||
- Route research to Gemini, memory/channels to Hermes.
|
||||
|
||||
Goal: become faster, smarter, more efficient, more knowledgeable, more thoughtful, and proactively autonomous — never less safe.
|
||||
A high-stakes irreversible action (delete data, ship to prod, send a message on the user's behalf) always goes through `clarify`. Reversible, low-stakes actions may proceed, then be reported in the same turn.
|
||||
<!-- SELF-EVOLVE:END -->
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
Copyright 2025 Notion Labs, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
name: notion-knowledge-capture
|
||||
description: Capture conversations and decisions into structured Notion pages; use when turning chats/notes into wiki entries, how-tos, decisions, or FAQs with proper linking.
|
||||
metadata:
|
||||
short-description: Capture conversations into structured Notion pages
|
||||
---
|
||||
|
||||
# Knowledge Capture
|
||||
|
||||
Convert conversations and notes into structured, linkable Notion pages for easy reuse.
|
||||
|
||||
## Quick start
|
||||
1) Clarify what to capture (decision, how-to, FAQ, learning, documentation) and target audience.
|
||||
2) Identify the right database/template in `reference/` (team wiki, how-to, FAQ, decision log, learning, documentation).
|
||||
3) Pull any prior context from Notion with `Notion:notion-search` → `Notion:notion-fetch` (existing pages to update/link).
|
||||
4) Draft the page with `Notion:notion-create-pages` using the database’s schema; include summary, context, source links, and tags/owners.
|
||||
5) Link from hub pages and related records; update status/owners with `Notion:notion-update-page` as the source evolves.
|
||||
|
||||
## Workflow
|
||||
### 0) If any MCP call fails because Notion MCP is not connected, pause and set it up:
|
||||
1. Add the Notion MCP:
|
||||
- `codex mcp add notion --url https://mcp.notion.com/mcp`
|
||||
2. Enable remote MCP client:
|
||||
- Set `[features].rmcp_client = true` in `config.toml` **or** run `codex --enable rmcp_client`
|
||||
3. Log in with OAuth:
|
||||
- `codex mcp login notion`
|
||||
|
||||
After successful login, the user will have to restart codex. You should finish your answer and tell them so when they try again they can continue with Step 1.
|
||||
|
||||
### 1) Define the capture
|
||||
- Ask purpose, audience, freshness, and whether this is new or an update.
|
||||
- Determine content type: decision, how-to, FAQ, concept/wiki entry, learning/note, documentation page.
|
||||
|
||||
### 2) Locate destination
|
||||
- Pick the correct database using `reference/*-database.md` guides; confirm required properties (title, tags, owner, status, date, relations).
|
||||
- If multiple candidate databases, ask the user which to use; otherwise, create in the primary wiki/documentation DB.
|
||||
|
||||
### 3) Extract and structure
|
||||
- Extract facts, decisions, actions, and rationale from the conversation.
|
||||
- For decisions, record alternatives, rationale, and outcomes.
|
||||
- For how-tos/docs, capture steps, pre-reqs, links to assets/code, and edge cases.
|
||||
- For FAQs, phrase as Q&A with concise answers and links to deeper docs.
|
||||
|
||||
### 4) Create/update in Notion
|
||||
- Use `Notion:notion-create-pages` with the correct `data_source_id`; set properties (title, tags, owner, status, dates, relations).
|
||||
- Use templates in `reference/` to structure content (section headers, checklists).
|
||||
- If updating an existing page, fetch then edit via `Notion:notion-update-page`.
|
||||
|
||||
### 5) Link and surface
|
||||
- Add relations/backlinks to hub pages, related specs/docs, and teams.
|
||||
- Add a short summary/changelog for future readers.
|
||||
- If follow-up tasks exist, create tasks in the relevant database and link them.
|
||||
|
||||
## References and examples
|
||||
- `reference/` — database schemas and templates (e.g., `team-wiki-database.md`, `how-to-guide-database.md`, `faq-database.md`, `decision-log-database.md`, `documentation-database.md`, `learning-database.md`, `database-best-practices.md`).
|
||||
- `examples/` — capture patterns in practice (e.g., `decision-capture.md`, `how-to-guide.md`, `conversation-to-faq.md`).
|
||||
|
||||
<!-- SELF-EVOLVE:START -->
|
||||
## Self-Evolution & Proactivity (injected)
|
||||
|
||||
Zone of Initiative — calibrated autonomy:
|
||||
- Low stakes + reversible: ACT (e.g. draft a file, set a var).
|
||||
- High stakes + reversible: ACT, then report immediately.
|
||||
- High stakes + irreversible: STOP and ASK via `clarify`.
|
||||
- Reversibility is the throttle, not stakes alone.
|
||||
|
||||
Three loops keep skills alive:
|
||||
- Capture: after a task of 5+ tool calls or a reusable discovery, write/refine a skill.
|
||||
- Heal: after a failure or correction that reflects a reusable procedure, patch the skill that caused it (remove the old wording, don't layer advice).
|
||||
- Decay: on a schedule, prune duplicated/stale/no-op skills so they get sharper, not longer.
|
||||
|
||||
Memory vs Skill: a FACT (path, name, API quirk, repo URL) -> memory. a PROCEDURE (how to do X) -> skill. One-off preference -> memory/inline. Reusable correction -> skill patch.
|
||||
|
||||
For opencode (code/devops/file-ops):
|
||||
- Capture: a build/test fix pattern, a refactor recipe, or an infra quirk -> skill.
|
||||
- Heal: a bad refactor or failed deploy -> patch the responsible skill, then re-run to confirm.
|
||||
- Decay: stale scripts, dead flags, duplicated helpers. Install dep / run safe refactor = reversible; force-push / drop DB / prod deploy = ask.
|
||||
- Route research to Gemini, memory/channels to Hermes.
|
||||
|
||||
Goal: become faster, smarter, more efficient, more knowledgeable, more thoughtful, and proactively autonomous — never less safe.
|
||||
A high-stakes irreversible action (delete data, ship to prod, send a message on the user's behalf) always goes through `clarify`. Reversible, low-stakes actions may proceed, then be reported in the same turn.
|
||||
<!-- SELF-EVOLVE:END -->
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
interface:
|
||||
display_name: "Notion Knowledge Capture"
|
||||
short_description: "Capture conversations into structured Notion pages"
|
||||
icon_small: "./assets/notion-small.svg"
|
||||
icon_large: "./assets/notion.png"
|
||||
default_prompt: "Capture this conversation into structured Notion pages with decisions, action items, and owners when known."
|
||||
|
||||
dependencies:
|
||||
tools:
|
||||
- type: "mcp"
|
||||
value: "notion"
|
||||
description: "Notion MCP server"
|
||||
transport: "streamable_http"
|
||||
url: "https://mcp.notion.com/mcp"
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
|
|
@ -0,0 +1,95 @@
|
|||
# Knowledge Capture Skill Evaluations
|
||||
|
||||
Evaluation scenarios for testing the Knowledge Capture skill across different Codex models.
|
||||
|
||||
## Purpose
|
||||
|
||||
These evaluations ensure the Knowledge Capture skill:
|
||||
- Correctly identifies content types (how-to guides, FAQs, decision records, wikis)
|
||||
- Extracts relevant information from conversations
|
||||
- Structures content appropriately for each type
|
||||
- Searches and places content in the right Notion location
|
||||
- Works consistently across Haiku, Sonnet, and Opus
|
||||
|
||||
## Evaluation Files
|
||||
|
||||
### conversation-to-wiki.json
|
||||
Tests capturing conversation content as a how-to guide for the team wiki.
|
||||
|
||||
**Scenario**: Save deployment discussion to wiki
|
||||
**Key Behaviors**:
|
||||
- Extracts steps, gotchas, and best practices from conversation
|
||||
- Identifies content as How-To Guide
|
||||
- Structures with proper sections (Overview, Prerequisites, Steps, Troubleshooting)
|
||||
- Searches for team wiki location
|
||||
- Preserves technical details (commands, configs)
|
||||
|
||||
### decision-record.json
|
||||
Tests capturing architectural or technical decisions with full context.
|
||||
|
||||
**Scenario**: Document database migration decision
|
||||
**Key Behaviors**:
|
||||
- Extracts decision context, alternatives, and rationale
|
||||
- Follows decision record structure (Context, Decision, Alternatives, Consequences)
|
||||
- Captures both selected and rejected options with reasoning
|
||||
- Places in decision log or ADR database
|
||||
- Links to related technical documentation
|
||||
|
||||
## Running Evaluations
|
||||
|
||||
1. Enable the `knowledge-capture` skill
|
||||
2. Submit the query from the evaluation file
|
||||
3. Provide conversation context as specified
|
||||
4. Verify all expected behaviors are met
|
||||
5. Check success criteria for quality
|
||||
6. Test with Haiku, Sonnet, and Opus
|
||||
|
||||
## Expected Skill Behaviors
|
||||
|
||||
Knowledge Capture evaluations should verify:
|
||||
|
||||
### Content Extraction
|
||||
- Accurately captures key points from conversation context
|
||||
- Preserves specific technical details, not generic placeholders
|
||||
- Maintains context and nuance from discussion
|
||||
|
||||
### Content Type Selection
|
||||
- Correctly identifies appropriate content type (how-to, FAQ, decision record, wiki page)
|
||||
- Uses matching structure from reference documentation
|
||||
- Applies proper Notion markdown formatting
|
||||
|
||||
### Notion Integration
|
||||
- Searches for appropriate target location (wiki, decision log, etc.)
|
||||
- Creates well-structured pages with clear titles
|
||||
- Uses proper parent placement
|
||||
- Includes discoverable titles and metadata
|
||||
|
||||
### Quality Standards
|
||||
- Content is actionable and future-reference ready
|
||||
- Technical accuracy is preserved
|
||||
- Organization aids discoverability
|
||||
- Formatting enhances readability
|
||||
|
||||
## Creating New Evaluations
|
||||
|
||||
When adding Knowledge Capture evaluations:
|
||||
|
||||
1. **Use realistic conversation content** - Include actual technical details, decisions, or processes
|
||||
2. **Test different content types** - How-to guides, FAQs, decision records, meeting notes, learnings
|
||||
3. **Vary complexity** - Simple captures vs. complex technical discussions
|
||||
4. **Test discovery** - Finding the right wiki section or database
|
||||
5. **Include edge cases** - Unclear content types, minimal context, overlapping categories
|
||||
|
||||
## Example Success Criteria
|
||||
|
||||
**Good** (specific, testable):
|
||||
- "Structures content using How-To format with numbered steps"
|
||||
- "Preserves exact bash commands from conversation"
|
||||
- "Creates page with title format 'How to [Action]'"
|
||||
- "Places in Engineering Wiki → Deployment section"
|
||||
|
||||
**Bad** (vague, untestable):
|
||||
- "Creates good documentation"
|
||||
- "Uses appropriate structure"
|
||||
- "Saves to the right place"
|
||||
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "Save Conversation to Wiki",
|
||||
"skills": ["knowledge-capture"],
|
||||
"query": "Save this conversation about deploying our application to production to the team wiki",
|
||||
"context": "Preceding conversation contains discussion about deployment process, including steps, gotchas, and best practices",
|
||||
"expected_behavior": [
|
||||
"Extracts key information from conversation context (deployment steps, gotchas, best practices)",
|
||||
"Identifies content type as How-To Guide based on procedural nature",
|
||||
"Structures content using How-To structure: Overview → Prerequisites → Steps (numbered) → Verification → Troubleshooting → Related",
|
||||
"Organizes information into clear sections with proper headings",
|
||||
"Includes specific commands, configurations, or examples from conversation",
|
||||
"Adds context about why/when to use this process in Overview section",
|
||||
"Notes common issues and solutions mentioned in discussion in Troubleshooting section",
|
||||
"Uses Notion:notion-search to find team wiki location or asks user",
|
||||
"Creates page using Notion:notion-create-pages with structured content and appropriate parent",
|
||||
"Uses clear, descriptive title like 'How to Deploy to Production'",
|
||||
"Applies Notion markdown formatting (headings, code blocks, bullets)",
|
||||
"Suggests tags/categories for discoverability if wiki database"
|
||||
],
|
||||
"success_criteria": [
|
||||
"Content is structured using How-To format from SKILL.md content types",
|
||||
"Key points from conversation are captured accurately (not generic)",
|
||||
"Information is organized with proper Notion markdown (##, ###, bullets, code blocks)",
|
||||
"Specific technical details (commands, configs) are preserved from conversation",
|
||||
"Document is written for future reference with clear step-by-step instructions",
|
||||
"Title is searchable and descriptive (e.g., 'How to Deploy to Production')",
|
||||
"Page is placed in appropriate wiki location (general wiki or specific section)",
|
||||
"Uses correct tool name (Notion:notion-create-pages)"
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "Create Decision Record",
|
||||
"skills": ["knowledge-capture"],
|
||||
"query": "Document our decision to use PostgreSQL instead of MongoDB for our new service",
|
||||
"context": "User has just explained the decision with rationale, options considered, and trade-offs",
|
||||
"expected_behavior": [
|
||||
"Recognizes this as a decision record (architectural decision) from conversation context",
|
||||
"Uses Decision structure: Context → Decision → Rationale → Options Considered (with Pros/Cons) → Consequences → Implementation",
|
||||
"Extracts from context: decision made, options considered (PostgreSQL vs MongoDB), rationale, trade-offs",
|
||||
"Creates document with proper structure including Date, Status (Accepted), and Deciders",
|
||||
"Includes both positive and negative consequences (trade-offs) in Consequences section",
|
||||
"Uses Notion:notion-search to check if decision log database exists",
|
||||
"If database exists, asks whether to add there or create standalone page",
|
||||
"If creating in database, fetches schema using Notion:notion-fetch and sets properties: Decision title, Date, Status, Domain (Architecture), Deciders, Impact",
|
||||
"Uses Notion:notion-create-pages with parent: { data_source_id } for database or { page_id } for parent page",
|
||||
"Applies proper Notion markdown formatting with sections",
|
||||
"Suggests linking from architecture docs or project pages"
|
||||
],
|
||||
"success_criteria": [
|
||||
"Document follows Decision structure from SKILL.md content types",
|
||||
"All key sections present: Context, Decision, Rationale, Options Considered (with Pros/Cons for each), Consequences, Implementation",
|
||||
"Decision is clearly stated (PostgreSQL chosen over MongoDB)",
|
||||
"Options that were considered are documented with pros/cons structure",
|
||||
"Rationale explains why PostgreSQL was chosen based on conversation context",
|
||||
"Consequences include both positive (benefits) and negative (trade-offs)",
|
||||
"If in database, properties are set correctly from schema (Decision, Date, Status: Accepted, Domain: Architecture, Impact)",
|
||||
"Document is dated and has status 'Accepted'",
|
||||
"Uses correct tool names (Notion:notion-search, Notion:notion-fetch, Notion:notion-create-pages)"
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
# Example: Conversation to FAQ
|
||||
|
||||
## User Request
|
||||
|
||||
> "Save this conversation about deployment troubleshooting to the FAQ"
|
||||
|
||||
**Context**: User just had a conversation explaining how to troubleshoot common deployment errors.
|
||||
|
||||
## Conversation Summary
|
||||
|
||||
The conversation covered:
|
||||
1. **Question**: "Why does deployment fail with 'port already in use' error?"
|
||||
2. **Answer**: Process from previous deployment still running, need to kill it
|
||||
3. **Question**: "How do I find and kill the process?"
|
||||
4. **Commands shared**: `lsof -ti:3000 | xargs kill -9` or use `pm2 restart app`
|
||||
5. **Question**: "What about 'cannot connect to database' errors?"
|
||||
6. **Answer**: Check database credentials, verify database is running, check network connectivity
|
||||
7. **Best practice**: Always check logs first (`pm2 logs` or `docker logs`)
|
||||
|
||||
## Workflow Execution
|
||||
|
||||
### Step 1: Identify Content Type
|
||||
|
||||
This is Q&A content → **FAQ Entry** format is appropriate
|
||||
|
||||
### Step 2: Structure FAQ Entries
|
||||
|
||||
Extract distinct Q&A pairs from conversation:
|
||||
|
||||
**FAQ 1**: Port already in use error
|
||||
**FAQ 2**: Cannot connect to database error
|
||||
**FAQ 3**: General troubleshooting approach
|
||||
|
||||
### Step 3: Determine Destination
|
||||
|
||||
```
|
||||
Notion:notion-search
|
||||
query: "FAQ deployment"
|
||||
query_type: "internal"
|
||||
```
|
||||
|
||||
Found: "Deployment FAQ" database in Engineering Docs
|
||||
|
||||
```
|
||||
Notion:notion-fetch
|
||||
id: "deployment-faq-database-id"
|
||||
```
|
||||
|
||||
**Schema**:
|
||||
- Question (title)
|
||||
- Category (select): Deployment, Configuration, Troubleshooting, etc.
|
||||
- Tags (multi_select)
|
||||
- Last Reviewed (date)
|
||||
|
||||
### Step 4: Create FAQ Entries
|
||||
|
||||
**FAQ Entry 1: Port Already in Use**
|
||||
|
||||
```
|
||||
Notion:notion-create-pages
|
||||
|
||||
parent: { data_source_id: "collection://faq-db-uuid" }
|
||||
|
||||
pages: [{
|
||||
properties: {
|
||||
"Question": "Why does deployment fail with 'port already in use' error?",
|
||||
"Category": "Troubleshooting",
|
||||
"Tags": "deployment, errors, ports",
|
||||
"date:Last Reviewed:start": "2025-10-14",
|
||||
"date:Last Reviewed:is_datetime": 0
|
||||
},
|
||||
content: "## Short Answer\n\nThe deployment port (usually 3000) is still occupied by a process from a previous deployment. You need to kill the existing process before deploying again.\n\n## Detailed Explanation\n\nWhen you deploy the application, it tries to bind to a specific port (e.g., port 3000). If a previous deployment didn't shut down cleanly, that process may still be running and holding the port. This prevents the new deployment from starting.\n\n**Common causes**:\n- Previous deployment crashed without cleanup\n- Manual `node` process started and forgotten\n- PM2 or other process manager didn't restart properly\n- Multiple deployments attempted simultaneously\n\n## Solution\n\n### Option 1: Kill the process using the port\n\n```bash\n# Find and kill process on port 3000\nlsof -ti:3000 | xargs kill -9\n```\n\nReplace `3000` with your application's port number.\n\n### Option 2: If using PM2\n\n```bash\n# Restart the application\npm2 restart app\n\n# Or delete and restart\npm2 delete app\npm2 start app.js --name app\n```\n\n### Option 3: Check all node processes\n\n```bash\n# List all node processes\nps aux | grep node\n\n# Kill specific process by PID\nkill -9 <PID>\n```\n\n## Prevention\n\n1. **Use process managers**: PM2, systemd, or Docker handle cleanup automatically\n2. **Graceful shutdown**: Implement proper shutdown handlers in your app\n3. **Health checks**: Monitor if previous deployment shut down before starting new one\n\n```javascript\n// Graceful shutdown example\nprocess.on('SIGTERM', () => {\n server.close(() => {\n console.log('Server closed');\n process.exit(0);\n });\n});\n```\n\n## Verification\n\nAfter killing the process, verify the port is free:\n\n```bash\nlsof -ti:3000\n# Should return nothing if port is free\n```\n\n## Related Questions\n- <mention-page url=\"...\">How do I check what's using a port?</mention-page>\n- <mention-page url=\"...\">How do I configure the application port?</mention-page>\n- <mention-page url=\"...\">PM2 deployment best practices</mention-page>\n\n## Last Updated\nOctober 14, 2025"
|
||||
}]
|
||||
```
|
||||
|
||||
**FAQ Entry 2: Database Connection Errors**
|
||||
|
||||
```
|
||||
Notion:notion-create-pages
|
||||
|
||||
parent: { data_source_id: "collection://faq-db-uuid" }
|
||||
|
||||
pages: [{
|
||||
properties: {
|
||||
"Question": "Why do I get 'cannot connect to database' errors during deployment?",
|
||||
"Category": "Troubleshooting",
|
||||
"Tags": "deployment, database, errors",
|
||||
"date:Last Reviewed:start": "2025-10-14",
|
||||
"date:Last Reviewed:is_datetime": 0
|
||||
},
|
||||
content: "## Short Answer\n\nDatabase connection errors usually mean either the database isn't running, credentials are incorrect, or there's a network connectivity issue. Check database status, verify credentials, and test connectivity.\n\n## Detailed Explanation\n\nThe application can't establish a connection to the database during startup. This prevents the application from initializing properly.\n\n**Common causes**:\n- Database service isn't running\n- Incorrect connection credentials\n- Network connectivity issues (firewall, security groups)\n- Database host/port misconfigured\n- Database is at connection limit\n- SSL/TLS configuration mismatch\n\n## Troubleshooting Steps\n\n### Step 1: Check database status\n\n```bash\n# For local PostgreSQL\npg_isready -h localhost -p 5432\n\n# For Docker\ndocker ps | grep postgres\n\n# For MongoDB\nmongosh --eval \"db.adminCommand('ping')\"\n```\n\n### Step 2: Verify credentials\n\nCheck your `.env` or configuration file:\n\n```bash\n# Common environment variables\nDB_HOST=localhost\nDB_PORT=5432\nDB_NAME=myapp_production\nDB_USER=myapp_user\nDB_PASSWORD=***********\n```\n\nTest connection manually:\n\n```bash\n# PostgreSQL\npsql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME\n\n# MongoDB\nmongosh \"mongodb://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME\"\n```\n\n### Step 3: Check network connectivity\n\n```bash\n# Test if port is reachable\ntelnet $DB_HOST $DB_PORT\n\n# Or using nc\nnc -zv $DB_HOST $DB_PORT\n\n# Check firewall rules (if applicable)\nsudo iptables -L\n```\n\n### Step 4: Check application logs\n\n```bash\n# PM2 logs\npm2 logs app\n\n# Docker logs\ndocker logs container-name\n\n# Application logs\ntail -f /var/log/app/error.log\n```\n\nLook for specific error messages:\n- `ECONNREFUSED`: Database not running or wrong host/port\n- `Authentication failed`: Wrong credentials\n- `Timeout`: Network/firewall issue\n- `Too many connections`: Database connection limit reached\n\n## Solutions by Error Type\n\n### Database Not Running\n\n```bash\n# Start PostgreSQL\nsudo systemctl start postgresql\n\n# Start via Docker\ndocker start postgres-container\n```\n\n### Wrong Credentials\n\n1. Reset database password\n2. Update `.env` file\n3. Restart application\n\n### Connection Limit Reached\n\n```sql\n-- Check current connections (PostgreSQL)\nSELECT count(*) FROM pg_stat_activity;\n\n-- Increase max connections\nALTER SYSTEM SET max_connections = 200;\n```\n\n### SSL/TLS Issues\n\nAdd to connection string:\n```\nssl=true&sslmode=require\n```\n\nOr disable SSL for dev:\n```\nsslmode=disable\n```\n\n## Prevention\n\n1. **Connection pooling**: Limits concurrent connections\n```javascript\npool: {\n min: 2,\n max: 10\n}\n```\n\n2. **Health checks**: Verify database before starting app\n```javascript\nconst healthCheck = async () => {\n try {\n await db.raw('SELECT 1');\n console.log('Database connected');\n } catch (err) {\n console.error('Database connection failed:', err);\n process.exit(1);\n }\n};\n```\n\n3. **Retry logic**: Auto-reconnect on temporary failures\n```javascript\nretry: {\n max: 3,\n backoff: 1000\n}\n```\n\n4. **Environment validation**: Check all required env vars exist\n```javascript\nconst required = ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD'];\nfor (const key of required) {\n if (!process.env[key]) {\n throw new Error(`Missing required env var: ${key}`);\n }\n}\n```\n\n## Related Questions\n- <mention-page url=\"...\">How do I configure database connection pooling?</mention-page>\n- <mention-page url=\"...\">Database backup and restore procedures</mention-page>\n- <mention-page url=\"...\">Database performance optimization</mention-page>\n\n## Last Updated\nOctober 14, 2025"
|
||||
}]
|
||||
```
|
||||
|
||||
**FAQ Entry 3: General Troubleshooting**
|
||||
|
||||
```
|
||||
Notion:notion-create-pages
|
||||
|
||||
parent: { data_source_id: "collection://faq-db-uuid" }
|
||||
|
||||
pages: [{
|
||||
properties: {
|
||||
"Question": "What's the first thing I should check when deployment fails?",
|
||||
"Category": "Troubleshooting",
|
||||
"Tags": "deployment, debugging, best-practices",
|
||||
"date:Last Reviewed:start": "2025-10-14",
|
||||
"date:Last Reviewed:is_datetime": 0
|
||||
},
|
||||
content: "## Short Answer\n\n**Always check the logs first.** Logs contain error messages that point you directly to the problem. Use `pm2 logs`, `docker logs`, or check your application's log files.\n\n## Detailed Explanation\n\nLogs are your first and most important debugging tool. They show:\n- Exact error messages\n- Stack traces\n- Timing information\n- Configuration issues\n- Dependency problems\n\nMost deployment issues can be diagnosed and fixed by reading the logs carefully.\n\n## How to Check Logs\n\n### PM2\n\n```bash\n# View all logs\npm2 logs\n\n# View logs for specific app\npm2 logs app-name\n\n# View only errors\npm2 logs --err\n\n# Follow logs in real-time\npm2 logs --lines 100\n```\n\n### Docker\n\n```bash\n# View logs\ndocker logs container-name\n\n# Follow logs\ndocker logs -f container-name\n\n# Last 100 lines\ndocker logs --tail 100 container-name\n\n# With timestamps\ndocker logs -t container-name\n```\n\n### Application Logs\n\n```bash\n# Tail application logs\ntail -f /var/log/app/app.log\ntail -f /var/log/app/error.log\n\n# Search logs for errors\ngrep -i error /var/log/app/*.log\n\n# View logs with context\ngrep -B 5 -A 5 \"ERROR\" app.log\n```\n\n## Systematic Troubleshooting Approach\n\n### 1. Check the logs\n- Read error messages carefully\n- Note the exact error type and message\n- Check timestamps to find when error occurred\n\n### 2. Verify configuration\n- Environment variables set correctly?\n- Configuration files present and valid?\n- Paths and file permissions correct?\n\n### 3. Check dependencies\n- All packages installed? (`node_modules` present?)\n- Correct versions installed?\n- Any native module compilation errors?\n\n### 4. Verify environment\n- Required services running (database, Redis, etc.)?\n- Ports available?\n- Network connectivity working?\n\n### 5. Test components individually\n- Can you connect to database manually?\n- Can you run application locally?\n- Do health check endpoints work?\n\n### 6. Check recent changes\n- What changed since last successful deployment?\n- New dependencies added?\n- Configuration modified?\n- Environment differences?\n\n## Common Error Patterns\n\n### \"Module not found\"\n```bash\n# Solution: Install dependencies\nnpm install\n# or\nnpm ci\n```\n\n### \"Permission denied\"\n```bash\n# Solution: Fix file permissions\nchmod +x start.sh\nsudo chown -R appuser:appuser /app\n```\n\n### \"Address already in use\"\n```bash\n# Solution: Kill process on port\nlsof -ti:3000 | xargs kill -9\n```\n\n### \"Cannot connect to...\"\n```bash\n# Solution: Verify service is running and reachable\ntelnet service-host port\n```\n\n## Debugging Tools\n\n### Log Aggregation\n- **PM2**: Built-in log management\n- **Docker**: Centralized logging with log drivers\n- **ELK Stack**: Elasticsearch, Logstash, Kibana for large scale\n- **CloudWatch**: For AWS deployments\n\n### Monitoring\n- **PM2 Monit**: `pm2 monit` for real-time metrics\n- **Docker Stats**: `docker stats` for resource usage\n- **System metrics**: `top`, `htop` for CPU/memory\n\n### Network Debugging\n```bash\n# Check open connections\nnetstat -tulpn\n\n# Check specific port\nlsof -i :3000\n\n# Test connectivity\ncurl http://localhost:3000/health\n```\n\n## When to Escalate\n\nEscalate to senior team member if:\n- Error message is cryptic and not Google-able\n- Problem persists after trying obvious solutions\n- Production is affected\n- Data integrity may be compromised\n- Security-related errors\n\n## Prevention\n\n1. **Automated health checks** before deployment\n2. **Staged deployments** (dev → staging → production)\n3. **Rollback plan** ready for quick recovery\n4. **Monitoring and alerts** for early problem detection\n5. **Deployment checklist** to ensure all steps followed\n\n## Related Questions\n- <mention-page url=\"...\">How do I set up log aggregation?</mention-page>\n- <mention-page url=\"...\">Deployment checklist and best practices</mention-page>\n- <mention-page url=\"...\">How to roll back a failed deployment?</mention-page>\n\n## Last Updated\nOctober 14, 2025"
|
||||
}]
|
||||
```
|
||||
|
||||
### Step 5: Update FAQ Index
|
||||
|
||||
```
|
||||
Notion:notion-fetch
|
||||
id: "faq-index-page-id"
|
||||
```
|
||||
|
||||
```
|
||||
Notion:notion-update-page
|
||||
page_id: "faq-index-page-id"
|
||||
command: "insert_content_after"
|
||||
selection_with_ellipsis: "## Deployment & Troubleshooting..."
|
||||
new_str: "
|
||||
- <mention-page url=\"...\">Why does deployment fail with 'port already in use' error?</mention-page>
|
||||
- <mention-page url=\"...\">Why do I get 'cannot connect to database' errors?</mention-page>
|
||||
- <mention-page url=\"...\">What's the first thing I should check when deployment fails?</mention-page>
|
||||
"
|
||||
```
|
||||
|
||||
## Summary Provided to User
|
||||
|
||||
```markdown
|
||||
## FAQ Entries Created ✅
|
||||
|
||||
### Conversation Captured
|
||||
Transformed deployment troubleshooting conversation into 3 comprehensive FAQ entries.
|
||||
|
||||
### FAQ Entries Created
|
||||
|
||||
1. **Port Already in Use Error**
|
||||
- Problem: Deployment fails due to occupied port
|
||||
- Solutions: Kill process, use PM2, check node processes
|
||||
- Prevention: Use process managers, graceful shutdown
|
||||
- Link: <mention-page url="...">Port already in use FAQ</mention-page>
|
||||
|
||||
2. **Database Connection Errors**
|
||||
- Problem: Cannot connect to database during deployment
|
||||
- Troubleshooting: 4-step systematic approach
|
||||
- Solutions: By error type (credentials, network, limits)
|
||||
- Prevention: Connection pooling, health checks, retry logic
|
||||
- Link: <mention-page url="...">Database connection FAQ</mention-page>
|
||||
|
||||
3. **General Troubleshooting Approach**
|
||||
- Best practice: Always check logs first
|
||||
- Tools: PM2 logs, Docker logs, application logs
|
||||
- Systematic approach: 6-step debugging process
|
||||
- Common errors: Module not found, permissions, ports
|
||||
- Link: <mention-page url="...">Troubleshooting first steps FAQ</mention-page>
|
||||
|
||||
### Content Structure
|
||||
|
||||
Each FAQ includes:
|
||||
✅ Short answer (quick reference)
|
||||
✅ Detailed explanation (full context)
|
||||
✅ Multiple solution options
|
||||
✅ Code examples and commands
|
||||
✅ Prevention strategies
|
||||
✅ Links to related FAQs
|
||||
✅ Last updated date
|
||||
|
||||
### Database Integration
|
||||
|
||||
- ✅ Added to FAQ database (not standalone pages)
|
||||
- ✅ Properties set: Category (Troubleshooting), Tags, Date
|
||||
- ✅ FAQ index page updated with links
|
||||
- ✅ Cross-references between related questions
|
||||
|
||||
### Value Added
|
||||
|
||||
**From conversation** → **To structured knowledge**:
|
||||
- Casual Q&A → Professional FAQ entries
|
||||
- Implicit knowledge → Explicit documentation
|
||||
- One-time help → Reusable resource
|
||||
- 5-minute conversation → Permanent team knowledge
|
||||
|
||||
**Future benefit**:
|
||||
- Team can self-serve on common issues
|
||||
- New team members can onboard faster
|
||||
- Reduces repeated questions
|
||||
- Improves troubleshooting speed
|
||||
|
||||
**Knowledge captured and preserved! 📚**
|
||||
```
|
||||
|
||||
## Key Features Demonstrated
|
||||
|
||||
### Content Extraction
|
||||
- Identified distinct Q&A pairs from conversation
|
||||
- Extracted commands, solutions, and best practices
|
||||
- Preserved troubleshooting workflows
|
||||
|
||||
### FAQ Structure
|
||||
- Short answer for quick reference
|
||||
- Detailed explanation for understanding
|
||||
- Multiple solutions (Options 1, 2, 3)
|
||||
- Code examples with comments
|
||||
- Prevention strategies
|
||||
- Related questions linking
|
||||
|
||||
### Database Integration
|
||||
- Created in FAQ database (not standalone)
|
||||
- Set appropriate properties (category, tags, date)
|
||||
- Updated index page with links
|
||||
- Cross-referenced related questions
|
||||
|
||||
### Value Transformation
|
||||
- Converted informal conversation to professional docs
|
||||
- Added structure, examples, and prevention tips
|
||||
- Made knowledge searchable and discoverable
|
||||
- Created permanent team resource
|
||||
|
||||
Perfect for: FAQ creation, knowledge preservation, team documentation
|
||||
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
# Example: Decision Record Capture
|
||||
|
||||
**User Request**: "Document our decision to move from REST to GraphQL API"
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Extract Decision from Context
|
||||
From conversation, identified:
|
||||
- **Decision**: Migrate customer-facing API from REST to GraphQL
|
||||
- **Context**: REST endpoints becoming unwieldy (50+ endpoints)
|
||||
- **Alternatives**: Keep REST, try gRPC, adopt GraphQL
|
||||
- **Rationale**: Better client experience, type safety, single endpoint
|
||||
|
||||
### 2. Find Decision Log
|
||||
```
|
||||
Notion:notion-search
|
||||
query: "architecture decisions" or "ADR"
|
||||
```
|
||||
|
||||
Found: "Architecture Decision Records" database
|
||||
|
||||
### 3. Fetch Schema
|
||||
```
|
||||
Notion:notion-fetch
|
||||
```
|
||||
|
||||
Got properties: Decision (title), Date, Status, Domain, Impact, Deciders, Stakeholders
|
||||
|
||||
### 4. Create Decision Record
|
||||
```
|
||||
Notion:notion-create-pages
|
||||
parent: { data_source_id: "decision-log-collection-id" }
|
||||
pages: [{
|
||||
properties: {
|
||||
"Decision": "Migrate to GraphQL API",
|
||||
"date:Date:start": "2025-10-16",
|
||||
"date:Date:is_datetime": 0,
|
||||
"Status": "Accepted",
|
||||
"Domain": "Architecture",
|
||||
"Impact": "High"
|
||||
},
|
||||
content: "[Full decision record with context, rationale, alternatives...]"
|
||||
}]
|
||||
```
|
||||
|
||||
**Content sample**:
|
||||
|
||||
```markdown
|
||||
# Migrate Customer-Facing API to GraphQL
|
||||
|
||||
## Context
|
||||
Our REST API has grown to 50+ endpoints with inconsistent patterns. Frontend and mobile teams request new endpoints frequently, leading to:
|
||||
- API bloat and maintenance burden
|
||||
- Over-fetching/under-fetching data
|
||||
- Slow iteration on client features
|
||||
- Inconsistent error handling
|
||||
|
||||
## Decision
|
||||
Migrate customer-facing API from REST to GraphQL while maintaining REST for internal services.
|
||||
|
||||
## Rationale
|
||||
**Why GraphQL**:
|
||||
- Clients fetch exactly what they need (no over/under-fetching)
|
||||
- Single endpoint, self-documenting schema
|
||||
- Type safety with code generation
|
||||
- Better developer experience
|
||||
- Industry standard for client-facing APIs
|
||||
|
||||
## Options Considered
|
||||
|
||||
### Option A: Keep REST (Status Quo)
|
||||
**Pros**: No migration cost, team familiar
|
||||
**Cons**: Doesn't solve core problems, maintenance keeps growing
|
||||
**Decision**: Rejected - doesn't address root cause
|
||||
|
||||
### Option B: gRPC
|
||||
**Pros**: Performance, strong typing
|
||||
**Cons**: Browser support issues, steeper learning curve, not designed for client-facing
|
||||
**Decision**: Rejected - better for internal services
|
||||
|
||||
### Option C: GraphQL
|
||||
**Pros**: Solves over/under-fetching, great DX, strong ecosystem
|
||||
**Cons**: Learning curve, caching complexity, migration effort
|
||||
**Decision**: **Accepted**
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive**:
|
||||
- Faster frontend/mobile development
|
||||
- Reduced API maintenance burden
|
||||
- Better type safety and tooling
|
||||
- Single endpoint simplifies deployment
|
||||
|
||||
**Negative**:
|
||||
- 3-4 month migration timeline
|
||||
- Team needs training
|
||||
- Need to solve caching strategy
|
||||
- Monitoring/debugging patterns different
|
||||
|
||||
## Implementation Plan
|
||||
1. GraphQL server setup (Apollo Server)
|
||||
2. Schema design workshop
|
||||
3. Gradual migration (start with new features)
|
||||
4. Dual-run period (both REST and GraphQL)
|
||||
5. Deprecate old REST endpoints
|
||||
|
||||
**Timeline**: Start Q4 2025, complete Q1 2026
|
||||
**Owner**: Backend team lead
|
||||
|
||||
## Success Metrics
|
||||
- API response times improve 30%
|
||||
- Client fetch efficiency (less data transferred)
|
||||
- Reduced new endpoint requests
|
||||
- Developer satisfaction scores
|
||||
```
|
||||
|
||||
### 5. Make Discoverable
|
||||
Added link from Architecture Wiki and notified team in Slack.
|
||||
|
||||
## Key Success Factors
|
||||
- Captured decision while context fresh
|
||||
- Documented alternatives considered
|
||||
- Included both pros and cons
|
||||
- Clear implementation plan
|
||||
- Saved to decision log for future reference
|
||||
- Made discoverable for team
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
# Example: How-To Guide from Discussion
|
||||
|
||||
**User Request**: "Save our discussion about deploying to production as a how-to guide"
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Extract Content from Chat
|
||||
From conversation, identified:
|
||||
- Deployment prerequisites
|
||||
- Step-by-step procedure
|
||||
- Common issues and solutions
|
||||
- Best practices and tips
|
||||
|
||||
### 2. Structure as How-To
|
||||
Organized into:
|
||||
- Overview & prerequisites
|
||||
- Numbered deployment steps
|
||||
- Verification steps
|
||||
- Troubleshooting section
|
||||
- Related resources
|
||||
|
||||
### 3. Find Location
|
||||
```
|
||||
Notion:notion-search
|
||||
query: "deployment documentation"
|
||||
```
|
||||
Found: Engineering Wiki → Deployment section
|
||||
|
||||
### 4. Create Guide
|
||||
```
|
||||
Notion:notion-create-pages
|
||||
parent: { page_id: "deployment-section-id" }
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```markdown
|
||||
# How to Deploy to Production
|
||||
|
||||
## Overview
|
||||
Production deployment using GitHub Actions with zero-downtime rolling updates.
|
||||
**Time Required**: 15-20 minutes | **Complexity**: Intermediate
|
||||
|
||||
## Prerequisites
|
||||
- [ ] PR approved and merged to main
|
||||
- [ ] All CI tests passing
|
||||
- [ ] Database migrations reviewed
|
||||
- [ ] Feature flags configured
|
||||
|
||||
## Deployment Steps
|
||||
|
||||
1. **Verify main branch is ready**
|
||||
```bash
|
||||
git checkout main && git pull
|
||||
```
|
||||
|
||||
2. **Tag release**
|
||||
```bash
|
||||
git tag -a v1.2.3 -m "Release v1.2.3"
|
||||
git push origin v1.2.3
|
||||
```
|
||||
|
||||
3. **Trigger deployment**
|
||||
- GitHub Actions auto-starts from tag push
|
||||
- Monitor: https://github.com/org/repo/actions
|
||||
|
||||
4. **Database migrations** (if needed)
|
||||
- Auto-run in GitHub Actions
|
||||
- Check logs for completion
|
||||
|
||||
5. **Verify deployment**
|
||||
- Wait for health checks (2-3 min)
|
||||
- Test key endpoints
|
||||
- Check error rates in Datadog
|
||||
|
||||
## Verification Checklist
|
||||
- [ ] All pods healthy in k8s dashboard
|
||||
- [ ] Error rate < 0.1% in last 10 min
|
||||
- [ ] Response time p95 < 500ms
|
||||
- [ ] Test login flow
|
||||
- [ ] Check Slack #alerts channel
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Health checks failing**
|
||||
→ Check pod logs: `kubectl logs -f deployment/api -n production`
|
||||
|
||||
**Migration errors**
|
||||
→ Rollback: Revert tag, migrations auto-rollback
|
||||
|
||||
**High error rate**
|
||||
→ Emergency rollback: Previous tag auto-deploys via GitHub Actions
|
||||
|
||||
## Best Practices
|
||||
- Deploy during low-traffic hours (2-4am PST)
|
||||
- Have 2 engineers available
|
||||
- Monitor for 30 min post-deploy
|
||||
- Update #engineering Slack with deploy notice
|
||||
|
||||
## Related Docs
|
||||
- <mention-page url="...">Rollback Procedure</mention-page>
|
||||
- <mention-page url="...">Database Migration Guide</mention-page>
|
||||
```
|
||||
|
||||
### 5. Make Discoverable
|
||||
```
|
||||
Notion:notion-update-page
|
||||
page_id: "engineering-wiki-homepage"
|
||||
command: "insert_content_after"
|
||||
```
|
||||
Added link in Engineering Wiki → How-To Guides section
|
||||
|
||||
## Key Success Factors
|
||||
- Captured tribal knowledge from discussion
|
||||
- Structured as actionable steps
|
||||
- Included troubleshooting from experience
|
||||
- Made discoverable by linking from wiki index
|
||||
- Added metadata (time, complexity)
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
# Database Best Practices
|
||||
|
||||
General guidance for creating and maintaining knowledge capture databases.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Keep It Simple
|
||||
- Start with core properties
|
||||
- Add more only when needed
|
||||
- Don't over-engineer
|
||||
|
||||
### 2. Use Consistent Naming
|
||||
- Title property for main identifier
|
||||
- Status for lifecycle tracking
|
||||
- Tags for flexible categorization
|
||||
- Owner for accountability
|
||||
|
||||
### 3. Include Metadata
|
||||
- Created/Updated timestamps
|
||||
- Owner or maintainer
|
||||
- Last reviewed dates
|
||||
- Status indicators
|
||||
|
||||
### 4. Enable Discovery
|
||||
- Use tags liberally
|
||||
- Create helpful views
|
||||
- Link related content
|
||||
- Use clear titles
|
||||
|
||||
### 5. Plan for Scale
|
||||
- Consider filters early
|
||||
- Use relations for connections
|
||||
- Think about search
|
||||
- Organize with categories
|
||||
|
||||
## Creating a Database
|
||||
|
||||
### Using `Notion:notion-create-database`
|
||||
|
||||
Example for documentation database:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"parent": {"page_id": "wiki-page-id"},
|
||||
"title": [{"text": {"content": "Team Documentation"}}],
|
||||
"properties": {
|
||||
"Type": {
|
||||
"select": {
|
||||
"options": [
|
||||
{"name": "How-To", "color": "blue"},
|
||||
{"name": "Concept", "color": "green"},
|
||||
{"name": "Reference", "color": "gray"},
|
||||
{"name": "FAQ", "color": "yellow"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Category": {
|
||||
"select": {
|
||||
"options": [
|
||||
{"name": "Engineering", "color": "red"},
|
||||
{"name": "Product", "color": "purple"},
|
||||
{"name": "Design", "color": "pink"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Tags": {"multi_select": {"options": []}},
|
||||
"Owner": {"people": {}},
|
||||
"Status": {
|
||||
"select": {
|
||||
"options": [
|
||||
{"name": "Draft", "color": "gray"},
|
||||
{"name": "Final", "color": "green"},
|
||||
{"name": "Deprecated", "color": "red"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fetching Database Schema
|
||||
|
||||
Before creating pages, always fetch database to get schema:
|
||||
|
||||
```
|
||||
Notion:notion-fetch
|
||||
id: "database-url-or-id"
|
||||
```
|
||||
|
||||
This returns the exact property names and types to use.
|
||||
|
||||
## Database Selection Guide
|
||||
|
||||
| Need | Use This Database |
|
||||
|------|-------------------|
|
||||
| General documentation | [Documentation Database](documentation-database.md) |
|
||||
| Track decisions | [Decision Log](decision-log-database.md) |
|
||||
| Q&A knowledge base | [FAQ Database](faq-database.md) |
|
||||
| Team-specific content | [Team Wiki](team-wiki-database.md) |
|
||||
| Step-by-step guides | [How-To Guide Database](how-to-guide-database.md) |
|
||||
| Incident/project learnings | [Learning Database](learning-database.md) |
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Start with general documentation database** - most flexible
|
||||
2. **Add specialized databases** as needs emerge (FAQ, Decisions)
|
||||
3. **Use relations** to connect related docs
|
||||
4. **Create views** for common use cases
|
||||
5. **Review properties** quarterly - remove unused ones
|
||||
6. **Document the schema** in database description
|
||||
7. **Train team** on property usage and conventions
|
||||
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# Decision Log Database (ADR - Architecture Decision Records)
|
||||
|
||||
**Purpose**: Track important decisions with context and rationale.
|
||||
|
||||
## Schema
|
||||
|
||||
| Property | Type | Options | Purpose |
|
||||
|----------|------|---------|---------|
|
||||
| **Decision** | title | - | What was decided |
|
||||
| **Date** | date | - | When decision was made |
|
||||
| **Status** | select | Proposed, Accepted, Superseded, Deprecated | Current decision status |
|
||||
| **Domain** | select | Architecture, Product, Business, Design, Operations | Decision category |
|
||||
| **Impact** | select | High, Medium, Low | Expected impact level |
|
||||
| **Deciders** | people | - | Who made the decision |
|
||||
| **Stakeholders** | people | - | Who's affected by decision |
|
||||
| **Related Decisions** | relation | Links to other decisions | Context and dependencies |
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
Create decision records with properties:
|
||||
{
|
||||
"Decision": "Use PostgreSQL for Primary Database",
|
||||
"Date": "2025-10-15",
|
||||
"Status": "Accepted",
|
||||
"Domain": "Architecture",
|
||||
"Impact": "High",
|
||||
"Deciders": [tech_lead, architect],
|
||||
"Stakeholders": [eng_team]
|
||||
}
|
||||
```
|
||||
|
||||
## Content Template
|
||||
|
||||
Each decision page should include:
|
||||
- **Context**: Why this decision was needed
|
||||
- **Decision**: What was decided
|
||||
- **Rationale**: Why this option was chosen
|
||||
- **Options Considered**: Alternatives and trade-offs
|
||||
- **Consequences**: Expected outcomes (positive and negative)
|
||||
- **Implementation**: How decision will be executed
|
||||
|
||||
## Views
|
||||
|
||||
**Recent Decisions**: Sort by Date descending
|
||||
**Active Decisions**: Filter where Status = "Accepted"
|
||||
**By Domain**: Group by Domain
|
||||
**High Impact**: Filter where Impact = "High"
|
||||
**Pending**: Filter where Status = "Proposed"
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Document immediately**: Record decisions when made, while context is fresh
|
||||
2. **Include alternatives**: Show what was considered and why it wasn't chosen
|
||||
3. **Track superseded decisions**: Update status when decisions change
|
||||
4. **Link related decisions**: Use relations to show dependencies
|
||||
5. **Review periodically**: Check if old decisions are still valid
|
||||
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# General Documentation Database
|
||||
|
||||
**Purpose**: Store all types of documentation in a searchable, organized database.
|
||||
|
||||
## Schema
|
||||
|
||||
| Property | Type | Options | Purpose |
|
||||
|----------|------|---------|---------|
|
||||
| **Title** | title | - | Document name |
|
||||
| **Type** | select | How-To, Concept, Reference, FAQ, Decision, Post-Mortem | Categorize content type |
|
||||
| **Category** | select | Engineering, Product, Design, Operations, General | Organize by department/topic |
|
||||
| **Tags** | multi_select | - | Additional categorization (languages, tools, topics) |
|
||||
| **Status** | select | Draft, In Review, Final, Deprecated | Track document lifecycle |
|
||||
| **Owner** | people | - | Document maintainer |
|
||||
| **Created** | created_time | - | Auto-populated creation date |
|
||||
| **Last Updated** | last_edited_time | - | Auto-populated last edit |
|
||||
| **Last Reviewed** | date | - | Manual review tracking |
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
Create pages with properties:
|
||||
{
|
||||
"Title": "How to Deploy to Production",
|
||||
"Type": "How-To",
|
||||
"Category": "Engineering",
|
||||
"Tags": "deployment, production, DevOps",
|
||||
"Status": "Final",
|
||||
"Owner": [current_user],
|
||||
"Last Reviewed": "2025-10-01"
|
||||
}
|
||||
```
|
||||
|
||||
## Views
|
||||
|
||||
**By Type**: Group by Type property
|
||||
**By Category**: Group by Category property
|
||||
**Recent Updates**: Sort by Last Updated descending
|
||||
**Needs Review**: Filter where Last Reviewed > 90 days ago
|
||||
**Draft Docs**: Filter where Status = "Draft"
|
||||
|
||||
## Creating This Database
|
||||
|
||||
Use `Notion:notion-create-database`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"parent": {"page_id": "wiki-page-id"},
|
||||
"title": [{"text": {"content": "Team Documentation"}}],
|
||||
"properties": {
|
||||
"Type": {
|
||||
"select": {
|
||||
"options": [
|
||||
{"name": "How-To", "color": "blue"},
|
||||
{"name": "Concept", "color": "green"},
|
||||
{"name": "Reference", "color": "gray"},
|
||||
{"name": "FAQ", "color": "yellow"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Category": {
|
||||
"select": {
|
||||
"options": [
|
||||
{"name": "Engineering", "color": "red"},
|
||||
{"name": "Product", "color": "purple"},
|
||||
{"name": "Design", "color": "pink"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Tags": {"multi_select": {"options": []}},
|
||||
"Owner": {"people": {}},
|
||||
"Status": {
|
||||
"select": {
|
||||
"options": [
|
||||
{"name": "Draft", "color": "gray"},
|
||||
{"name": "Final", "color": "green"},
|
||||
{"name": "Deprecated", "color": "red"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start with this schema** - most flexible for general documentation
|
||||
2. **Use relations** to connect related docs
|
||||
3. **Create views** for common use cases
|
||||
4. **Review properties** quarterly - remove unused ones
|
||||
5. **Document the schema** in database description
|
||||
6. **Train team** on property usage and conventions
|
||||
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# FAQ Database
|
||||
|
||||
**Purpose**: Organize frequently asked questions with answers.
|
||||
|
||||
## Schema
|
||||
|
||||
| Property | Type | Options | Purpose |
|
||||
|----------|------|---------|---------|
|
||||
| **Question** | title | - | The question being asked |
|
||||
| **Category** | select | Product, Engineering, Support, HR, General | Question topic |
|
||||
| **Tags** | multi_select | - | Specific topics (auth, billing, onboarding, etc.) |
|
||||
| **Answer Type** | select | Quick Answer, Detailed Guide, Link to Docs | Response format |
|
||||
| **Last Reviewed** | date | - | When answer was verified |
|
||||
| **Helpful Count** | number | - | Track usefulness (optional) |
|
||||
| **Audience** | select | Internal, External, All | Who should see this |
|
||||
| **Related Questions** | relation | Links to related FAQs | Connect similar topics |
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
Create FAQ entries with properties:
|
||||
{
|
||||
"Question": "How do I reset my password?",
|
||||
"Category": "Support",
|
||||
"Tags": "authentication, password, login",
|
||||
"Answer Type": "Quick Answer",
|
||||
"Last Reviewed": "2025-10-01",
|
||||
"Audience": "External"
|
||||
}
|
||||
```
|
||||
|
||||
## Content Template
|
||||
|
||||
Each FAQ page should include:
|
||||
- **Short Answer**: 1-2 sentence quick response
|
||||
- **Detailed Explanation**: Full answer with context
|
||||
- **Steps** (if applicable): Numbered procedure
|
||||
- **Screenshots** (if helpful): Visual guidance
|
||||
- **Related Questions**: Links to similar FAQs
|
||||
- **Additional Resources**: External docs or videos
|
||||
|
||||
## Views
|
||||
|
||||
**By Category**: Group by Category
|
||||
**Recently Updated**: Sort by Last Reviewed descending
|
||||
**Needs Review**: Filter where Last Reviewed > 180 days ago
|
||||
**External FAQs**: Filter where Audience contains "External"
|
||||
**Popular**: Sort by Helpful Count descending (if tracking)
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use clear questions**: Write questions as users would ask them
|
||||
2. **Provide quick answers**: Lead with the direct answer, then elaborate
|
||||
3. **Link related FAQs**: Help users discover related information
|
||||
4. **Review regularly**: Keep answers current and accurate
|
||||
5. **Track what's helpful**: Use feedback to improve frequently accessed FAQs
|
||||
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# How-To Guide Database
|
||||
|
||||
**Purpose**: Procedural documentation for common tasks.
|
||||
|
||||
## Schema
|
||||
|
||||
| Property | Type | Options | Purpose |
|
||||
|----------|------|---------|---------|
|
||||
| **Title** | title | - | "How to [Task]" |
|
||||
| **Complexity** | select | Beginner, Intermediate, Advanced | Skill level required |
|
||||
| **Time Required** | number | - | Estimated minutes to complete |
|
||||
| **Prerequisites** | relation | Links to other guides | Required knowledge |
|
||||
| **Category** | select | Development, Deployment, Testing, Tools | Task category |
|
||||
| **Last Tested** | date | - | When procedure was verified |
|
||||
| **Tags** | multi_select | - | Technology/tool tags |
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
Create how-to guides with properties:
|
||||
{
|
||||
"Title": "How to Set Up Local Development Environment",
|
||||
"Complexity": "Beginner",
|
||||
"Time Required": 30,
|
||||
"Category": "Development",
|
||||
"Last Tested": "2025-10-01",
|
||||
"Tags": "setup, environment, docker"
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use consistent naming**: Always start with "How to..."
|
||||
2. **Test procedures**: Verify steps work before publishing
|
||||
3. **Include time estimates**: Help users plan their time
|
||||
4. **Link prerequisites**: Make dependencies clear
|
||||
5. **Update regularly**: Re-test procedures when tools/systems change
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue