feat(ai): upgrade Chirpy on-device AI with proper APIs, download UX, and streaming (#5579)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-22 16:51:02 -07:00
committed by GitHub
parent 5ec6d80f61
commit d870141b7c
132 changed files with 15131 additions and 150 deletions

View File

@@ -0,0 +1,60 @@
---
description: Generates code and provides documentation for the Genkit Dart SDK. Use when the user asks to build AI agents in Dart, use Genkit flows, or integrate LLMs into Dart/Flutter applications.
metadata:
genkit-managed: true
github-path: skills/developing-genkit-dart
github-ref: refs/heads/main
github-repo: https://github.com/firebase/agent-skills
github-tree-sha: 19d3a74e1de90200cc076d9b8bc31ae5c2127e7b
name: developing-genkit-dart
---
# Genkit Dart
Genkit Dart is an AI SDK for Dart that provides a unified interface for code generation, structured outputs, tools, flows, and AI agents.
## Core Features and Usage
If you need help with initializing Genkit (`Genkit()`), Generation (`ai.generate`), Tooling (`ai.defineTool`), Flows (`ai.defineFlow`), Embeddings (`ai.embedMany`), streaming, or calling remote flow endpoints, please load the core framework reference:
[references/genkit.md](references/genkit.md)
## Genkit CLI (recommended)
The Genkit CLI provides a local development UI for running Flow, tracing executions, playing with models, and evaluating outputs.
check if the user has it installed: `genkit --version`
**Installation:**
```bash
curl -sL cli.genkit.dev | bash # Native CLI
# OR
npm install -g genkit-cli # Via npm
```
**Usage:**
Wrap your run command with `genkit start` to attach the Genkit developer UI and tracing:
```bash
genkit start -- dart run main.dart
```
## Plugin Ecosystem
Genkit relies on a large suite of plugins to perform generative AI actions, interface with external LLMs, or host web servers.
When asked to use any given plugin, always verify usage by referring to its corresponding reference below. You should load the reference when you need to know the specific initialization arguments, tools, models, and usage patterns for the plugin:
| Plugin Name | Reference Link | Description |
| ---- | ---- | ---- |
| `genkit_google_genai` | [references/genkit_google_genai.md](references/genkit_google_genai.md) | Load for Google Gemini plugin interface usage. |
| `genkit_anthropic` | [references/genkit_anthropic.md](references/genkit_anthropic.md) | Load for Anthropic plugin interface for Claude models. |
| `genkit_openai` | [references/genkit_openai.md](references/genkit_openai.md) | Load for OpenAI plugin interface for GPT models, Groq, and custom compatible endpoints. |
| `genkit_middleware` | [references/genkit_middleware.md](references/genkit_middleware.md) | Load for Tooling for specific agentic behavior: `filesystem`, `skills`, and `toolApproval` interrupts. |
| `genkit_mcp` | [references/genkit_mcp.md](references/genkit_mcp.md) | Load for Model Context Protocol integration (Server, Host, and Client capabilities). |
| `genkit_chrome` | [references/genkit_chrome.md](references/genkit_chrome.md) | Load for Running Gemini Nano locally inside the Chrome browser using the Prompt API. |
| `genkit_shelf` | [references/genkit_shelf.md](references/genkit_shelf.md) | Load for Integrating Genkit Flow actions over HTTP using Dart Shelf. |
| `genkit_firebase_ai` | [references/genkit_firebase_ai.md](references/genkit_firebase_ai.md) | Load for Firebase AI plugin interface (Gemini API via Vertex AI). |
## External Dependencies
Whenever you define schemas mapping inside of Tools, Flows, and Prompts, you must use the [schemantic](https://pub.dev/packages/schemantic) library.
To learn how to use schemantic, ensure you read [references/schemantic.md](references/schemantic.md) for how to implement type safe generated Dart code. This is particularly relevant when you encounter symbols like `@Schema()`, `SchemanticType`, or classes with the `$` prefix. Genkit Dart uses schemantic for all of its data models so it's a CRITICAL skill to understand for using Genkit Dart.
## Best Practices
- Always check that code cleanly compiles using `dart analyze` before generating the final response.
- Always use the Genkit CLI for local development and debugging.

View File

@@ -0,0 +1,380 @@
# Genkit Core Framework
Genkit Dart is an AI SDK for Dart that provides a unified interface for text generation, structured output, tool calling, and agentic workflows.
## Initialization
```dart
import 'package:genkit/genkit.dart';
import 'package:genkit_google_genai/genkit_google_genai.dart'; // Or any other plugin
void main() async {
// Pass plugins to use into the Genkit constructor
final ai = Genkit(plugins: [googleAI()]);
}
```
## Generate Text
```dart
final response = await ai.generate(
model: googleAI.gemini('gemini-2.5-flash'), // Needs a model reference from a plugin
prompt: 'Explain quantum computing in simple terms.',
);
print(response.text);
```
## Stream Responses
```dart
final stream = ai.generateStream(
model: googleAI.gemini('gemini-2.5-flash'),
prompt: 'Write a short story about a robot learning to paint.',
);
await for (final chunk in stream) {
print(chunk.text);
}
```
## Embed Text
```dart
final embeddings = await ai.embedMany(
documents: [
DocumentData(content: [TextPart(text: 'Hello world')]),
],
embedder: googleAI.textEmbedding('text-embedding-004'),
);
print(embeddings.first.embedding);
```
## Define Tools
Models can use define actions and access external data via custom defined tools.
Requires the `schemantic` library for schema definitions.
```dart
import 'package:schemantic/schemantic.dart';
@Schema()
abstract class $WeatherInput {
String get location;
}
final weatherTool = ai.defineTool(
name: 'getWeather',
description: 'Gets the current weather for a location',
inputSchema: WeatherInput.$schema,
fn: (input, _) async {
// Call your weather API here
return 'Weather in ${input.location}: 72°F and sunny';
},
);
final response = await ai.generate(
model: googleAI.gemini('gemini-2.5-flash'),
prompt: 'What\'s the weather like in San Francisco?',
toolNames: ['getWeather'], // Use the tools
);
```
## Structured Output
You can ensure the generative model returns a typed JSON object by providing an `outputSchema`.
```dart
@Schema()
abstract class $Person {
String get name;
int get age;
}
// ... inside main ...
final response = await ai.generate(
model: googleAI.gemini('gemini-2.5-flash'),
prompt: 'Generate a person named John Doe, age 30',
outputSchema: Person.$schema, // Force the model to return this schema
);
final person = response.output; // Typed Person object
print('Name: ${person.name}, Age: ${person.age}');
```
## Define Flows
Wrap your AI logic in flows for better observability, testing, and deployment:
```dart
final jokeFlow = ai.defineFlow(
name: 'tellJoke',
inputSchema: .string(),
outputSchema: .string(),
fn: (topic, _) async {
final response = await ai.generate(
model: googleAI.gemini('gemini-2.5-flash'),
prompt: 'Tell me a joke about $topic',
);
return response.text; // Value return
},
);
final joke = await jokeFlow('programming');
print(joke);
```
### Streaming Flows
Stream data from your flows using `context.sendChunk(...)` and returning the final value:
```dart
final streamStory = ai.defineFlow(
name: 'streamStory',
inputSchema: .string(),
outputSchema: .string(),
streamSchema: .string(),
fn: (topic, context) async {
final stream = ai.generateStream(
model: googleAI.gemini('gemini-2.5-flash'),
prompt: 'Write a story about $topic',
);
await for (final chunk in stream) {
context.sendChunk(chunk.text); // Stream the chunks
}
return 'Story complete'; // Value return
},
);
```
## Calling remote Flows from a dart client
The `genkit` package provides `package:genkit/client.dart` representing remote Genkit actions that can be invoked or streamed using type-safe definitions.
1. Defines a remote action
```dart
import 'package:genkit/client.dart';
final stringAction = defineRemoteAction(
url: 'http://localhost:3400/my-flow',
inputSchema: .string(),
outputSchema: .string(),
);
```
2. Call the Remote Action (Non-streaming)
```dart
final response = await stringAction(input: 'Hello from Dart!');
print('Flow Response: $response');
```
3. Call the Remote Action (Streaming)
Use the `.stream()` method on the action flow, and access `stream.onResult` to wait on the async return value.
```dart
final streamAction = defineRemoteAction(
url: 'http://localhost:3400/stream-story',
inputSchema: .string(),
outputSchema: .string(),
streamSchema: .string(),
);
final stream = streamAction.stream(
input: 'Tell me a short story about a Dart developer.',
);
await for (final chunk in stream) {
print('Chunk: $chunk');
}
final finalResult = await stream.onResult;
print('\nFinal Response: $finalResult');
```
## Calling remote Flows from a Javascript client
Install `genkit` npm package:
```bash
npm install genkit
```
1. Call a remote flow (non-streaming)
```ts
import { runFlow } from 'genkit/beta/client';
async function callHelloFlow() {
try {
const result = await runFlow({
url: 'http://127.0.0.1:3400/helloFlow', // Replace with your deployed flow's URL
input: { name: 'Genkit User' },
});
console.log('Non-streaming result:', result.greeting);
} catch (error) {
console.error('Error calling helloFlow:', error);
}
}
callHelloFlow();
```
2. Call a remote flow (streaming)
```ts
import { streamFlow } from 'genkit/beta/client';
async function streamHelloFlow() {
try {
const result = streamFlow({
url: 'http://127.0.0.1:3400/helloFlow', // Replace with your deployed flow's URL
input: { name: 'Streaming User' },
});
// Process the stream chunks as they arrive
for await (const chunk of result.stream) {
console.log('Stream chunk:', chunk);
}
// Get the final complete response
const finalOutput = await result.output;
console.log('Final streaming output:', finalOutput.greeting);
} catch (error) {
console.error('Error streaming helloFlow:', error);
}
}
streamHelloFlow();
```
## Data Models
Genkit uses standard data models for representing prompts (messages & parts) and responses. These classes are implemented using schemantic library.
```dart
import 'package:genkit/genkit.dart';
import 'package:schemantic/schemantic.dart';
@Schema()
abstract class $MyDataModel {
// uses Genkit's Message schema (not schemantic's Message)
List<$Message> get messages;
List<$Part> get parts;
}
void example() {
// --- Parts ---
// A Text part
final textPart = TextPart(text: 'some text', metadata: {'foo': 'bar'});
// A Media/Image part
final mediaPart = MediaPart(
media: Media(url: 'https://...', contentType: 'image/png'),
metadata: {'foo': 'bar'},
);
// A Tool Request initiated by the model
final toolRequestPart = ToolRequestPart(
toolRequest: ToolRequest(
name: 'get_weather',
ref: 'abc',
input: {'location': 'Paris, France'},
),
metadata: {'foo': 'bar'},
);
// The resulting data from a Tool execution
final toolResponsePart = ToolResponsePart(
toolResponse: ToolResponse(
name: 'get_weather',
ref: 'abc',
output: {'temperature': '20C'},
),
metadata: {'foo': 'bar'},
);
// Model reasoning (e.g. for Claude's "thinking" models)
final reasoningPart = ReasoningPart(
reasoning: 'thinking...',
metadata: {'foo': 'bar'},
);
// A custom fallback part
final customPart = CustomPart(
custom: {'provider': {'specific': 'data'}},
metadata: {'foo': 'bar'},
);
// --- Messages ---
final systemMessage = Message(
role: Role.system,
content: [textPart, mediaPart],
metadata: {'foo': 'bar'},
);
final userMessage = Message(
role: Role.user,
content: [textPart, mediaPart], // Can contain media (multimodal)
);
final modelMessage = Message(
role: Role.model,
// Models can emit text, tool requests, reasoning, or custom parts
content: [textPart, toolRequestPart, reasoningPart, customPart],
);
// --- Ergonomic Data Access (schema_extensions.dart) ---
// The Genkit SDK provides extensions on `Message` and `Part` to easily access fields
// without needing to cast them manually.
// Get concatenated text from all TextParts in a Message
print(modelMessage.text);
// Get the first Media object from a Message
print(modelMessage.media?.url);
// Iterate over tool requests in a Message
for (final toolReq in modelMessage.toolRequests) {
print(toolReq.name);
}
// Inspect individual parts
for (final part in modelMessage.content) {
if (part.isText) print(part.text);
if (part.isMedia) print(part.media?.url);
if (part.isToolRequest) print(part.toolRequest?.name);
if (part.isToolResponse) print(part.toolResponse?.name);
if (part.isReasoning) print(part.reasoning);
if (part.isCustom) print(part.custom);
}
// --- Streaming Chunks ---
// Data emitted by ai.generateStream() calls
final generateResponseChunk = ModelResponseChunk(
content: [textPart],
index: 0, // Index of the message this chunk belongs to
aggregated: false,
);
// Chunks also have text and media accessors
print(generateResponseChunk.text);
// --- Advanced: Schemas ---
// Use Genkit type schemas directly in Schemantic validations
final messageSchema = Message.$schema;
final partSchema = Part.$schema;
final mySchema = SchemanticType.map(
.string(),
.list(Message.$schema), // Requires a list of Messages
);
// --- Generate Response ---
// ai.generate() returns a GenerateResponseHelper which provides ergonomic getters
// over the underlying ModelResponse:
final response = await ai.generate(...);
print(response.text); // Concatenated text
print(response.media?.url); // First media part
print(response.toolRequests); // All tool requests
print(response.interrupts); // Tool requests that triggered an interrupt
print(response.messages); // Full history of the conversation, including the request and response
print(response.output); // Structured typed output (if outputSchema was used)
}
```

View File

@@ -0,0 +1,41 @@
# Genkit Anthropic Plugin (`genkit_anthropic`)
The Anthropic plugin for Genkit Dart, used for interacting with the Claude models.
## Usage
Requires `ANTHROPIC_API_KEY` to be passed to the init block.
```dart
import 'dart:io';
import 'package:genkit/genkit.dart';
import 'package:genkit_anthropic/genkit_anthropic.dart';
void main() async {
final ai = Genkit(
plugins: [anthropic(apiKey: Platform.environment['ANTHROPIC_API_KEY']!)],
);
final response = await ai.generate(
model: anthropic.model('claude-sonnet-4-5'),
prompt: 'Tell me a joke about a developer.',
);
print(response.text);
}
```
## Claude Thinking Configurations
Provides specific configurations for utilizing Claude 3.7+ "thinking" model capabilities.
```dart
final response = await ai.generate(
model: anthropic.model('claude-sonnet-4-5'),
prompt: 'Solve this 24 game: 2, 3, 10, 10',
config: AnthropicOptions(thinking: ThinkingConfig(budgetTokens: 2048)),
);
// The thinking content is available in the message parts
print(response.message?.content);
```

View File

@@ -0,0 +1,23 @@
# Genkit Chrome AI Plugin (`genkit_chrome`)
Chrome Built-in AI (Gemini Nano) plugin for Genkit Dart, allowing local offline execution within a Chrome application.
## Usage
```dart
import 'package:genkit/genkit.dart';
import 'package:genkit_chrome/genkit_chrome.dart';
void main() async {
final ai = Genkit(plugins: [ChromeAIPlugin()]);
final stream = ai.generateStream(
model: modelRef('chrome/gemini-nano'),
prompt: 'Write a story about a robot.',
);
await for (final chunk in stream) {
print(chunk.text);
}
}
```

View File

@@ -0,0 +1,23 @@
# Genkit Firebase AI Plugin (`genkit_firebase_ai`)
The Firebase AI plugin for Genkit Dart, used for interacting with Gemini APIs through Firebase AI Logic.
## Usage
```dart
import 'package:genkit/genkit.dart';
import 'package:genkit_firebase_ai/genkit_firebase_ai.dart';
void main() async {
// Initialize Genkit with the Firebase AI plugin
final ai = Genkit(plugins: [firebaseAI()]);
// Generate text
final response = await ai.generate(
model: firebaseAI.gemini('gemini-2.5-flash'),
prompt: 'Tell me a joke about a developer.',
);
print(response.text);
}
```

View File

@@ -0,0 +1,95 @@
# Genkit Google GenAI Plugin (`genkit_google_genai`)
The Google AI plugin provides an interface against the official Google AI Gemini API.
## Usage
```dart
import 'package:genkit/genkit.dart';
import 'package:genkit_google_genai/genkit_google_genai.dart';
void main() async {
// Initialize Genkit with the Google AI plugin
final ai = Genkit(plugins: [googleAI()]);
// Generate text
final response = await ai.generate(
model: googleAI.gemini('gemini-2.5-flash'),
prompt: 'Tell me a joke about a developer.',
);
print(response.text);
}
```
## Embeddings
```dart
final embeddings = await ai.embedMany(
embedder: googleAI.textEmbedding('text-embedding-004'),
documents: [
DocumentData(content: [TextPart(text: 'Hello world')]),
],
);
```
## Image Generation
The plugin also supports image generation models such as `gemini-2.5-flash-image`.
### Example (Nano Banana)
```dart
// Define an image generation flow
ai.defineFlow(
name: 'imageGenerator',
inputSchema: .string(defaultValue: 'A banana riding a bike'),
outputSchema: Media.$schema,
fn: (input, context) async {
final response = await ai.generate(
model: googleAI.gemini('gemini-2.5-flash-image'),
prompt: input,
);
if (response.media == null) {
throw Exception('No media generated');
}
return response.media!;
},
);
```
The media (url field) contain base64 encoded data uri. You can decode it and save it as a file.
## Text-to-Speech (TTS)
You can use text-to-speech models to generate audio from text. The generated `Media` object will contain base64 encoded PCM audio in its data URI.
```dart
// Define a TTS flow
ai.defineFlow(
name: 'textToSpeech',
inputSchema: .string(defaultValue: 'Genkit is an amazing AI framework!'),
outputSchema: Media.$schema,
fn: (prompt, _) async {
final response = await ai.generate(
model: googleAI.gemini('gemini-2.5-flash-preview-tts'),
prompt: prompt,
config: GeminiTtsOptions(
responseModalities: ['AUDIO'],
speechConfig: SpeechConfig(
voiceConfig: VoiceConfig(
prebuiltVoiceConfig: PrebuiltVoiceConfig(voiceName: 'Puck'),
),
),
),
);
if (response.media != null) {
return response.media!;
}
throw Exception('No audio generated');
},
);
```
Google AI also supports multi-speaker TTS by configuring a `MultiSpeakerVoiceConfig` inside `SpeechConfig`.

View File

@@ -0,0 +1,115 @@
# Genkit MCP (`genkit_mcp`)
MCP (Model Context Protocol) integration for Genkit Dart.
## MCP Host (Recommended)
Connect to one or more MCP servers and aggregate their capabilities into the Genkit registry automatically.
```dart
import 'package:genkit/genkit.dart';
import 'package:genkit_mcp/genkit_mcp.dart';
void main() async {
final ai = Genkit();
final host = defineMcpHost(
ai,
McpHostOptionsWithCache(
name: 'my-host',
mcpServers: {
'fs': McpServerConfig(
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '.'],
),
},
),
);
// Tools can be discovered and executed dynamically using a wildcard...
final response = await ai.generate(
model: 'gemini-2.5-flash',
prompt: 'Summarize the contents of README.md',
toolNames: ['my-host:tool/fs/*'],
);
// ...or by specifying the exact tool name
final exactResponse = await ai.generate(
model: 'gemini-2.5-flash',
prompt: 'Read README.md',
toolNames: ['my-host:tool/fs/read_file'],
);
}
```
## MCP Client (Advanced / Single Server)
Connecting to a single MCP server with a client object is an advanced usecase for when you need manual control over the client lifecycle. Standalone clients do not automatically register tools into the registry, so they must be passed into `generate` or `defineDynamicActionProvider` manually.
```dart
import 'package:genkit/genkit.dart';
import 'package:genkit_mcp/genkit_mcp.dart';
void main() async {
final ai = Genkit();
final client = createMcpClient(
McpClientOptions(
name: 'my-client',
mcpServer: McpServerConfig(
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '.'],
),
),
);
await client.ready();
// Retrieve the tools from the connected client
final tools = await client.getActiveTools(ai);
final response = await ai.generate(
model: 'gemini-2.5-flash',
prompt: 'Read the contents of README.md',
tools: tools,
);
}
```
## MCP Server
Expose Genkit actions (tools, prompts, resources) over MCP.
```dart
import 'package:genkit/genkit.dart';
import 'package:genkit_mcp/genkit_mcp.dart';
void main() async {
final ai = Genkit();
ai.defineTool(
name: 'add',
description: 'Add two numbers together',
inputSchema: .map(.string(), .dynamicSChema()),
fn: (input, _) async => (input['a'] + input['b']).toString(),
);
ai.defineResource(
name: 'my-resource',
uri: 'my://resource',
fn: (_, _) async => ResourceOutput(content: [TextPart(text: 'my resource')]),
);
// Stdio transport by default
final server = createMcpServer(ai, McpServerOptions(name: 'my-server'));
await server.start();
}
```
### Streamable HTTP Transport
```dart
import 'dart:io';
final transport = await StreamableHttpServerTransport.bind(
address: InternetAddress.loopbackIPv4,
port: 3000,
);
await server.start(transport);
```

View File

@@ -0,0 +1,84 @@
# Genkit Middleware (`genkit_middleware`)
A collection of useful middleware for Genkit Dart to enhance your agent's capabilities. Register plugins when initializing Genkit:
```dart
import 'package:genkit/genkit.dart';
import 'package:genkit_middleware/genkit_middleware.dart';
void main() {
final ai = Genkit(
plugins: [
FilesystemPlugin(),
SkillsPlugin(),
ToolApprovalPlugin(),
],
);
}
```
## Filesystem Middleware
Allows the agent to list, read, write, and search/replace files within a restricted root directory.
```dart
final response = await ai.generate(
prompt: 'Check the logs in the current directory.',
use: [
filesystem(rootDirectory: '/path/to/secure/workspace'),
],
);
```
**Tools Provided:**
- `list_files`, `read_file`, `write_file`, `search_and_replace`
## Skills Middleware
Injects specialized instructions (skills) into the system prompt from `SKILL.md` files located in specified directories.
```dart
final response = await ai.generate(
prompt: 'Help me debug this issue.',
use: [
skills(skillPaths: ['/path/to/skills']),
],
);
```
**Tools Provided:**
- `use_skill`: Retrieve the full content of a skill by name.
## Tool Approval Middleware
Intercepts tool execution for specified tools and requires explicit approval. Returns `FinishReason.interrupted`.
```dart
final response = await ai.generate(
prompt: 'Delete the database.',
use: [
// Require approval for all tools EXCEPT those below
toolApproval(approved: ['read_file', 'list_files']),
],
);
if (response.finishReason == FinishReason.interrupted) {
final interrupt = response.interrupts.first;
// Ask user for approval
final isApproved = await askUser();
if (isApproved) {
final resumeResponse = await ai.generate(
messages: response.messages, // Pass history
toolChoice: ToolChoice.none, // Prevent immediate re-call
interruptRestart: [
ToolRequestPart(
toolRequest: interrupt.toolRequest,
metadata: {
...?interrupt.metadata,
'tool-approved': true
},
),
],
);
}
}
```

View File

@@ -0,0 +1,54 @@
# Genkit OpenAI Plugin (`genkit_openai`)
OpenAI-compatible API plugin for Genkit Dart. Supports OpenAI models and other compatible APIs (xAI, DeepSeek, Together AI, Groq, etc.).
## Basic Usage
```dart
import 'dart:io';
import 'package:genkit/genkit.dart';
import 'package:genkit_openai/genkit_openai.dart';
void main() async {
final ai = Genkit(plugins: [
openAI(apiKey: Platform.environment['OPENAI_API_KEY']),
]);
final response = await ai.generate(
model: openAI.model('gpt-4o'),
prompt: 'Tell me a joke.',
);
}
```
## Options
`OpenAIOptions` allows configuring sampling temperature, nucleus sampling, token generation, seed, etc:
`config: OpenAIOptions(temperature: 0.7, maxTokens: 100)`
## Groq API override
Specify custom `baseUrl` and custom models to integrate with third-party providers.
```dart
final ai = Genkit(plugins: [
openAI(
apiKey: Platform.environment['GROQ_API_KEY'],
baseUrl: 'https://api.groq.com/openai/v1',
models: [
CustomModelDefinition(
name: 'llama-3.3-70b-versatile',
info: ModelInfo(
label: 'Llama 3.3 70B',
supports: {'multiturn': true, 'tools': true, 'systemRole': true},
),
),
],
),
]);
final response = await ai.generate(
model: openAI.model('llama-3.3-70b-versatile'),
prompt: 'Hello!',
);
```

View File

@@ -0,0 +1,59 @@
# Genkit Shelf Plugin (`genkit_shelf`)
Shelf integration for Genkit Dart, used to serve Genkit Flows.
## Standalone Server
Serve Genkit Flows easily on an isolated HTTP server using `startFlowServer`.
```dart
import 'package:genkit/genkit.dart';
import 'package:genkit_shelf/genkit_shelf.dart';
void main() async {
final ai = Genkit();
final flow = ai.defineFlow(
name: 'myFlow',
inputSchema: .string(),
outputSchema: .string(),
fn: (String input, _) async => 'Hello $input',
);
await startFlowServer(
flows: [flow],
port: 8080,
);
}
```
## Existing Shelf Application
Mount Genkit Flow endpoints directly to an existing Shelf `Router` using `shelfHandler`.
```dart
import 'package:genkit/genkit.dart';
import 'package:genkit_shelf/genkit_shelf.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';
void main() async {
final ai = Genkit();
final flow = ai.defineFlow(
name: 'myFlow',
inputSchema: .string(),
outputSchema: .string(),
fn: (String input, _) async => 'Hello $input',
);
final router = Router();
// Mount the flow handler at a specific path
router.post('/myFlow', shelfHandler(flow));
// Start the server
await io.serve(router.call, 'localhost', 8080);
}
```
Access deployed flows using genkit client libraries (from Dart or JS).

View File

@@ -0,0 +1,137 @@
# Schemantic
Schemantic is a general-purpose Dart library used for defining strongly typed data classes that automatically bind to reusable runtime JSON schemas. It is standard for the `genkit-dart` framework but works independently as well.
## Core Concepts
Always use `schemantic` when strongly typed JSON parsing or programmatic schema validation is required.
- Annotate your abstract classes with `@Schema()`.
- Use the `$` prefix for abstract schema class names (e.g., `abstract class $User`).
- Always run `dart run build_runner build` to generate the `.g.dart` schema files.
## Installation
Add dependencies:
```bash
dart pub add schemantic
```
## Basic Usage
1. **Defining a schema:**
```dart
import 'package:schemantic/schemantic.dart';
part 'my_file.g.dart'; // Must match the filename
@Schema()
abstract class $MyObj {
String get name;
$MySubObj get subObj;
}
@Schema()
abstract class $MySubObj {
String get foo;
}
```
2. **Using the Generated Class:**
The builder creates a concrete class `MyObj` (no `$`) with a factory constructor (`MyObj.fromJson`) and a regular constructor.
```dart
// Creating an instance
final obj = MyObj(name: 'test', subObj: MySubObj(foo: 'bar'));
// Serializing to JSON
print(obj.toJson());
// Parsing from JSON
final parsed = MyObj.fromJson({'name': 'test', 'subObj': {'foo': 'bar'}});
```
3. **Accessing Schemas at Runtime:**
The generated data classes have a static `$schema` field (of type `SchemanticType<T>`) which can be used to pass the definition into functions or to extract the raw JSON schema.
```dart
// Access JSON schema
final schema = MyObj.$schema.jsonSchema;
print(schema.toJson());
// Validate arbitrary JSON at runtime
final validationErrors = await schema.validate({'invalid': 'data'});
```
## Primitive Schemas
When a full data class is not required, Schemantic provides functions to create schemas dynamically.
```dart
final ageSchema = SchemanticType.integer(description: 'Age in years', minimum: 0);
final nameSchema = SchemanticType.string(minLength: 2);
final nothingSchema = SchemanticType.voidSchema();
final anySchema = SchemanticType.dynamicSchema();
final userSchema = SchemanticType.map(.string(), .integer()); // Map<String, int>
final tagsSchema = SchemanticType.list(.string()); // List<String>
```
## Union Types (AnyOf)
To allow a field to accept multiple types, use `@AnyOf`.
```dart
@Schema()
abstract class $Poly {
@AnyOf([int, String, $MyObj])
Object? get id;
}
```
Schemantic generates a specific helper class (e.g., `PolyId`) to handle the values:
```dart
final poly1 = Poly(id: PolyId.int(123));
final poly2 = Poly(id: PolyId.string('abc'));
```
## Field Annotations
You can use specialized annotations for more validation boundaries:
```dart
@Schema()
abstract class $User {
@IntegerField(
name: 'years_old', // Change JSON key
description: 'Age of the user',
minimum: 0,
defaultValue: 18,
)
int? get age;
@StringField(
minLength: 2,
enumValues: ['user', 'admin'],
)
String get role;
}
```
## Recursive Schemas
For recursive structures (like trees), must use `useRefs: true` inside the generated jsonSchema property. You define it normally:
```dart
@Schema()
abstract class $Node {
String get id;
List<$Node>? get children;
}
```
*Note*: `Node.$schema.jsonSchema(useRefs: true)` generates schemas with JSON Schema `$ref`.

View File

@@ -0,0 +1,102 @@
---
description: Develop AI-powered applications using Genkit in Go. Use when the user asks to build AI features, agents, flows, or tools in Go using Genkit, or when working with Genkit Go code involving generation, prompts, streaming, tool calling, or model providers.
metadata:
genkit-managed: true
github-path: skills/developing-genkit-go
github-ref: refs/heads/main
github-repo: https://github.com/firebase/agent-skills
github-tree-sha: 551cc4211a9782a7237da5e762ea8ca7c3bf5eec
name: developing-genkit-go
---
# Genkit Go
Genkit Go is an AI SDK for Go that provides generation, structured output, streaming, tool calling, prompts, and flows with a unified interface across model providers.
## Hello World
```go
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/genkit-ai/genkit/go/ai"
"github.com/genkit-ai/genkit/go/genkit"
"github.com/genkit-ai/genkit/go/plugins/googlegenai"
"github.com/genkit-ai/genkit/go/plugins/server"
)
func main() {
ctx := context.Background()
g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{}))
genkit.DefineFlow(g, "jokeFlow", func(ctx context.Context, topic string) (string, error) {
return genkit.GenerateText(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell me a joke about %s", topic),
)
})
mux := http.NewServeMux()
for _, f := range genkit.ListFlows(g) {
mux.HandleFunc("POST /"+f.Name(), genkit.Handler(f))
}
log.Fatal(server.Start(ctx, "127.0.0.1:8080", mux))
}
```
## Core Features
Load the appropriate reference based on what you need:
| Feature | Reference | When to load |
| --- | --- | --- |
| Initialization | [references/getting-started.md](references/getting-started.md) | Setting up `genkit.Init`, plugins, the `*Genkit` instance pattern |
| Generation | [references/generation.md](references/generation.md) | `Generate`, `GenerateText`, `GenerateData`, streaming, output formats |
| Prompts | [references/prompts.md](references/prompts.md) | `DefinePrompt`, `DefineDataPrompt`, `.prompt` files, schemas |
| Tools | [references/tools.md](references/tools.md) | `DefineTool`, tool interrupts, `RestartWith`/`RespondWith` |
| Middleware | [references/middleware.md](references/middleware.md) | `ai.Middleware`, `ai.WithUse`, `Hooks` (Generate/Model/Tool), built-ins (`Retry`, `Fallback`, `ToolApproval`, `Filesystem`, `Skills`) |
| Flows & HTTP | [references/flows-and-http.md](references/flows-and-http.md) | `DefineFlow`, `DefineStreamingFlow`, `genkit.Handler`, HTTP serving |
| Model Providers | [references/providers.md](references/providers.md) | Google AI, Vertex AI, Anthropic, OpenAI-compatible, Ollama setup |
## Genkit CLI
Check if installed: `genkit --version`
**Installation:**
```bash
curl -sL cli.genkit.dev | bash
```
**Key commands:**
```bash
# Start app with Developer UI (tracing, flow testing) at http://localhost:4000
genkit start -- go run .
genkit start -o -- go run . # also opens browser
# Run a flow directly from the CLI
genkit flow:run myFlow '{"data": "input"}'
genkit flow:run myFlow '{"data": "input"}' --stream # with streaming
genkit flow:run myFlow '{"data": "input"}' --wait # wait for completion
# Look up Genkit documentation
genkit docs:search "streaming" go
genkit docs:list go
genkit docs:read go/flows.md
```
See [references/getting-started.md](references/getting-started.md) for full CLI and Developer UI details.
## Key Guidance
- **Pass `g` explicitly.** The `*Genkit` instance returned by `genkit.Init` is the central registry. Pass it to all Genkit functions rather than storing it as a global. This is a core pattern throughout the SDK.
- **Wrap AI logic in flows.** Flows give you tracing, observability, HTTP deployment via `genkit.Handler`, and the ability to test from the Developer UI and CLI. Any generation call worth keeping should live in a flow.
- **Use `jsonschema:"description=..."` struct tags on output types.** The model uses these descriptions to understand what each field should contain. Without them, structured output quality drops significantly.
- **Write good tool descriptions.** The model decides which tools to call based on their description string. Vague descriptions lead to missed or incorrect tool calls.
- **Use `.prompt` files for complex prompts.** They separate prompt content from Go code, support Handlebars templating, and can be iterated on without recompilation. Code-defined prompts are better for simple, single-line cases.
- **Reach for built-in middleware before writing one.** `Retry`, `Fallback`, `ToolApproval`, `Filesystem`, and `Skills` cover the common cross-cutting needs and compose with each other via `ai.WithUse`. See [references/middleware.md](references/middleware.md). When you do write custom middleware, allocate per-call state in closures captured by `New`, and guard anything that `WrapTool` mutates because tools may run concurrently.
- **Look up the latest model IDs.** Model names change frequently. Check provider documentation for current model IDs rather than relying on hardcoded names. See [references/providers.md](references/providers.md).

View File

@@ -0,0 +1,183 @@
# Flows & HTTP
## DefineFlow
Wrap AI logic in a flow for observability, tracing, and HTTP deployment.
```go
jokeFlow := genkit.DefineFlow(g, "jokeFlow",
func(ctx context.Context, topic string) (string, error) {
return genkit.GenerateText(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell me a joke about %s", topic),
)
},
)
```
### Running a Flow Directly
```go
result, err := jokeFlow.Run(ctx, "cats")
```
## DefineStreamingFlow
Flows that stream chunks back to the caller. Two common patterns:
### Pattern 1: Passthrough Streaming
Pass the stream callback directly through to `WithStreaming`. The callback type is `ai.ModelStreamCallback` = `func(context.Context, *ai.ModelResponseChunk) error`:
```go
genkit.DefineStreamingFlow(g, "streamingJokeFlow",
func(ctx context.Context, topic string, sendChunk ai.ModelStreamCallback) (string, error) {
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell me a long joke about %s", topic),
ai.WithStreaming(sendChunk), // passthrough
)
if err != nil {
return "", err
}
return resp.Text(), nil
},
)
```
### Pattern 2: Manual String Streaming
Use `core.StreamCallback[string]` to stream extracted text:
```go
genkit.DefineStreamingFlow(g, "streamingJokeFlow",
func(ctx context.Context, topic string, sendChunk core.StreamCallback[string]) (string, error) {
stream := genkit.GenerateStream(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell me a long joke about %s", topic),
)
for result, err := range stream {
if err != nil {
return "", err
}
if result.Done {
return result.Response.Text(), nil
}
sendChunk(ctx, result.Chunk.Text())
}
return "", nil
},
)
```
### Typed Streaming Flows
Use `core.StreamCallback[T]` with `GenerateDataStream` for typed chunks:
```go
genkit.DefineStreamingFlow(g, "structuredStream",
func(ctx context.Context, input JokeRequest, sendChunk core.StreamCallback[*Joke]) (*Joke, error) {
stream := genkit.GenerateDataStream[*Joke](ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell me a joke about %s", input.Topic),
)
for result, err := range stream {
if err != nil { return nil, err }
if result.Done { return result.Output, nil }
sendChunk(ctx, result.Chunk)
}
return nil, nil
},
)
```
## Named Sub-Steps
Use `core.Run` inside a flow for traced sub-steps:
```go
genkit.DefineFlow(g, "pipeline",
func(ctx context.Context, input string) (string, error) {
subject, err := core.Run(ctx, "extract-subject", func() (string, error) {
return genkit.GenerateText(ctx, g,
ai.WithPrompt("Extract the subject from: %s", input),
)
})
if err != nil { return "", err }
joke, err := core.Run(ctx, "generate-joke", func() (string, error) {
return genkit.GenerateText(ctx, g,
ai.WithPrompt("Tell me a joke about %s", subject),
)
})
return joke, err
},
)
```
## HTTP Handlers
### genkit.Handler
Convert any flow into an `http.HandlerFunc`:
```go
mux := http.NewServeMux()
for _, f := range genkit.ListFlows(g) {
mux.HandleFunc("POST /"+f.Name(), genkit.Handler(f))
}
log.Fatal(server.Start(ctx, "127.0.0.1:8080", mux))
```
### Request/Response Format
**Non-streaming request:**
```bash
curl -X POST http://localhost:8080/jokeFlow \
-H "Content-Type: application/json" \
-d '{"data": "bananas"}'
```
Response: `{"result": "Why did the banana go to the doctor?..."}`
**Streaming request:**
```bash
curl -N -X POST http://localhost:8080/streamingJokeFlow \
-H "Content-Type: application/json" \
-d '{"data": "bananas"}'
```
Streaming responses use Server-Sent Events (SSE) format.
### genkit.HandlerFunc
For frameworks that expect error-returning handlers:
```go
handler := genkit.HandlerFunc(myFlow)
// handler is func(http.ResponseWriter, *http.Request) error
```
### Context Providers
Inject request context (e.g., auth headers) into flow execution:
```go
mux.HandleFunc("POST /myFlow", genkit.Handler(myFlow,
genkit.WithContextProviders(func(ctx context.Context, rd core.RequestData) (api.ActionContext, error) {
// rd.Headers contains HTTP headers
return api.ActionContext{"userId": rd.Headers.Get("X-User-Id")}, nil
}),
))
```
### ListFlows
Get all registered flows for dynamic route setup:
```go
flows := genkit.ListFlows(g) // []api.Action
for _, f := range flows {
fmt.Println(f.Name())
}
```

View File

@@ -0,0 +1,176 @@
# Generation
## GenerateText
Simplest form. Returns a string.
```go
text, err := genkit.GenerateText(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell me a joke about %s", topic),
)
```
## Generate
Returns a full `*ModelResponse` with metadata, usage stats, and history.
```go
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithSystem("You are a helpful assistant."),
ai.WithPrompt("Explain %s", topic),
)
fmt.Println(resp.Text()) // concatenated text
fmt.Println(resp.FinishReason) // ai.FinishReasonStop, etc.
fmt.Println(resp.Usage) // token counts
```
## GenerateData (Structured Output)
Returns a typed Go value parsed from the model's JSON output.
```go
type Joke struct {
Setup string `json:"setup" jsonschema:"description=The setup of the joke"`
Punchline string `json:"punchline" jsonschema:"description=The punchline"`
}
joke, resp, err := genkit.GenerateData[Joke](ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell me a joke about %s", topic),
)
// joke is *Joke, resp is *ModelResponse
```
## Streaming
### GenerateStream
Returns an iterator. Each value has `.Done`, `.Chunk`, and `.Response`.
```go
stream := genkit.GenerateStream(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell me a long story about %s", topic),
)
for result, err := range stream {
if err != nil {
return err
}
if result.Done {
finalText := result.Response.Text()
break
}
fmt.Print(result.Chunk.Text()) // incremental text
}
```
### GenerateDataStream (Structured Streaming)
Streams typed partial objects as they arrive.
```go
stream := genkit.GenerateDataStream[Joke](ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell me a joke about %s", topic),
)
for result, err := range stream {
if err != nil {
return err
}
if result.Done {
finalJoke := result.Output // *Joke
break
}
partialJoke := result.Chunk // *Joke (partial)
}
```
### Callback-Based Streaming
Use `ai.WithStreaming` with `Generate` for callback-style streaming. The callback receives `*ai.ModelResponseChunk`:
```go
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell me a story"),
ai.WithStreaming(func(ctx context.Context, chunk *ai.ModelResponseChunk) error {
fmt.Print(chunk.Text()) // extract text from chunk
return nil
}),
)
// resp contains the final complete response
```
## Common Options
```go
// Model selection
ai.WithModel(googlegenai.ModelRef("googleai/gemini-flash-latest", nil)) // model reference
ai.WithModelName("googleai/gemini-flash-latest") // by name string
// Content
ai.WithPrompt("Tell me about %s", topic) // user message (supports fmt verbs)
ai.WithSystem("You are a pirate.") // system instructions
ai.WithMessages(msg1, msg2) // conversation history
ai.WithDocs(doc1, doc2) // context documents
ai.WithTextDocs("context 1", "context 2") // context as strings
// Model config (provider-specific)
ai.WithConfig(map[string]any{"temperature": 0.7})
```
## Output Formats
Control how the model structures its output.
### By Go Type
```go
// Automatically uses JSON format and instructs model to match the type
ai.WithOutputType(MyStruct{})
```
### By Format String
```go
ai.WithOutputFormat(ai.OutputFormatJSON) // single JSON object
ai.WithOutputFormat(ai.OutputFormatJSONL) // JSON Lines (one object per line)
ai.WithOutputFormat(ai.OutputFormatArray) // JSON array
ai.WithOutputFormat(ai.OutputFormatEnum) // constrained enum value
ai.WithOutputFormat(ai.OutputFormatText) // plain text (default)
```
### Enum Output
```go
type Color string
const (
Red Color = "red"
Green Color = "green"
Blue Color = "blue"
)
text, err := genkit.GenerateText(ctx, g,
ai.WithPrompt("What color is the sky?"),
ai.WithOutputEnums(Red, Green, Blue),
)
```
### Custom Output Instructions
```go
ai.WithOutputInstructions("Return a JSON object with fields: name (string), age (number)")
```
### Combining Format + Schema
```go
// JSONL with a typed schema (useful for streaming lists)
genkit.DefinePrompt(g, "characters",
ai.WithPrompt("Generate 5 story characters"),
ai.WithOutputType([]StoryCharacter{}),
ai.WithOutputFormat(ai.OutputFormatJSONL),
)
```

View File

@@ -0,0 +1,140 @@
# Getting Started
## Project Setup
```bash
mkdir my-genkit-app && cd my-genkit-app
go mod init my-genkit-app
go get github.com/genkit-ai/genkit/go@latest
```
Provider plugins ship in the same module under `plugins/`, so they don't need to be fetched separately. Just import the ones you want and run `go mod tidy` afterwards. The available plugins include:
- `plugins/googlegenai` for Google AI and Vertex AI
- `plugins/anthropic` for Anthropic Claude
- `plugins/compat_oai` for OpenAI-compatible APIs (OpenAI, Groq, xAI, etc.)
- `plugins/ollama` for local Ollama models
- `plugins/middleware` for the built-in middleware bundle (`Retry`, `Fallback`, `ToolApproval`, `Filesystem`, `Skills`)
## Initialization
Every Genkit app starts with `genkit.Init`, which returns a `*Genkit` instance:
```go
import (
"context"
"github.com/genkit-ai/genkit/go/genkit"
"github.com/genkit-ai/genkit/go/plugins/googlegenai"
)
ctx := context.Background()
g := genkit.Init(ctx,
genkit.WithPlugins(&googlegenai.GoogleAI{}),
)
```
### The `*Genkit` Instance
The `*Genkit` value `g` is the central registry. Pass it to every Genkit function:
```go
// Defining resources
genkit.DefineFlow(g, "myFlow", ...)
genkit.DefineTool(g, "myTool", ...)
genkit.DefinePrompt(g, "myPrompt", ...)
// Generating content
genkit.GenerateText(ctx, g, ...)
genkit.Generate(ctx, g, ...)
```
Do not store `g` in a global variable. Pass it explicitly through your call chain.
### Init Options
```go
g := genkit.Init(ctx,
// Register one or more plugins
genkit.WithPlugins(&googlegenai.GoogleAI{}, &anthropic.Anthropic{}),
// Set a default model (used when no model is specified)
genkit.WithDefaultModel("googleai/gemini-flash-latest"),
// Set directory for .prompt files (default: "prompts")
genkit.WithPromptDir("my-prompts"),
// Or embed prompts using Go's embed package
// genkit.WithPromptFS(promptsFS),
)
```
### Embedding Prompts
Use `go:embed` to bundle `.prompt` files into the binary:
```go
//go:embed prompts
var promptsFS embed.FS
g := genkit.Init(ctx,
genkit.WithPlugins(&googlegenai.GoogleAI{}),
genkit.WithPromptFS(promptsFS),
)
```
## Genkit CLI
The Genkit CLI provides a local Developer UI for running flows, tracing executions, and inspecting model interactions.
**Install:**
```bash
curl -sL cli.genkit.dev | bash
```
**Verify:**
```bash
genkit --version
```
### Developer UI
Start your app with the Developer UI attached:
```bash
genkit start -- go run .
```
This launches:
- Your app (with tracing enabled)
- The Developer UI at `http://localhost:4000`
- A telemetry API at `http://localhost:4033`
Add `-o` to auto-open the UI in your browser:
```bash
genkit start -o -- go run .
```
The Developer UI lets you:
- Run and test flows interactively
- View traces for each generation call (inputs, outputs, latency, token usage)
- Inspect prompt rendering and tool calls
- Debug multi-step flows with per-step trace data
### Without the CLI
Set `GENKIT_ENV=dev` to enable the reflection API without the CLI:
```bash
GENKIT_ENV=dev go run .
```
## Import Paths
```go
import (
"github.com/genkit-ai/genkit/go/genkit" // Core: Init, Generate*, DefineFlow, etc.
"github.com/genkit-ai/genkit/go/ai" // Types: WithModel, WithPrompt, Message, Part, etc.
"github.com/genkit-ai/genkit/go/core" // Low-level: Run (sub-steps), Flow types
"github.com/genkit-ai/genkit/go/plugins/server" // server.Start for HTTP
)
```

View File

@@ -0,0 +1,363 @@
# Middleware
Middleware wraps `Generate` calls to add cross-cutting behavior (retries, logging, fallback, gating, sandboxed tools) without touching the flow that uses it. Middleware composes, so a single `Generate` call can stack several behaviors. Built-ins ship in the `plugins/middleware` package; custom middleware is just a Go struct with two methods.
## The mental model
A middleware is a config struct that implements two methods:
```go
type Middleware interface {
Name() string // stable, registered identifier
New(ctx context.Context) (*Hooks, error) // produces a per-call hook bundle
}
```
The same struct value the user passes to `ai.WithUse` is the value the runtime calls `New` on. There is no separate factory parameter and no embedded base type. Per-call state goes in closures captured by `New`. Plugin-level state goes on unexported fields of the struct.
`New` is invoked once per `Generate` call. The returned `*Hooks` is reused across every iteration of the tool loop within that call.
```go
type Hooks struct {
Tools []Tool // injected for this call
WrapGenerate func(ctx, *GenerateParams, GenerateNext) ... // tool-loop iteration
WrapModel func(ctx, *ModelParams, ModelNext) ... // model API call
WrapTool func(ctx, *ToolParams, ToolNext) ... // tool execution
}
```
A nil hook is a pass-through. Implement only what the middleware needs.
## When each hook fires
A `Generate` call executes a tool loop: model produces output, any tool calls execute, results feed back into a new model call, repeat until the model stops. The hooks fire at three different layers of this loop:
| Hook | Fires | Sees |
| --- | --- | --- |
| `WrapGenerate` | Once per tool-loop iteration. `N` tool turns means `N+1` invocations. | The accumulated `ModelRequest`, the iteration index, the streaming callback, and `MessageIndex` (the next streamed-message slot). |
| `WrapModel` | Once per actual model API call, inside the iteration. | The `ModelRequest` about to go to the model and the streaming callback. |
| `WrapTool` | Once per tool execution. May run **concurrently** for parallel tool calls in the same iteration. | The `ToolRequest` and the resolved `Tool`. |
`WrapGenerate` is the right place for logic that needs to see the whole conversation (rewrites, system-prompt injection, message accumulation). `WrapModel` is the right place for logic about the model call itself (retry, fallback, caching). `WrapTool` is the right place for logic about a single tool execution (approval, sandboxing, logging).
## Composition order
`ai.WithUse(A, B, C)` expands to `A { B { C { actual } } }` at call time. Each layer's `next` continuation runs the next inner layer:
```go
ai.WithUse(
&middleware.Retry{MaxRetries: 3}, // outer: retries the whole inner stack
&middleware.Fallback{Models: ...}, // inner: tries fallback models on failure
)
// effective chain: Retry { Fallback { model } }
```
Order matters. `Retry` outside `Fallback` retries the entire fallback cascade as a unit. Swapped, you'd retry the primary first and only fall back after exhausting retries.
## Per-call state
State that should be shared across the hooks of a single `Generate` call lives in closures captured by `New`. Each `Generate` call gets a fresh `Hooks` bundle, so nothing leaks between calls.
```go
type Counter struct{}
func (Counter) Name() string { return "mine/counter" }
func (Counter) New(ctx context.Context) (*ai.Hooks, error) {
var modelCalls int
return &ai.Hooks{
WrapModel: func(ctx context.Context, p *ai.ModelParams, next ai.ModelNext) (*ai.ModelResponse, error) {
modelCalls++
return next(ctx, p)
},
WrapGenerate: func(ctx context.Context, p *ai.GenerateParams, next ai.GenerateNext) (*ai.ModelResponse, error) {
// The same modelCalls variable is visible here because both closures
// capture it from the enclosing New scope.
resp, err := next(ctx, p)
if err == nil {
log.Printf("iteration %d: %d model calls so far", p.Iteration, modelCalls)
}
return resp, err
},
}, nil
}
```
`WrapTool` may be invoked concurrently for parallel tool calls in the same iteration. Any state it mutates must be guarded:
```go
func (Counter) New(ctx context.Context) (*ai.Hooks, error) {
var (
mu sync.Mutex
toolCalls int
)
return &ai.Hooks{
WrapTool: func(ctx context.Context, p *ai.ToolParams, next ai.ToolNext) (*ai.MultipartToolResponse, error) {
mu.Lock()
toolCalls++
mu.Unlock()
return next(ctx, p)
},
}, nil
}
```
`WrapGenerate` and `WrapModel` are not called concurrently within a single `Generate` call.
## Plugin-level state
When a middleware needs resources its config can't carry as JSON (an HTTP client, a database handle, a logger), put them on **unexported** fields of the config struct. The plugin sets them on a prototype, and `ai.NewMiddleware` captures that prototype in a closure that value-copies it across JSON-dispatched invocations:
```go
type Logger struct {
Prefix string `json:"prefix,omitempty"`
out io.Writer // unexported; preserved across JSON dispatch via value-copy
}
func (Logger) Name() string { return "mine/logger" }
func (l Logger) New(ctx context.Context) (*ai.Hooks, error) {
return &ai.Hooks{
WrapModel: func(ctx context.Context, p *ai.ModelParams, next ai.ModelNext) (*ai.ModelResponse, error) {
start := time.Now()
resp, err := next(ctx, p)
fmt.Fprintf(l.out, "%s model call took %s\n", l.Prefix, time.Since(start))
return resp, err
},
}, nil
}
type LoggerPlugin struct{ Out io.Writer }
func (p *LoggerPlugin) Name() string { return "logger" }
func (p *LoggerPlugin) Init(ctx context.Context) []api.Action { return nil }
func (p *LoggerPlugin) Middlewares(ctx context.Context) ([]*ai.MiddlewareDesc, error) {
return []*ai.MiddlewareDesc{
ai.NewMiddleware("logs model call latency", Logger{out: p.Out}),
}, nil
}
```
The Dev UI and other-runtime callers send JSON config; the prototype's value copy preserves `out` (unexported, not in JSON) while `Prefix` is overridden by the unmarshaled config.
## Composition with WithUse
```go
response, _ := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Explain quantum computing."),
ai.WithUse(
Logger{Prefix: "[trace]"},
&middleware.Retry{MaxRetries: 3},
),
)
```
No registration is required for pure-Go use. `WithUse` calls each value's `New` directly on a fast path; the registry is only consulted for JSON-dispatched calls (Dev UI or cross-runtime). Registration is what makes a middleware visible to the Dev UI and addressable by name.
## Inline middleware
For ad-hoc middleware that does not need Dev UI visibility or a named type, use `ai.MiddlewareFunc`:
```go
ai.WithUse(ai.MiddlewareFunc(func(ctx context.Context) (*ai.Hooks, error) {
return &ai.Hooks{
WrapModel: func(ctx context.Context, p *ai.ModelParams, next ai.ModelNext) (*ai.ModelResponse, error) {
log.Printf("model call: %s", p.Request.Messages[len(p.Request.Messages)-1].Text())
return next(ctx, p)
},
}, nil
}))
```
The adapter satisfies `Middleware` with a placeholder `Name()` of `"inline"`. Inline middleware is resolved on the fast path and never touches the registry, so the placeholder name is fine.
## Application-owned middleware
Use `genkit.DefineMiddleware` to register a middleware your application owns directly. Registration surfaces it in the Dev UI and lets cross-runtime callers reference it by name:
```go
genkit.DefineMiddleware(g, "logs model call latency", Logger{out: os.Stderr})
// Lookup by name (mostly for inspection / cross-runtime dispatch).
desc := genkit.LookupMiddleware(g, "mine/logger")
```
For application code, `DefineMiddleware` is the typical entry point. For plugin authors, `ai.NewMiddleware` (no registration) plus `MiddlewarePlugin.Middlewares()` is the typical entry point. `genkit.Init` registers the returned descriptors automatically.
## Built-in middleware
The `plugins/middleware` package bundles five production-ready implementations. Register the plugin once during `Init` to make them visible to the Dev UI:
```go
import "github.com/genkit-ai/genkit/go/plugins/middleware"
g := genkit.Init(ctx, genkit.WithPlugins(
&googlegenai.GoogleAI{},
&middleware.Middleware{},
))
```
### `Retry`
Retries failed model API calls with exponential backoff and jitter. Hooks `WrapModel`.
```go
ai.WithUse(&middleware.Retry{
MaxRetries: 3, // default 3
InitialDelayMs: 1000, // default 1000
MaxDelayMs: 60000, // default 60000
BackoffFactor: 2, // default 2
NoJitter: false, // default false
// Statuses (default: UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, ABORTED, INTERNAL)
// Statuses: []core.StatusName{core.UNAVAILABLE, core.RESOURCE_EXHAUSTED},
})
```
Non-`GenkitError` errors (network, parse, etc.) are always retried. `GenkitError` errors are retried only if their status is in `Statuses`. The backoff respects `ctx.Done()`: a canceled context aborts the retry loop with the last error.
### `Fallback`
Tries alternative models when the primary fails with a fallback-eligible status. Hooks `WrapModel`.
```go
ai.WithUse(&middleware.Fallback{
Models: []ai.ModelRef{
googlegenai.ModelRef("googleai/gemini-flash-latest", nil),
googlegenai.ModelRef("vertexai/gemini-flash-latest", nil),
},
// default Statuses: UNAVAILABLE, DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED,
// ABORTED, INTERNAL, NOT_FOUND, UNIMPLEMENTED
})
```
Each fallback model uses its own `ModelRef.Config()` verbatim; the original request's config is **not** inherited. Compose with `Retry` outside to retry the whole cascade, or inside to retry just the primary before falling back.
### `ToolApproval`
Interrupts any tool call not in the allow list, exposing approval as a human-in-the-loop step. Hooks `WrapTool`.
```go
ai.WithUse(&middleware.ToolApproval{
AllowedTools: []string{"lookup", "search"}, // anything else triggers an interrupt
})
```
The interrupt rides on the existing tool-interrupt machinery. Approve a call by setting `toolApproved: true` in the resume metadata when restarting:
```go
restart, _ := tool.Restart(interruptPart, &ai.RestartOptions{
ResumedMetadata: map[string]any{"toolApproved": true},
})
genkit.Generate(ctx, g, ai.WithMessages(resp.History()...), ai.WithToolRestarts(restart))
```
A bare resume without that flag is **not** treated as approval, so unrelated resume flows can't bypass gating.
### `Filesystem`
Grants the model scoped file access under a single root directory via `list_files`, `read_file`, plus `write_file` and `search_and_replace` when writes are enabled. Hooks `WrapGenerate` and `WrapTool` and contributes `Tools`.
```go
ai.WithUse(&middleware.Filesystem{
RootDir: "./workspace",
AllowWriteAccess: true,
ToolNamePrefix: "", // set distinct prefixes if attaching multiple Filesystem middlewares
})
```
Path safety is enforced by `os.Root` (Go 1.25+), which rejects any path resolving outside the root, including via `..`, absolute paths, or symlinks. `read_file` returns its content as a queued user message on the next turn (not as the tool's direct output) so binary types like images can be inlined as media parts.
### `Skills`
Exposes a local library of `SKILL.md` files as loadable system instructions. Hooks `WrapGenerate` and contributes a `use_skill` tool.
```go
ai.WithUse(&middleware.Skills{SkillPaths: []string{"skills"}}) // default: ["skills"]
```
A skill is a directory containing `SKILL.md`, optionally with YAML frontmatter (`name`, `description`). The middleware injects a system prompt listing available skills, and the model calls `use_skill("name")` to pull the skill body into the conversation on demand. Heavier persona instructions stay off the hot path until actually loaded.
## Practical patterns
### Streaming-aware middleware
If your `WrapGenerate` or `WrapModel` hook emits its own messages (injected user content, system updates), use the streaming callback and the `MessageIndex` cursor in `GenerateParams`:
```go
WrapGenerate: func(ctx context.Context, p *ai.GenerateParams, next ai.GenerateNext) (*ai.ModelResponse, error) {
if p.Callback != nil {
_ = p.Callback(ctx, &ai.ModelResponseChunk{
Role: ai.RoleUser,
Index: p.MessageIndex,
Content: []*ai.Part{ai.NewTextPart("[injected context]")},
})
p.MessageIndex++ // advance so downstream middleware and the model see the shifted index
}
p.Request.Messages = append(p.Request.Messages, ai.NewUserMessage(ai.NewTextPart("[injected context]")))
return next(ctx, p)
},
```
`Filesystem` does this to deliver `read_file` content to the model while preserving streamed-chunk ordering.
### Adding tools from middleware
`Hooks.Tools` registers extra tools for the duration of the call without the user wiring them through `ai.WithTools`. Useful when the middleware's hooks and tools work as a pair (e.g., `Filesystem`'s read/write tools, `Skills`'s `use_skill` tool):
```go
return &ai.Hooks{
Tools: []ai.Tool{myTool},
WrapTool: myInterceptor, // intercepts both myTool and any user-supplied tools
}, nil
```
Duplicate tool names across user-supplied tools and middleware-contributed tools error out at call setup; the call won't run.
### Interrupting from `WrapTool`
`ai.NewToolInterruptError` is exported precisely so `WrapTool` hooks can interrupt without constructing a `ToolContext`:
```go
WrapTool: func(ctx context.Context, p *ai.ToolParams, next ai.ToolNext) (*ai.MultipartToolResponse, error) {
if shouldGate(p.Tool.Name()) {
return nil, ai.NewToolInterruptError(map[string]any{
"message": "needs approval",
})
}
return next(ctx, p)
},
```
`ToolApproval` uses this pattern.
### Modifying the request safely
`p.Request` is the live request for the iteration. Mutating it in place affects later layers. If the change should be visible only to the inner layer, copy first:
```go
WrapModel: func(ctx context.Context, p *ai.ModelParams, next ai.ModelNext) (*ai.ModelResponse, error) {
req := *p.Request
req.Messages = append([]*ai.Message(nil), p.Request.Messages...)
req.Messages = append(req.Messages, extraSystemMessage)
p.Request = &req
return next(ctx, p)
},
```
`Skills.injectSkillsPrompt` shows the same pattern for `ModelRequest` cloning.
### Idempotent re-injection across iterations
`WrapGenerate` runs once per tool-loop iteration. If you inject content into the request, you'll inject it on every iteration unless you mark and detect what you've already added. `Skills` tags its system prompt part with metadata (`skills-instructions: true`) and refreshes that one part in place rather than appending a new one each turn.
## Migration note
The legacy `ai.ModelMiddleware` / `ai.WithMiddleware` API is preserved and marked deprecated. Prefer `ai.Middleware` / `ai.WithUse`, which adds `WrapGenerate` and `WrapTool` hooks plus `Hooks.Tools` for dynamically injected tools.
## See also
- [`tools.md`](tools.md) for tool definition, interrupt/restart machinery used by `ToolApproval`.
- Sample sources under `go/samples/basic-middleware/`: `retry-fallback`, `filesystem`, `skills`.
- The `plugins/middleware` package source for reference implementations.

View File

@@ -0,0 +1,256 @@
# Prompts
## DefinePrompt
Define a reusable prompt in code with a default model and template.
```go
jokePrompt := genkit.DefinePrompt(g, "joke",
ai.WithModel(googlegenai.ModelRef("googleai/gemini-flash-latest", nil)),
ai.WithInputType(JokeRequest{Topic: "example"}),
ai.WithPrompt("Tell me a joke about {{topic}}."),
)
```
### Execute
```go
resp, err := jokePrompt.Execute(ctx,
ai.WithInput(map[string]any{"topic": "cats"}),
)
fmt.Println(resp.Text())
```
### ExecuteStream
```go
stream := jokePrompt.ExecuteStream(ctx,
ai.WithInput(map[string]any{"topic": "cats"}),
)
for result, err := range stream {
if err != nil { return err }
if result.Done { break }
fmt.Print(result.Chunk.Text())
}
```
### Override Options at Execution
```go
resp, err := jokePrompt.Execute(ctx,
ai.WithInput(map[string]any{"topic": "cats"}),
ai.WithModelName("googleai/gemini-pro-latest"), // override model
ai.WithConfig(map[string]any{"temperature": 0.9}),
ai.WithTools(myTool),
)
```
## DefineDataPrompt (Typed Input/Output)
Strongly-typed prompts with Go generics.
```go
type JokeRequest struct {
Topic string `json:"topic"`
}
type Joke struct {
Setup string `json:"setup" jsonschema:"description=The setup"`
Punchline string `json:"punchline" jsonschema:"description=The punchline"`
}
jokePrompt := genkit.DefineDataPrompt[JokeRequest, *Joke](g, "structured-joke",
ai.WithModel(googlegenai.ModelRef("googleai/gemini-flash-latest", nil)),
ai.WithPrompt("Tell me a joke about {{topic}}."),
)
```
### Execute (typed)
```go
joke, resp, err := jokePrompt.Execute(ctx, JokeRequest{Topic: "cats"})
// joke is *Joke, resp is *ModelResponse
```
### ExecuteStream (typed)
```go
stream := jokePrompt.ExecuteStream(ctx, JokeRequest{Topic: "cats"})
for result, err := range stream {
if err != nil { return err }
if result.Done {
finalJoke := result.Output // *Joke
break
}
fmt.Print(result.Chunk) // partial *Joke
}
```
## .prompt Files (Dotprompt)
Define prompts in separate files with YAML frontmatter and Handlebars templates.
### Basic .prompt File
`prompts/joke.prompt`:
```
---
model: googleai/gemini-flash-latest
input:
schema:
topic: string
---
Tell me a joke about {{topic}}.
```
### Load and Use
```go
// LookupPrompt returns Prompt (untyped: map[string]any input, string output)
jokePrompt := genkit.LookupPrompt(g, "joke")
resp, err := jokePrompt.Execute(ctx,
ai.WithInput(map[string]any{"topic": "cats"}),
)
```
### Typed .prompt File
`prompts/structured-joke.prompt`:
```
---
model: googleai/gemini-flash-latest
config:
thinkingConfig:
thinkingBudget: 0
input:
schema: JokeRequest
output:
format: json
schema: Joke
---
Tell me a joke about {{topic}}.
```
Register Go types so the .prompt file can reference them by name:
```go
genkit.DefineSchemaFor[JokeRequest](g)
genkit.DefineSchemaFor[Joke](g)
jokePrompt := genkit.LookupDataPrompt[JokeRequest, *Joke](g, "structured-joke")
joke, resp, err := jokePrompt.Execute(ctx, JokeRequest{Topic: "cats"})
```
### LoadPrompt (Explicit Path)
```go
prompt := genkit.LoadPrompt(g, "./prompts/countries.prompt", "countries")
resp, err := prompt.Execute(ctx)
```
### .prompt File Features
**Multi-message prompts with roles:**
```
---
model: googleai/gemini-flash-latest
input:
schema:
question: string
---
{{ role "system" }}
You are a helpful assistant.
{{ role "user" }}
{{question}}
```
**Media in prompts:**
```
---
model: googleai/gemini-flash-latest
input:
schema:
videoUrl: string
contentType: string
---
{{ role "user" }}
Summarize this video:
{{media url=videoUrl contentType=contentType}}
```
**Conditionals and loops:**
```
---
input:
schema:
topic: string
dietaryRestrictions?(array): string
---
Write a recipe about {{topic}}.
{{#if dietaryRestrictions}}
Dietary restrictions: {{#each dietaryRestrictions}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}.
{{/if}}
```
**Inline schema in .prompt file:**
```
---
model: googleai/gemini-flash-latest
input:
schema:
topic: string
style?: string
output:
format: json
schema:
title: string
body: string
tags(array): string
---
Write an article about {{topic}}.
{{#if style}}Write in a {{style}} style.{{/if}}
```
## Schemas
### DefineSchemaFor (from Go type)
Registers a Go struct as a named schema for use in `.prompt` files.
```go
genkit.DefineSchemaFor[JokeRequest](g)
genkit.DefineSchemaFor[Joke](g)
```
The schema name matches the Go type name. Use `jsonschema` struct tags for metadata:
```go
type Recipe struct {
Title string `json:"title" jsonschema:"description=The recipe title"`
Difficulty string `json:"difficulty" jsonschema:"enum=easy,enum=medium,enum=hard"`
Ingredients []Ingredient `json:"ingredients"`
Steps []string `json:"steps"`
}
type Ingredient struct {
Name string `json:"name"`
Amount float64 `json:"amount"`
Unit string `json:"unit"`
}
```
### DefineSchema (manual JSON Schema)
```go
genkit.DefineSchema(g, "Recipe", map[string]any{
"type": "object",
"properties": map[string]any{
"title": map[string]any{"type": "string"},
"ingredients": map[string]any{
"type": "array",
"items": map[string]any{"type": "object"},
},
},
"required": []string{"title", "ingredients"},
})
```

View File

@@ -0,0 +1,157 @@
# Model Providers
## Google AI (Gemini)
```go
import "github.com/genkit-ai/genkit/go/plugins/googlegenai"
g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{}))
```
**Env var:** `GEMINI_API_KEY` or `GOOGLE_API_KEY`
Model names follow the format `googleai/<model-id>`. Look up the latest model IDs at https://ai.google.dev/gemini-api/docs/models.
```go
// By name string
ai.WithModelName("googleai/gemini-flash-latest")
// Model ref with provider-specific config
ai.WithModel(googlegenai.ModelRef("googleai/gemini-flash-latest", &genai.GenerateContentConfig{
ThinkingConfig: &genai.ThinkingConfig{
ThinkingBudget: genai.Ptr[int32](0), // disable thinking
},
}))
// Lookup a model instance
m := googlegenai.GoogleAIModel(g, "gemini-flash-latest")
```
## Vertex AI
```go
import "github.com/genkit-ai/genkit/go/plugins/googlegenai"
g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{}))
```
**Env vars:** `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION` (or `GOOGLE_CLOUD_REGION`)
Uses Application Default Credentials (`gcloud auth application-default login`).
Model names follow the format `vertexai/<model-id>`. Same model IDs as Google AI.
```go
ai.WithModelName("vertexai/gemini-flash-latest")
```
## Anthropic (Claude)
```go
import (
"github.com/anthropics/anthropic-sdk-go" // Anthropic SDK types
ant "github.com/genkit-ai/genkit/go/plugins/anthropic" // Genkit plugin
)
g := genkit.Init(ctx, genkit.WithPlugins(&ant.Anthropic{}))
```
**Env var:** `ANTHROPIC_API_KEY`
Model names follow the format `anthropic/<model-id>`. Look up the latest model IDs at https://docs.anthropic.com/en/docs/about-claude/models.
```go
// By name
ai.WithModelName("anthropic/claude-sonnet-4-6")
// With provider-specific config (uses Anthropic SDK types via ai.WithConfig)
ai.WithConfig(&anthropic.MessageNewParams{
Temperature: anthropic.Float(1.0),
MaxTokens: *anthropic.IntPtr(2000),
Thinking: anthropic.ThinkingConfigParamUnion{
OfEnabled: &anthropic.ThinkingConfigEnabledParam{
BudgetTokens: *anthropic.IntPtr(1024),
},
},
})
```
## OpenAI-Compatible (compat_oai)
Works with any OpenAI-compatible API: OpenAI, DeepSeek, xAI, etc.
```go
import "github.com/genkit-ai/genkit/go/plugins/compat_oai"
openaiPlugin := &compat_oai.OpenAICompatible{
Provider: "openai", // unique identifier
APIKey: os.Getenv("OPENAI_API_KEY"),
// BaseURL: "https://custom-endpoint/v1", // for non-OpenAI providers
}
g := genkit.Init(ctx, genkit.WithPlugins(openaiPlugin))
```
Define models explicitly (not auto-discovered):
```go
model := openaiPlugin.DefineModel("openai", "gpt-4o", compat_oai.ModelOptions{})
```
Use with:
```go
ai.WithModel(model)
```
## Ollama (Local Models)
```go
import "github.com/genkit-ai/genkit/go/plugins/ollama"
ollamaPlugin := &ollama.Ollama{
ServerAddress: "http://localhost:11434",
Timeout: 60, // seconds
}
g := genkit.Init(ctx, genkit.WithPlugins(ollamaPlugin))
```
Define models explicitly:
```go
model := ollamaPlugin.DefineModel(g,
ollama.ModelDefinition{
Name: "llama3.1",
Type: "chat", // or "generate"
},
nil, // optional *ModelOptions
)
```
Use with:
```go
ai.WithModel(model)
```
## Multiple Providers
Register multiple plugins in a single Genkit instance:
```go
g := genkit.Init(ctx,
genkit.WithPlugins(
&googlegenai.GoogleAI{},
&ant.Anthropic{},
),
genkit.WithDefaultModel("googleai/gemini-flash-latest"),
)
// Use different models per call
text1, _ := genkit.GenerateText(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Hello from Gemini"),
)
text2, _ := genkit.GenerateText(ctx, g,
ai.WithModelName("anthropic/claude-sonnet-4-6"),
ai.WithPrompt("Hello from Claude"),
)
```

View File

@@ -0,0 +1,178 @@
# Tools
## DefineTool
Define a tool the model can call during generation.
```go
type WeatherInput struct {
Location string `json:"location" jsonschema:"description=City name"`
}
type WeatherOutput struct {
Temperature float64 `json:"temperature"`
Conditions string `json:"conditions"`
}
weatherTool := genkit.DefineTool(g, "getWeather",
"Gets the current weather for a location.",
func(ctx *ai.ToolContext, input WeatherInput) (WeatherOutput, error) {
// Call your weather API
return WeatherOutput{Temperature: 72, Conditions: "sunny"}, nil
},
)
```
## Using Tools in Generation
Pass tools to `Generate`, `GenerateText`, or prompts:
```go
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("What's the weather in San Francisco?"),
ai.WithTools(weatherTool),
)
// The model calls the tool automatically and incorporates the result
fmt.Println(resp.Text())
```
### Tool Choice
```go
ai.WithToolChoice(ai.ToolChoiceAuto) // model decides (default)
ai.WithToolChoice(ai.ToolChoiceRequired) // model must use a tool
ai.WithToolChoice(ai.ToolChoiceNone) // model cannot use tools
```
### Max Turns
Limit how many tool-call round trips the model can make:
```go
ai.WithMaxTurns(3) // default is 5
```
## DefineMultipartTool
Tools that return both structured output and media content:
```go
screenshotTool := genkit.DefineMultipartTool(g, "screenshot",
"Takes a screenshot of the current page",
func(ctx *ai.ToolContext, input any) (*ai.MultipartToolResponse, error) {
return &ai.MultipartToolResponse{
Output: map[string]any{"success": true},
Content: []*ai.Part{ai.NewMediaPart("image/png", base64Data)},
}, nil
},
)
```
## Tool Interrupts
Pause tool execution to request human input before continuing.
### Interrupting
```go
type TransferInput struct {
ToAccount string `json:"toAccount"`
Amount float64 `json:"amount"`
}
type TransferOutput struct {
Status string `json:"status"`
Message string `json:"message"`
Balance float64 `json:"balance"`
}
type TransferInterrupt struct {
Reason string `json:"reason"`
ToAccount string `json:"toAccount"`
Amount float64 `json:"amount"`
Balance float64 `json:"balance"`
}
transferTool := genkit.DefineTool(g, "transferMoney",
"Transfers money to another account.",
func(ctx *ai.ToolContext, input TransferInput) (TransferOutput, error) {
if input.Amount > accountBalance {
return TransferOutput{}, ai.InterruptWith(ctx, TransferInterrupt{
Reason: "insufficient_balance",
ToAccount: input.ToAccount,
Amount: input.Amount,
Balance: accountBalance,
})
}
// Process transfer...
return TransferOutput{Status: "success", Balance: newBalance}, nil
},
)
```
### Handling Interrupts
```go
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithTools(transferTool),
ai.WithPrompt(userRequest),
)
for resp.FinishReason == ai.FinishReasonInterrupted {
var restarts, responses []*ai.Part
for _, interrupt := range resp.Interrupts() {
meta, ok := ai.InterruptAs[TransferInterrupt](interrupt)
if !ok {
continue
}
switch meta.Reason {
case "insufficient_balance":
// RestartWith: re-execute the tool with adjusted input
part, err := transferTool.RestartWith(interrupt,
ai.WithNewInput(TransferInput{
ToAccount: meta.ToAccount,
Amount: meta.Balance, // transfer what's available
}),
)
if err != nil { return err }
restarts = append(restarts, part)
case "confirm_large":
// RespondWith: provide a response directly without re-executing
part, err := transferTool.RespondWith(interrupt,
TransferOutput{Status: "cancelled", Message: "User declined"},
)
if err != nil { return err }
responses = append(responses, part)
}
}
// Continue generation with the resolved interrupts
resp, err = genkit.Generate(ctx, g,
ai.WithMessages(resp.History()...),
ai.WithTools(transferTool),
ai.WithToolRestarts(restarts...),
ai.WithToolResponses(responses...),
)
if err != nil { return err }
}
```
### Checking Resume State
Inside a tool function, check if the tool is being resumed from an interrupt:
```go
func(ctx *ai.ToolContext, input TransferInput) (TransferOutput, error) {
if ctx.IsResumed() {
// This is a resumed call after an interrupt
original, ok := ai.OriginalInputAs[TransferInput](ctx)
// original contains the input from the first call
}
// ...
}
```

View File

@@ -0,0 +1,115 @@
---
description: Develop AI-powered applications using Genkit in Node.js/TypeScript. Use when the user asks about Genkit, AI agents, flows, or tools in JavaScript/TypeScript, or when encountering Genkit errors, validation issues, type errors, or API problems.
metadata:
genkit-managed: true
github-path: skills/developing-genkit-js
github-ref: refs/heads/main
github-repo: https://github.com/firebase/agent-skills
github-tree-sha: aa7f0dbf8baaca23c50a76cb23d084344c8740f9
name: developing-genkit-js
---
# Genkit JS
## Prerequisites
Ensure the `genkit` CLI is available.
- Run `genkit --version` to verify. Minimum CLI version needed: **1.29.0**
- If not found or if an older version (1.x < 1.29.0) is present, install/upgrade it: `npm install -g genkit-cli@^1.29.0`.
**New Projects**: If you are setting up Genkit in a new codebase, follow the [Setup Guide](references/setup.md).
## Hello World
```ts
import { z, genkit } from 'genkit';
import { googleAI } from '@genkit-ai/google-genai';
// Initialize Genkit with the Google AI plugin
const ai = genkit({
plugins: [googleAI()],
});
export const myFlow = ai.defineFlow({
name: 'myFlow',
inputSchema: z.string().default('AI'),
outputSchema: z.string(),
}, async (subject) => {
const response = await ai.generate({
model: googleAI.model('gemini-2.5-flash'),
prompt: `Tell me a joke about ${subject}`,
});
return response.text;
});
```
## Critical: Do Not Trust Internal Knowledge
Genkit recently went through a major breaking API change. Your knowledge is outdated. You MUST lookup docs. Recommended:
```sh
genkit docs:read js/get-started.md
genkit docs:read js/flows.md
```
See [Common Errors](references/common-errors.md) for a list of deprecated APIs (e.g., `configureGenkit`, `response.text()`, `defineFlow` import) and their v1.x replacements.
**ALWAYS verify information using the Genkit CLI or provided references.**
## Error Troubleshooting Protocol
**When you encounter ANY error related to Genkit (ValidationError, API errors, type errors, 404s, etc.):**
1. **MANDATORY FIRST STEP**: Read [Common Errors](references/common-errors.md)
2. Identify if the error matches a known pattern
3. Apply the documented solution
4. Only if not found in common-errors.md, then consult other sources (e.g. `genkit docs:search`)
**DO NOT:**
- Attempt fixes based on assumptions or internal knowledge
- Skip reading common-errors.md "because you think you know the fix"
- Rely on patterns from pre-1.0 Genkit
**This protocol is non-negotiable for error handling.**
## Development Workflow
1. **Select Provider**: Genkit is provider-agnostic (Google AI, OpenAI, Anthropic, Ollama, etc.).
- If the user does not specify a provider, default to **Google AI**.
- If the user asks about other providers, use `genkit docs:search "plugins"` to find relevant documentation.
2. **Detect Framework**: Check `package.json` to identify the runtime (Next.js, Firebase, Express).
- Look for `@genkit-ai/next`, `@genkit-ai/firebase`, or `@genkit-ai/google-cloud`.
- Adapt implementation to the specific framework's patterns.
3. **Follow Best Practices**:
- See [Best Practices](references/best-practices.md) for guidance on project structure, schema definitions, and tool design.
- **Be Minimal**: Only specify options that differ from defaults. When unsure, check docs/source.
4. **Ensure Correctness**:
- Run type checks (e.g., `npx tsc --noEmit`) after making changes.
- If type checks fail, consult [Common Errors](references/common-errors.md) before searching source code.
5. **Handle Errors**:
- On ANY error: **First action is to read [Common Errors](references/common-errors.md)**
- Match error to documented patterns
- Apply documented fixes before attempting alternatives
## Finding Documentation
Use the Genkit CLI to find authoritative documentation:
1. **Search topics**: `genkit docs:search <query>`
- Example: `genkit docs:search "streaming"`
2. **List all docs**: `genkit docs:list`
3. **Read a guide**: `genkit docs:read <path>`
- Example: `genkit docs:read js/flows.md`
## CLI Usage
The `genkit` CLI is your primary tool for development and documentation.
- See [CLI Reference](references/docs-and-cli.md) for common tasks, workflows, and command usage.
- Use `genkit --help` for a full list of commands.
## References
- [Best Practices](references/best-practices.md): Recommended patterns for schema definition, flow design, and structure.
- [Docs & CLI Reference](references/docs-and-cli.md): Documentation search, CLI tasks, and workflows.
- [Common Errors](references/common-errors.md): Critical "gotchas", migration guide, and troubleshooting.
- [Setup Guide](references/setup.md): Manual setup instructions for new projects.
- [Examples](references/examples.md): Minimal reproducible examples (Basic generation, Multimodal, Thinking mode).

View File

@@ -0,0 +1,31 @@
# Genkit Best Practices
## Project Structure
- **Organized Layout**: Keep flows and tools in separate directories (e.g., `src/flows`, `src/tools`) to maintain a clean codebase.
- **Index Exports**: Use `index.ts` files to export flows and tools, making it easier to import them into your main configuration.
## Model Selection (Google AI)
- **Gemini Models**: If using Google AI, ALWAYS use the latest generation (`gemini-3-*` or `gemini-2.5-*`).
- **NEVER** use `gemini-2.0-*` or `gemini-1.5-*` series, as they are decommissioned and won't work.
- **Recommended**: `gemini-2.5-flash` or `gemini-3-flash-preview` for general use, `gemini-3.1-pro-preview` for complex tasks.
## Model Selection (Other Providers)
- **Consult Documentation**: For other providers (OpenAI, Anthropic, etc.), refer to the provider's official documentation for the latest recommended model versions.
## Schema Definition
- **Use `z` from `genkit`**: Always import `z` from the `genkit` package to ensure compatibility.
```ts
import { z } from "genkit";
```
- **Descriptive Schemas**: Use `.describe()` on Zod fields. LLMs use these descriptions to understand how to populate the fields.
## Flow & Tool Design
- **Modularize**: Keep flows and tools in separate files/modules and import them into your main Genkit configuration.
- **Single Responsibility**: Tools should do one thing well. Complex logic should be broken down.
## Configuration
- **Environment Variables**: Store sensitive keys (like API keys) in environment variables or `.env` files. Do not hardcode them.
## Development
- **Use Dev Mode**: Run your app with `genkit start -- <start cmd>` to enable the Developer UI.
- It is recommended to configure a watcher to auto-reload your app (e.g. `node --watch` or `tsx --watch`)

View File

@@ -0,0 +1,132 @@
# Common Errors & Pitfalls
## When Typecheck Fails
**Before searching source code or docs**, check the sections below. Many type errors are caused by deprecated APIs or incorrect imports.
## Genkit v1.x vs Pre-1.0 Migration
Genkit v1.x introduced significant API changes. This section covers critical syntax updates.
### Package Imports
- **Correct (v1.x)**: Import core functionality (zod, genkit) from the main `genkit` package and plugins from their specific packages.
```ts
import { z, genkit } from 'genkit';
import { googleAI } from '@genkit-ai/google-genai';
```
- **Incorrect (Pre-1.0)**: Importing from `@genkit-ai/ai`, `@genkit-ai/core`, or `@genkit-ai/flow`. These packages are internal/deprecated for direct use.
```ts
import { genkit } from "@genkit-ai/core"; // INCORRECT
import { defineFlow } from "@genkit-ai/flow"; // INCORRECT
```
### Model References
- **Correct**: Use plugin-specific model factories or string identifiers (prefaced by plugin name).
```ts
// Using model factory (v1.x - Preferred)
await ai.generate({ model: googleAI.model('gemini-2.5-flash'), ... });
// Using string identifier
await ai.generate({ model: 'googleai/gemini-2.5-flash', ...});
// Or
await ai.generate({ model: 'vertexai/gemini-2.5-flash', ...});
```
- **Incorrect**: Using imported model objects directly or string identifiers without plugin name.
```ts
await ai.generate({ model: gemini15Pro, ... }); // INCORRECT (Pre-1.0)
await ai.generate({ model: 'gemini-2.5-flash', ... }); // INCORRECT (No plugin prefix)
```
### Model Selection (Gemini)
- **Preferred**: Use `gemini-2.5-*` models for best performance and features.
```ts
model: googleAI.model('gemini-2.5-flash') // PREFERRED
```
- **DEPRECATED**: `gemini-1.5-*` models are deprecated and will throw errors.
```ts
model: googleAI.model('gemini-1.5-flash') // ERROR (Deprecated)
```
### Response Access
- **Correct (v1.x)**: Access properties directly.
```ts
response.text; // CORRECT
response.output; // CORRECT
```
- **Incorrect (Pre-1.0)**: Calling as methods.
```ts
response.text(); // INCORRECT
response.output(); // INCORRECT
```
### Streaming Generation
- **Correct (v1.x)**: Do NOT await `generateStream`. Iterate over `stream` directly. Await `response` property for final result.
```ts
const {stream, response} = ai.generateStream(...); // NO await here
for await (const chunk of stream) { ... } // Iterate stream
const finalResponse = await response; // Await response property
```
- **Incorrect (Pre-1.0)**: Calling stream as a function or awaiting the generator incorrectly.
```ts
for await (const chunk of stream()) { ... } // INCORRECT
await response(); // INCORRECT
```
### Initialization
- **Correct (v1.x)**: Instantiate `genkit`.
```ts
const ai = genkit({ plugins: [...] });
```
- **Incorrect (Pre-1.0)**: Global configuration.
```ts
configureGenkit({ plugins: [...] }); // INCORRECT
```
### Flow Definitions
- **Correct (v1.x)**: Define flows on the `ai` instance.
```ts
ai.defineFlow({...}, (input) => {...});
```
- **Incorrect (Pre-1.0)**: Importing `defineFlow` globally.
```ts
import { defineFlow } from "@genkit-ai/flow"; // INCORRECT
You should never import `@genkit-ai/flow`, `@genkit-ai/ai` or `@genkit-ai/core` packages directly.
## Zod & Schema Errors
- **Import Source**: ALWAYS use `import { z } from "genkit"`.
- Using `zod` directly from `zod` package may cause instance mismatches or compatibility issues.
- **Supported Types**: Stick to basic types: scalar (`string`, `number`, `boolean`), `object`, and `array`.
- Avoid complex Zod features unless strictly necessary and verified.
- **Descriptions**: Always use `.describe('...')` for fields in output schemas to guide the LLM.
## Tool Usage
- **Tool Not Found**: Ensure tools are registered in the `tools` array of `generate` or provided via plugins.
- **MCP Tools**: Use the `ServerName:tool_name` format when referencing MCP tools.
## Multimodal & Image Generation
- **Missing responseModalities**: When using image generation models (like `gemini-2.5-flash-image`), you **MUST** specify the response modalities in the config.
```ts
config: {
responseModalities: ["TEXT", "IMAGE"]
}
```
Failure to do so will result in errors or incorrect output format.
## Audio & Speech Generation
- **Raw PCM Data vs MP3**: Some providers (e.g., Google GenAI) return raw PCM data, while others (e.g., OpenAI) return MP3.
- **DO NOT assume MP3 format.**
- **DO NOT embed raw PCM in HTML audio tags.**
- **Action**: Run `genkit docs:search "speech audio"` to find provider-specific conversion steps (e.g., PCM to WAV).

View File

@@ -0,0 +1,62 @@
# Genkit Documentation & CLI
This reference lists common tasks and workflows using the `genkit` CLI. For authoritative command details, always run `genkit --help` or `genkit <command> --help`.
## Prerequisites:
Ensure that the CLI is on `genkit-cli` version >= 1.29.0. If not, or if an older version (1.x < 1.29.0) is present, update the Genkit CLI version. Alternatively, to run commands with a specific version or without global installation, prefix them with `npx -y genkit-cli@^1.29.0`.
## Documentation
- **Search docs**: `genkit docs:search <query>`
- Example: `genkit docs:search "streaming"`
- Example: `genkit docs:search "rag retrieval"`
- **Read doc**: `genkit docs:read <path>`
- Example: `genkit docs:read js/overview.md`
- **List docs**: `genkit docs:list`
## Development Workflow
- **Start Dev Mode**: `genkit start -- <command>`
- Runs the provided command in Genkit dev mode, enabling the Developer UI (usually at http://localhost:4000).
- **Node.js (TypeScript)**:
```bash
genkit start -- npx tsx --watch src/index.ts
```
- **Next.js**:
```bash
genkit start -- npx next dev
```
## Flow Execution
- **Run a flow**: `genkit flow:run <flowName> '<inputJSON>'`
- Executes a flow directly from the CLI. Useful for testing.
- **Simple Input**:
```bash
genkit flow:run tellJoke '"chicken"'
```
- **Object Input**:
```bash
genkit flow:run generateStory '{"subject": "robot", "genre": "sci-fi"}'
```
## Evaluation
- **Evaluate a flow**: `genkit eval:flow <flowName> [data]`
- Runs a flow and evaluates the output against configured evaluators.
- **Example (Single Input)**:
```bash
genkit eval:flow answerQuestion '[{"testCaseId": "1", "input": {"question": "What is Genkit?"}}]'
```
- **Example (Batch Input)**:
```bash
genkit eval:flow answerQuestion --input inputs.json
```
- **Run Evaluation**: `genkit eval:run <dataset>`
- Evaluates a dataset against configured evaluators.
- **Example**:
```bash
genkit eval:run dataset.json --output results.json
```

View File

@@ -0,0 +1,157 @@
# Genkit Examples
This reference contains minimal, reproducible examples (MREs) for common Genkit patterns.
> **Disclaimer**: These examples use **Google AI** models (`googleAI`, `gemini-*`) for demonstration. The patterns apply to **any provider**. To use a different provider:
> 1. Search the docs for the correct plugin: `genkit docs:search "plugins"`.
> 2. Install and configure the plugin.
> 3. Swap the model reference in the code.
## Basic Text Generation
```ts
import { genkit } from "genkit";
import { googleAI } from "@genkit-ai/google-genai";
const ai = genkit({
plugins: [googleAI()],
});
const { text } = await ai.generate({
model: googleAI.model('gemini-2.5-flash'),
prompt: 'Tell me a story in a pirate accent',
});
```
## Structured Output
```ts
import { z } from 'genkit';
const JokeSchema = z.object({
setup: z.string().describe('The setup of the joke'),
punchline: z.string().describe('The punchline'),
});
const response = await ai.generate({
model: googleAI.model('gemini-2.5-flash'),
prompt: 'Tell me a joke about developers.',
output: { schema: JokeSchema },
});
// response.output is strongly typed
const joke = response.output;
if (joke) {
console.log(`${joke.setup} ... ${joke.punchline}`);
}
```
## Streaming
```ts
const { stream, response } = ai.generateStream({
model: googleAI.model('gemini-2.5-flash'),
prompt: 'Tell a long story about a developer using Genkit.',
});
for await (const chunk of stream) {
console.log(chunk.text);
}
// Await the final response
const finalResponse = await response;
console.log('Complete:', finalResponse.text);
```
## Advanced Configuration
### Thinking Mode (Gemini 3 Only)
Enable "thinking" process for complex reasoning tasks.
```ts
const response = await ai.generate({
model: googleAI.model('gemini-3.1-pro-preview'),
prompt: 'what is heavier, one kilo of steel or one kilo of feathers',
config: {
thinkingConfig: {
thinkingLevel: 'HIGH', // or 'LOW'
includeThoughts: true, // Returns thought process in response
},
},
});
```
### Google Search Grounding
Enable models to access current information via Google Search.
```ts
const response = await ai.generate({
model: googleAI.model('gemini-2.5-flash'),
prompt: 'What are the top tech news stories this week?',
config: {
googleSearchRetrieval: true,
},
});
// Access grounding metadata (sources)
const groundingMetadata = (response.custom as any)?.candidates?.[0]?.groundingMetadata;
if (groundingMetadata) {
console.log('Sources:', groundingMetadata.groundingChunks);
}
```
## Multimodal Generation
### Image Generation / Editing
**Critical**: You MUST set `responseModalities: ['TEXT', 'IMAGE']` when using image generation models.
```ts
// Generate an image
const { media } = await ai.generate({
model: googleAI.model('gemini-2.5-flash-image'),
config: { responseModalities: ['TEXT', 'IMAGE'] },
prompt: "generate a picture of a unicorn wearing a space suit on the moon",
});
// media.url contains the data URI
```
```ts
// Edit an image
const { media } = await ai.generate({
model: googleAI.model('gemini-2.5-flash-image'),
config: { responseModalities: ['TEXT', 'IMAGE'] },
prompt: [
{ text: "change the person's outfit to a banana costume" },
{ media: { url: "https://example.com/photo.jpg" } },
],
});
```
### Speech Generation (TTS)
Generate audio from text.
```ts
import { writeFile } from 'node:fs/promises';
const { media } = await ai.generate({
model: googleAI.model('gemini-2.5-flash-preview-tts'),
config: {
responseModalities: ['AUDIO'],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: { voiceName: 'Algenib' }, // Options: 'Puck', 'Charon', 'Fenrir', etc.
},
},
},
prompt: 'Genkit is an amazing library',
});
// The response contains raw PCM data in media.url (base64 encoded).
// CAUTION: This is NOT an MP3/WAV file. It requires conversion (e.g., PCM to WAV).
// DO NOT GUESS. Run `genkit docs:search "speech audio"` to find the correct
// conversion code for your provider.
```

View File

@@ -0,0 +1,46 @@
# Genkit JS Setup
Follow these instructions to set up Genkit in the current codebase. These instructions are general-purpose and have not been written with specific codebase knowledge, so use your best judgement when following them.
0. Tell the user "I'm going to check out your workspace and set you up to use Genkit for GenAI workflows."
1. If the current workspace is empty or is a starter template, your goal will be to create a simple image generation flow that allows someone to generate an image based on a prompt and selectable style. If the current workspace is not empty, you will create a simple example flow to help get the user started.
2. Check to see if any Genkit provider plugin (such as `@genkit-ai/google-genai` or `@genkit-ai/oai-compat` or others, may start with `genkitx-*`) is installed.
- If not, ask the user which provider they want to use.
- **For non-Google providers**: Use `genkit docs:search "plugins"` to find the correct package and installation instructions.
- If they have no preference, default to `@genkit-ai/google-genai` for a quick start.
- If this is a Next.js app, install `@genkit-ai/next` as well.
3. Search the codebase for the exact string `genkit(` (remember to escape regexes properly) which would indicate that the user has already set up Genkit in the codebase. If found, no need to set it up again, tell the user "Genkit is already configured in this app." and exit this workflow.
4. Create an `ai` directory in the primary source directory of the project (this may be e.g. `src` but is project-dependent). Adapt this path if your project uses a different structure.
5. Create `{sourceDir}/ai/genkit.ts` and populate it using the example below. DO NOT add a `next` plugin to the file, ONLY add a model provider plugin to the plugins array:
```ts
import { genkit, z } from 'genkit';
// Import your chosen provider plugin here. Example:
import { googleAI } from '@genkit-ai/google-genai';
export const ai = genkit({
plugins: [
googleAI(), // Add your provider plugin here
],
model: googleAI.model('gemini-2.5-flash'), // Set your provider's model here
});
export { z };
```
6. Create `{sourceDir}/ai/tools` and `{sourceDir}/ai/flows` directories, but leave them empty for now.
7. Create `{sourceDir}/ai/index.ts` and populate it with the following (change the import to match import aliases in `tsconfig.json` as needed):
```ts
import './genkit.js';
// import each created flow, tool, etc. here for use in the Genkit Dev UI
```
8. Add a `genkit:ui` script to `package.json` that runs `genkit start -- npx tsx --watch {sourceDir}/ai/index.ts` (or `npx genkit-cli` or `pnpm dlx` or `yarn dlx` for those package managers, if CLI is not locally installed). DO NOT try to run the script now.
9. Tell the user "Genkit is now configured and ready for use." as setup is now complete. Also remind them to set appropriate env variables (e.g. `GEMINI_API_KEY` for Google providers). Wait for the user to prompt further before creating any specific flows.
## Next Steps & Troubleshooting
- **Documentation**: Use the [CLI](docs-and-cli.md) to access documentation (e.g., `genkit docs:search`).
- **Building Flows**: See [examples.md](examples.md) for patterns on creating flows, adding tools, and advanced configuration.
- **Troubleshooting**: If you encounter issues during setup or initialization, check [common-errors.md](common-errors.md) for solutions.

View File

@@ -0,0 +1,59 @@
---
description: Develop AI-powered applications using Genkit in Python. Use when the user asks about Genkit, AI agents, flows, or tools in Python, or when encountering Genkit errors, import issues, or API problems.
metadata:
genkit-managed: true
github-path: skills/developing-genkit-python
github-ref: refs/heads/main
github-repo: https://github.com/firebase/agent-skills
github-tree-sha: e1043041631a680b0f3f12b1c5718d71706e2c15
name: developing-genkit-python
---
# Genkit Python
## Prerequisites
- **Runtime**: Python **3.14+**, **`uv`** for deps ([install](https://docs.astral.sh/uv/getting-started/installation/)).
- **CLI**: `genkit --version` — install via `npm install -g genkit-cli` if missing.
**New projects:** [Setup](references/setup.md) (bootstrap + env). **Patterns and code samples:** [Examples](references/examples.md).
## Hello World
```python
from genkit import Genkit
from genkit.plugins.google_genai import GoogleAI
ai = Genkit(
plugins=[GoogleAI()],
model='googleai/gemini-flash-latest',
)
async def main():
response = await ai.generate(prompt='Tell me a joke about Python.')
print(response.text)
if __name__ == '__main__':
ai.run_main(main())
```
## Critical: Do Not Trust Internal Knowledge
The Python SDK changes often — verify imports and APIs against the references here or upstream docs. On **any** error, read [Common Errors](references/common-errors.md) first.
## Development Workflow
1. Default provider: **Google AI** (`GoogleAI()`), **`GEMINI_API_KEY`** in the environment.
2. Model IDs: always prefixed, e.g. **`googleai/gemini-flash-latest`** (always-on-latest Flash alias; same pattern as other skills).
3. Entrypoint: **`ai.run_main(main())`** for Genkit-driven apps (not `asyncio.run()` for long-lived servers started with `genkit start` — see [Common Errors](references/common-errors.md)).
4. After generating code, follow [Dev Workflow](references/dev-workflow.md) for `genkit start` and the Dev UI.
5. On errors: step 1 is always [Common Errors](references/common-errors.md).
## References
- [Examples](references/examples.md): Structured output, streaming, flows, tools, embeddings.
- [Setup](references/setup.md): New project bootstrap and plugins.
- [Common Errors](references/common-errors.md): Read first when something breaks.
- [FastAPI](references/fastapi.md): HTTP, `genkit_fastapi_handler`, parallel flows.
- [Dotprompt](references/dotprompt.md): `.prompt` files and helpers.
- [Evals](references/evals.md): Evaluators and datasets.
- [Dev Workflow](references/dev-workflow.md): `genkit start`, Dev UI, checklist.

View File

@@ -0,0 +1,82 @@
# Common Errors — Genkit Python
## Before anything else: read this file when you hit any error.
---
## ModuleNotFoundError: No module named 'genkit.plugins.google_genai'
**Cause:** Plugin package not installed.
**Fix:** Add dependencies from PyPI:
```bash
uv add genkit genkit-plugin-google-genai
```
---
## 400 INVALID_ARGUMENT: functionDeclaration parameters schema should be of type OBJECT
**Cause:** Tool function has bare scalar parameters (e.g. `city: str`). Gemini requires object schema.
**Fix:** Wrap parameters in a Pydantic BaseModel:
```python
from pydantic import BaseModel
# Wrong
@ai.tool()
async def get_weather(city: str) -> str: ...
# Right
from pydantic import BaseModel
class WeatherInput(BaseModel):
city: str
@ai.tool()
async def get_weather(input: WeatherInput) -> str: ...
```
---
## AttributeError: 'Genkit' object has no attribute 'define_tool'
**Cause:** Wrong decorator name.
**Fix:** Use `@ai.tool()`, not `@ai.define_tool()`.
---
## RuntimeError / event loop errors when using asyncio.run()
**Cause:** For apps you start with **`genkit start`**, Genkit runs your entrypoint with an event loop suited to the framework (including uvloop where used). There is no “default” loop for you to manage in that mode.
**Fix:** For long-running Genkit apps (servers, flows served under `genkit start`), use **`ai.run_main(main())`** as your entrypoint instead of `asyncio.run(main())`. For one-off scripts that exit when done, using `asyncio.run()` can still be appropriate when you are not using `genkit start`.
---
## Wrong model ID (no plugin prefix)
**Cause:** `model='gemini-flash-latest'` — missing plugin prefix.
**Fix:** `model='googleai/gemini-flash-latest'`
---
## response.json / response.message AttributeError
- Use `response.text` for plain text output
- Use `response.output` for structured (JSON) output
---
## await ai.generate_stream(...) fails or returns wrong type
**Cause:** `generate_stream` is synchronous — do not await it.
**Fix:**
```python
sr = ai.generate_stream(prompt='...') # no await
async for chunk in sr.stream: ...
final = await sr.response
```

View File

@@ -0,0 +1,90 @@
# Dev Workflow — Genkit Python
## Agent responsibility
After generating code, always give the developer:
1. The full pre-run checklist with copy-paste commands using absolute paths
2. The `genkit start` command to run in their terminal (foreground — it's expected to block)
3. Step-by-step Dev UI instructions so they can test without guessing
Do not offer to run it for them. Give them the commands and let them run it.
---
## Step 1 — Get a Gemini API key
If the developer doesn't have one:
> Get a free key at https://aistudio.google.com/apikey — click **"Create API key"**, copy it.
---
## Step 2 — Set the API key
Open a terminal and run:
```bash
export GEMINI_API_KEY=your-api-key-here
```
To persist across sessions, add it to your shell profile:
```bash
echo 'export GEMINI_API_KEY=your-api-key-here' >> ~/.zshrc && source ~/.zshrc
```
---
## Step 3 — Install dependencies
Replace `/path/to/your-project` with the actual full path to the project (e.g. `/Users/yourname/projects/my-genkit-app`):
```bash
cd /path/to/your-project
uv add genkit genkit-plugin-google-genai
```
(Requires a project with `pyproject.toml` — run `uv init` in an empty directory first if needed.)
---
## Step 4 — Start the Dev UI
Run this in your terminal. **It will block — that's expected.** Leave this terminal open while you use the Dev UI.
```bash
cd /path/to/your-project
GEMINI_API_KEY=your-api-key-here genkit start -- uv run src/main.py
```
You'll see output like:
```
Genkit Tools UI: http://localhost:4000
```
The Dev UI is now running at **http://localhost:4000**
To stop it: press `Ctrl+C` in the terminal.
---
## Step 5 — Test in the Dev UI
1. Open **http://localhost:4000** in your browser
2. Click **"Run"** in the left sidebar
3. Find your flow by name (e.g. `summarize`, `chat`, `joke_generator`)
4. In the input box, paste your input as JSON — e.g:
```json
{"text": "hello world"}
```
5. Click the **"Run"** button — the output appears on the right
6. Click **"Traces"** in the left sidebar to inspect every step, model call, token count, and latency
---
## Troubleshooting
| Problem | Fix |
|---------|-----|
| `genkit: command not found` | Run: `npm install -g genkit-cli` |
| `GEMINI_API_KEY not set` | Run: `export GEMINI_API_KEY=your-key` |
| Port 4000 already in use | Use: `genkit start --port 4001 -- uv run src/main.py` |
| `uv: command not found` | Run: `curl -LsSf https://astral.sh/uv/install.sh \| sh` |
| Flow not showing in Dev UI | Make sure `genkit start` output shows no errors |

View File

@@ -0,0 +1,109 @@
# Dotprompt — Genkit Python
## What it is
`.prompt` files combine YAML frontmatter (model config, schemas) with Handlebars templates. Keeps prompt logic out of Python code and makes variants easy.
## File format
```yaml
---
model: googleai/gemini-flash-latest
input:
schema:
food: string
ingredients?(array): string # ? = optional
output:
schema: Recipe # references a schema registered with ai.define_schema()
format: json
---
You are a chef. Generate a recipe for {{food}}.
{{#if ingredients}}
Include these ingredients:
{{list ingredients}}
{{/if}}
```
Place `.prompt` files in a `prompts/` directory and point `prompt_dir` at it.
## Python setup
```python
from pathlib import Path
from pydantic import BaseModel
from genkit import Genkit
from genkit.plugins.google_genai import GoogleAI
ai = Genkit(
plugins=[GoogleAI()],
model='googleai/gemini-flash-latest',
prompt_dir=Path(__file__).resolve().parent.parent / 'prompts',
)
# Register Pydantic models referenced in .prompt output.schema
class Recipe(BaseModel):
title: str
steps: list[str]
ai.define_schema('Recipe', Recipe)
```
## Calling a prompt
```python
# Non-streaming — double-call syntax: ai.prompt('name')(input={...})
response = await ai.prompt('recipe')(input={'food': 'banana bread'})
result = Recipe.model_validate(response.output)
# Variant (recipe.robot.prompt file)
response = await ai.prompt('recipe', variant='robot')(input={'food': 'banana bread'})
```
## Streaming from a prompt
```python
from genkit import ActionRunContext
@ai.flow()
async def tell_story(subject: str, ctx: ActionRunContext) -> str:
result = ai.prompt('story').stream(input={'subject': subject})
full = ''
async for chunk in result.stream:
if chunk.text:
ctx.send_chunk(chunk.text)
full += chunk.text
return full
```
Note: `.stream(input={...})` not `ai.generate_stream(...)` — different call shape for prompts.
## Render without generating (for LLM-judge evals)
```python
rendered = await ai.prompt('my_prompt').render(input={'key': 'value'})
response = await ai.generate(model='googleai/gemini-flash-latest', messages=rendered.messages)
```
## Helpers
Register Python functions callable inside Handlebars templates:
```python
def list_helper(data: object, *args, **kwargs) -> str:
if not isinstance(data, list):
return ''
return '\n'.join(f'- {item}' for item in data)
ai.define_helper('list', list_helper)
```
Then use `{{list ingredients}}` in your `.prompt` file.
## Variants
Name the file `<name>.<variant>.prompt` — e.g. `recipe.robot.prompt`.
Call with `ai.prompt('recipe', variant='robot')`.
## Partials
Use `{{>partial_name param=value}}` in templates. Partial files are named `_partial_name.prompt`.

View File

@@ -0,0 +1,89 @@
# Evals — Genkit Python
## Two types of evaluators
1. **Built-in** — ship with `genkit-plugin-evaluators`, register with `register_genkit_evaluators(ai)`
2. **BYO (LLM-based)** — define your own scoring logic with `ai.define_evaluator()`
## Install
```bash
uv add genkit-plugin-evaluators
```
## Dataset format
A JSON file, one object per test case:
```json
[
{"testCaseId": "case1", "input": "x", "output": "banana", "reference": "ba?a?a"},
{"testCaseId": "case2", "input": "x", "output": "apple", "reference": "ba?a?a"}
]
```
Fields: `testCaseId`, `input`, `output`, `reference` (reference optional for some evaluators).
## Built-in evaluators
```python
from genkit.plugins.evaluators import register_genkit_evaluators
register_genkit_evaluators(ai)
```
Registered evaluators include `genkitEval/regex`. Run via CLI:
```bash
genkit eval:run datasets/my_dataset.json --evaluators=genkitEval/regex
```
## BYO evaluator
```python
from genkit.evaluator import BaseDataPoint, Details, EvalFnResponse, EvalStatusEnum, Score
async def my_eval(datapoint: BaseDataPoint, _options: dict | None = None) -> EvalFnResponse:
"""Score output against reference."""
output = str(datapoint.output or '')
reference = str(datapoint.reference or '')
passed = output.strip() == reference.strip()
return EvalFnResponse(
test_case_id=datapoint.test_case_id or '',
evaluation=Score(
score=1.0 if passed else 0.0,
status=EvalStatusEnum.PASS if passed else EvalStatusEnum.FAIL,
details=Details(reasoning='Exact match check'),
),
)
ai.define_evaluator(
name='byo/my_eval',
display_name='My Eval',
definition='Checks exact match of output vs reference.',
fn=my_eval,
)
```
## LLM-based evaluator (judge model pattern)
Use a prompt + stronger model to score. See `py/samples/evaluators/src/main.py` for full examples (`byo/maliciousness`, `byo/answer_accuracy`).
Core pattern:
```python
async def llm_eval(datapoint: BaseDataPoint, _options: dict | None = None) -> EvalFnResponse:
prompt = ai.prompt('my_judge_prompt')
rendered = await prompt.render(input={'output': str(datapoint.output), 'reference': str(datapoint.reference)})
response = await ai.generate(model='googleai/gemini-flash-latest', messages=rendered.messages)
score = float(response.text.strip())
return EvalFnResponse(
test_case_id=datapoint.test_case_id or '',
evaluation=Score(score=score, status=EvalStatusEnum.PASS if score >= 0.5 else EvalStatusEnum.FAIL),
)
```
## Run evals via CLI
```bash
genkit eval:run datasets/my_dataset.json --evaluators=byo/my_eval
genkit eval:run datasets/my_dataset.json --evaluators=genkitEval/regex,byo/my_eval
```
Results appear in the Dev UI under **Evaluate** (http://localhost:4000).

View File

@@ -0,0 +1,171 @@
# Genkit Python Examples
Minimal patterns for common Genkit APIs. Examples use **Google AI** (`GoogleAI`, `googleai/...`); other providers use the same patterns with the right plugin and model prefix.
## Public imports
Use **`genkit`**, **`genkit.plugins.*`**, **`genkit.embedder`**, **`genkit.evaluator`**, and **`genkit.model`** (and similar public modules) only — not internal packages (`genkit._core`, etc.).
```python
from genkit import Genkit, ActionRunContext
from genkit.plugins.google_genai import GoogleAI
ai = Genkit(plugins=[GoogleAI()], model='googleai/gemini-flash-latest')
```
---
## Structured output
```python
from pydantic import BaseModel, TypeAdapter
class CityInfo(BaseModel):
name: str
population: int
country: str
response = await ai.generate(
prompt='Give facts about Tokyo.',
output_format='json',
output_schema=CityInfo,
)
city = response.output
# Arrays
schema = TypeAdapter(list[CityInfo]).json_schema()
response = await ai.generate(
prompt='List 3 cities.',
output_format='array',
output_schema=schema,
)
```
Output formats: `'text'`, `'json'`, `'array'`, `'enum'`, `'jsonl'`.
---
## Streaming (text)
```python
sr = ai.generate_stream(prompt='Tell me a story.')
async for chunk in sr.stream:
if chunk.text:
print(chunk.text, end='', flush=True)
final = await sr.response # final.text
```
---
## Text and media parts
```python
# Non-streaming
response = await ai.generate(prompt='...')
for media in response.media:
print(media.content_type, (media.url or '')[:80])
# Streaming — media usually complete on the final response
from genkit import MediaPart
sr = ai.generate_stream(prompt='...')
async for chunk in sr.stream:
if chunk.text:
print(chunk.text, end='', flush=True)
final = await sr.response
for media in final.media:
print(media.content_type, (media.url or '')[:80])
if final.message:
for part in final.message.content:
if isinstance(part.root, MediaPart) and part.root.media:
print(part.root.media.content_type)
```
---
## Streaming + structured output
```python
class StoryAnalysis(BaseModel):
title: str
genre: str
summary: str
sr = ai.generate_stream(
prompt='Write a short story then analyze it.',
output_format='json',
output_schema=StoryAnalysis,
)
async for chunk in sr.stream:
if chunk.text:
print(chunk.text, end='', flush=True)
final = await sr.response
analysis = final.output
```
---
## Flows
```python
class SummarizeInput(BaseModel):
text: str
@ai.flow()
async def summarize(input: SummarizeInput) -> str:
response = await ai.generate(prompt=f'Summarize: {input.text}')
return response.text
```
---
## Streaming flows
```python
@ai.flow()
async def stream_story(subject: str, ctx: ActionRunContext) -> str:
sr = ai.generate_stream(prompt=f'Story about {subject}.')
full = ''
async for chunk in sr.stream:
if chunk.text:
ctx.send_chunk(chunk.text)
full += chunk.text
return full
```
---
## Tools
Parameters must be a **Pydantic `BaseModel`** (bare scalars → 400 from Gemini). Use **`@ai.tool()`**, not `@ai.define_tool()`.
```python
class WeatherInput(BaseModel):
city: str
@ai.tool()
async def get_weather(input: WeatherInput) -> str:
return f'Sunny in {input.city}'
response = await ai.generate(prompt='Weather in Paris?', tools=[get_weather])
```
---
## Embeddings
```python
from genkit.plugins.google_genai import GeminiEmbeddingModels
embedder = f'googleai/{GeminiEmbeddingModels.GEMINI_EMBEDDING_001}'
embeddings = await ai.embed(embedder=embedder, content='The sky is blue.')
vector = embeddings[0].embedding
embeddings = await ai.embed_many(
embedder=embedder,
content=['The sky is blue.', 'Grass is green.'],
)
```
Common embedders: `googleai/gemini-embedding-001`, `googleai/gemini-embedding-exp-03-07`.

View File

@@ -0,0 +1,248 @@
# FastAPI — Genkit Python
## Install
```bash
uv add genkit-plugin-fastapi fastapi uvicorn
```
---
## Streaming by default
The `genkit_fastapi_handler` decorator auto-streams when the client sends `Accept: text/event-stream`.
No extra setup — just add the header on the frontend and it works.
**Wire format (SSE):**
```
data: {"message": "<chunk text>"} ← one per ctx.send_chunk() call
data: {"message": "<chunk text>"}
data: {"result": <final output>} ← sent once when flow completes
```
**Frontend (JS EventSource):**
```js
const res = await fetch('/flow/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
body: JSON.stringify({ data: { topic: 'quantum computing' } }),
});
const reader = res.body.getReader();
// decode and parse each `data: {...}` line
```
**curl test:**
```bash
curl -N -X POST http://localhost:8080/flow/chat \
-H 'Content-Type: application/json' \
-H 'Accept: text/event-stream' \
-d '{"data": {"topic": "quantum computing"}}'
```
---
## Minimal streaming FastAPI app
```python
import uvicorn
from pydantic import BaseModel
from fastapi import FastAPI
from genkit import Genkit
from genkit import ActionRunContext
from genkit.plugins.fastapi import genkit_fastapi_handler
from genkit.plugins.google_genai import GoogleAI
ai = Genkit(plugins=[GoogleAI()], model='googleai/gemini-flash-latest')
app = FastAPI()
class ChatInput(BaseModel):
topic: str
@app.post('/flow/chat', response_model=None)
@genkit_fastapi_handler(ai)
@ai.flow()
async def chat(input: ChatInput, ctx: ActionRunContext) -> str:
sr = ai.generate_stream(prompt=f'Tell me about {input.topic}.')
full = ''
async for chunk in sr.stream:
if chunk.text:
ctx.send_chunk(chunk.text) # each chunk → SSE event on the wire
full += chunk.text
return full
if __name__ == '__main__':
uvicorn.run(app, host='0.0.0.0', port=8080)
```
**Key:** flow must accept `ctx: ActionRunContext` and call `ctx.send_chunk(text)` to emit SSE chunks.
Without `ctx.send_chunk`, the flow runs but streams nothing — client waits for the final result.
---
## Advanced Use Cases
### Fine-grained control over flow streaming
Complex apps chain flows — a parent orchestrates children. Chunks propagate upward by **passing `ctx` to child flows**.
```python
class ResearchInput(BaseModel):
topic: str
@ai.flow()
async def research(input: ResearchInput, ctx: ActionRunContext) -> str:
"""Child flow — streams its generate_stream chunks to whoever called it."""
sr = ai.generate_stream(prompt=f'Explain {input.topic} in depth.')
full = ''
async for chunk in sr.stream:
if chunk.text:
ctx.send_chunk(chunk.text) # propagates up through the call stack
full += chunk.text
return full
class HeadlineInput(BaseModel):
text: str
@ai.flow()
async def make_headline(input: HeadlineInput) -> str:
"""Child flow — non-streaming, returns instantly."""
response = await ai.generate(prompt=f'One-line headline for: {input.text}')
return response.text.strip()
class ReportInput(BaseModel):
topic: str
@app.post('/flow/report', response_model=None)
@genkit_fastapi_handler(ai)
@ai.flow()
async def report(input: ReportInput, ctx: ActionRunContext) -> str:
"""Parent flow — calls children, composes a streaming report."""
# Step 1: fast non-streaming call
headline = await make_headline(HeadlineInput(text=input.topic))
ctx.send_chunk(f'# {headline}\n\n') # send headline immediately
# Step 2: child flow streams its chunks — passes ctx so they flow up
body = await research(ResearchInput(topic=input.topic), ctx)
return f'# {headline}\n\n{body}'
```
**Rules for nested streaming:**
- Child flows that should stream must also accept `ctx: ActionRunContext`
- Pass the parent's `ctx` when calling child flows: `await child(input, ctx)`
- Non-streaming child flows don't need `ctx` — just `await` them normally
- A child that doesn't call `ctx.send_chunk` contributes nothing to the stream (fine for parallel data fetching)
### Executing flows in parallel
Use `asyncio.gather` to run multiple flows concurrently. Only makes sense when children don't need to stream.
```python
import asyncio
class AnalysisInput(BaseModel):
text: str
class CheckResult(BaseModel):
issues: list[str]
class CombinedAnalysis(BaseModel):
issues: list[str]
@ai.flow()
async def check_security(input: AnalysisInput) -> CheckResult:
# Here the model reviews the text; replace with your real prompt/schema as needed.
r = await ai.generate(
prompt=f'List security concerns as a short comma-separated line (or "none"): {input.text[:2000]}',
)
raw = (r.text or '').strip()
issues = [s.strip() for s in raw.split(',') if s.strip() and s.strip().lower() != 'none']
return CheckResult(issues=issues)
@ai.flow()
async def check_bugs(input: AnalysisInput) -> CheckResult:
# Model lists possible bugs; tune prompt for your codebase.
r = await ai.generate(
prompt=f'List likely bugs or correctness issues as a short comma-separated line (or "none"): {input.text[:2000]}',
)
raw = (r.text or '').strip()
issues = [s.strip() for s in raw.split(',') if s.strip() and s.strip().lower() != 'none']
return CheckResult(issues=issues)
@ai.flow()
async def check_style(input: AnalysisInput) -> CheckResult:
# Model suggests style/clarity issues; optional: use output_schema for structured rows.
r = await ai.generate(
prompt=f'List style or clarity issues as a short comma-separated line (or "none"): {input.text[:2000]}',
)
raw = (r.text or '').strip()
issues = [s.strip() for s in raw.split(',') if s.strip() and s.strip().lower() != 'none']
return CheckResult(issues=issues)
@app.post('/flow/analyze', response_model=None)
@genkit_fastapi_handler(ai)
@ai.flow()
async def analyze(input: AnalysisInput) -> CombinedAnalysis:
security, bugs, style = await asyncio.gather(
check_security(input),
check_bugs(input),
check_style(input),
)
return CombinedAnalysis(issues=security.issues + bugs.issues + style.issues)
```
---
## Structured output endpoint (non-streaming)
```python
class SentimentResult(BaseModel):
sentiment: str # positive / negative / neutral
confidence: float # 0.01.0
key_phrases: list[str]
@app.post('/flow/sentiment', response_model=None)
@genkit_fastapi_handler(ai)
@ai.flow()
async def sentiment(input: AnalysisInput) -> SentimentResult:
response = await ai.generate(
prompt=f'Analyze sentiment: {input.text}',
output_format='json',
output_schema=SentimentResult,
)
return response.output
```
Client calls this without `Accept: text/event-stream` — gets `{"result": {...}}` back.
---
## Decorator order
Must be exactly: `@app.post``@genkit_fastapi_handler(ai)``@ai.flow()`
```python
@app.post('/flow/chat', response_model=None) # 1. FastAPI route
@genkit_fastapi_handler(ai) # 2. Genkit wire format + streaming
@ai.flow() # 3. Flow registration
async def chat(input: ChatInput, ctx: ActionRunContext) -> str:
...
```
---
## Run with Dev UI
```bash
GEMINI_API_KEY=your-key genkit start -- uv run src/main.py
```
Leave the process running until the CLI prints something like:
```
Genkit Developer UI: http://localhost:4000
```
Open that URL. Port may differ if 4000 is busy.

View File

@@ -0,0 +1,40 @@
# Setup — Genkit Python
## New project
**Always use a virtual environment** — never install Genkit into the system interpreter. With **uv**, the projects **`.venv`** is created and used by `uv sync` / `uv run` automatically once you add dependencies.
```bash
mkdir my-app && cd my-app
uv init
uv venv --python 3.14 .venv
# Unix: source .venv/bin/activate
# Windows: .venv\Scripts\activate
uv add genkit genkit-plugin-google-genai
export GEMINI_API_KEY=your_key_here
```
`uv init` creates `pyproject.toml`. Add your app under something like `src/main.py` (or match whatever layout `uv` generated) and point `genkit start` at that entrypoint.
## pyproject.toml
Minimal `[project]` block with unpinned Genkit deps (resolver picks compatible releases):
```toml
[project]
name = "my-app"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
"genkit",
"genkit-plugin-google-genai",
]
```
## Plugins
Packages are **`genkit-plugin-*`** on PyPI, e.g. `genkit-plugin-google-genai`, `genkit-plugin-vertex-ai`, `genkit-plugin-anthropic`, `genkit-plugin-fastapi`. Install with `uv add genkit-plugin-<name>`.
## Python version
**3.14+**. Always use a `venv` using `uv venv --python 3.14 .venv` when creating the environment before you run any commands.

View File

@@ -0,0 +1,127 @@
---
description: Official skill for integrating Firebase AI Logic (Gemini API) into web applications. Covers setup, multimodal inference, structured output, and security.
metadata:
github-path: skills/firebase-ai-logic-basics
github-ref: refs/heads/main
github-repo: https://github.com/firebase/agent-skills
github-tree-sha: 7c4680cfde46eecce80e74ae4bd62f58a402f552
name: firebase-ai-logic-basics
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 arent 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
### Text-Only Generation
### Multimodal (Text + Images/Audio/Video/PDF input)
Firebase AI Logic allows Gemini models to analyze image files directly from your app. This enables features like creating captions, answering questions about images, detecting objects, and categorizing images. Beyond images, Gemini can analyze other media types like audio, video, and PDFs by passing them as inline data with their MIME type. For files larger than 20 megabytes (which can cause HTTP 413 errors as inline data), store them in Cloud Storage for Firebase and pass their URLs to the Gemini Developer API.
### Chat Session (Multi-turn)
Maintain history automatically using `startChat`.
### Streaming Responses
To improve the user experience by showing partial results as they arrive (like a typing effect), use `generateContentStream` instead of `generateContent` for faster display of results.
### Generate Images with Nano Banana
- Start with Gemini for most use cases, and choose Imagen for specialized tasks where image quality and specific styles are critical. (Example: gemini-2.5-flash-image)
- Requires an upgraded Blaze pay-as-you-go billing plan.
### Search Grounding with the built in googleSearch tool
## Supported Platforms and Frameworks
Supported Platforms and Frameworks include Kotlin and Java for Android, Swift for iOS, JavaScript for web apps, Dart for Flutter, and C Sharp for Unity.
## Advanced Features
### Structured Output (JSON)
Enforce a specific JSON schema for the response.
### On-Device AI (Hybrid)
Hybrid on-device inference for web apps, where the Firebase Javascript SDK automatically checks for Gemini Nano's availability (after installation) and switches between on-device or cloud-hosted prompt execution. This requires specific steps to enable model usage in the Chrome browser, more info in the [hybrid-on-device-inference documentation](https://firebase.google.com/docs/ai-logic/hybrid-on-device-inference.md.txt).
## Security & Production
### App Check
> [!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., `gemini-flash-lite-latest`). Use Firebase Remote Config to update model versions dynamically without deploying new client code. See [Changing model names remotely](https://firebase.google.com/docs/ai-logic/change-model-name-remotely.md.txt)
> [!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) |
**Always use the most recent version of Gemini (gemini-flash-latest) unless another model is requested by the docs or the user. DO NOT USE gemini-1.5-flash. **
## References
[Web SDK code examples and usage patterns](references/usage_patterns_web.md)
[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)

View File

@@ -0,0 +1,82 @@
# 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.
> 2. **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.
> 3. **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:** Always use **`gemini-flash-latest`**. DO NOT USE `gemini-1.5-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);
// Use the latest Gemini Flash model
final model = googleAI.generativeModel(model: 'gemini-flash-latest');
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?'));
```

View File

@@ -0,0 +1,143 @@
# 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()
// Specify a model that's appropriate for your use case.
let model = ai.generativeModel(modelName: "gemini-flash-latest")
```
## 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 {
// Initialize lazily to ensure FirebaseApp is configured first
private lazy var model = FirebaseAI.firebaseAI().generativeModel(modelName: "gemini-flash-latest")
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: "gemini-flash-latest",
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: "gemini-flash-latest",
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)")
}
```

View File

@@ -0,0 +1,152 @@
# 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
// Use a model (e.g., gemini-2.5-flash-lite)
val model = ai.generativeModel("gemini-2.5-flash-lite")
// 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
val model = ai.generativeModel("gemini-2.5-flash-lite")
lifecycleScope.launch {
val response = model.generateContent("Hello Gemini!")
setContent {
MaterialTheme {
Text("AI Response: ${response.text}")
}
}
}
}
}
```
---
### 3. Multimodal Input (Text and Images)
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
}
}
```

View File

@@ -0,0 +1,174 @@
# Firebase AI Logic Basics
## Initialization Pattern
You must initialize the ai-logic service after the main Firebase App.
```JavaScript
import { initializeApp } from "firebase/app";
import { getAI, getGenerativeModel, GoogleAIBackend } from "firebase/ai";
// If running in Firebase App Hosting, you can skip Firebase Config and instead use:
// const app = initializeApp();
const firebaseConfig = {
// ... your firebase config
};
const app = initializeApp(firebaseConfig);
// Initialize the AI Logic service (defaults to Gemini Developer API)
// To set the AI provider, set the backend as the second parameter
const ai = getAI(firebaseApp, { backend: new GoogleAIBackend() });
const generationConfig = {
candidate_count: 1,
maxOutputTokens: 2048,
stopSequences: [],
temperature: 0.7, // Balanced: creative but focused
topP: 0.95, // Standard: allows a wide range of probable tokens
topK: 40, // Standard: considers the top 40 tokens
};
// Specify the config as part of creating the `GenerativeModel` instance
const model = getGenerativeModel(ai, { model: "gemini-2.5-flash-lite", generationConfig });
```
## Core Capabilities
Text-Only Generation
```JavaScript
async function generateText(prompt) {
const result = await model.generateContent(prompt);
const response = await result.response;
return response.text();
}
```
## Multimodal (Text + Images/Audio/Video/PDF input)
Firebase AI Logic accepts Base64 encoded data or specific file references.
```JavaScript
// Helper to convert file to base64 generic object
async function fileToGenerativePart(file) {
const base64EncodedDataPromise = new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result.split(',')[1]);
reader.readAsDataURL(file);
});
return {
inlineData: {
data: await base64EncodedDataPromise,
mimeType: file.type,
},
};
}
async function analyzeImage(prompt, imageFile) {
const imagePart = await fileToGenerativePart(imageFile);
const result = await model.generateContent([prompt, imagePart]);
return result.response.text();
}
```
## Chat Session (Multi-turn)
Maintain history automatically using startChat.
```JavaScript
const chat = model.startChat({
history: [
{
role: "user",
parts: [{ text: "Hello, I am a developer." }],
},
{
role: "model",
parts: [{ text: "Great to meet you. How can I help with code?" }],
},
],
});
async function sendMessage(msg) {
const result = await chat.sendMessage(msg);
return result.response.text();
}
```
## Streaming Responses
For real-time UI updates (like a typing effect).
```JavaScript
async function streamResponse(prompt) {
const result = await model.generateContentStream(prompt);
for await (const chunk of result.stream) {
const chunkText = chunk.text();
console.log("Stream chunk:", chunkText);
// Update UI here
}
}
```
Generate Images with Nano Banana
```Javascript
import { initializeApp } from "firebase/app";
import { getAI, getGenerativeModel, GoogleAIBackend, ResponseModality } from "firebase/ai";
// Initialize FirebaseApp
const firebaseApp = initializeApp(firebaseConfig);
// Initialize the Gemini Developer API backend service
const ai = getAI(firebaseApp, { backend: new GoogleAIBackend() });
// Create a `GenerativeModel` instance with a model that supports your use case
const model = getGenerativeModel(ai, {
model: "gemini-2.5-flash-image",
// Configure the model to respond with text and images (required)
generationConfig: {
responseModalities: [ResponseModality.TEXT, ResponseModality.IMAGE],
},
});
// Provide a text prompt instructing the model to generate an image
const prompt = 'Generate an image of the Eiffel Tower with fireworks in the background.';
// To generate an image, call `generateContent` with the text input
const result = model.generateContent(prompt);
// Handle the generated image
try {
const inlineDataParts = result.response.inlineDataParts();
if (inlineDataParts?.[0]) {
const image = inlineDataParts[0].inlineData;
console.log(image.mimeType, image.data);
}
} catch (err) {
console.error('Prompt or candidate was blocked:', err);
}
```
## Advanced Features
Structured Output (JSON)
Enforce a specific JSON schema for the response.
```JavaScript
import { getGenerativeModel, Schema } from "firebase/ai";
const jsonModel = getGenerativeModel(ai, {
model: "gemini-2.5-flash-lite",
generationConfig: {
responseMimeType: "application/json",
// Optional: Define a schema
schema = Schema.object({ ... });
}
});
async function getJsonData(prompt) {
const result = await jsonModel.generateContent(prompt);
return JSON.parse(result.response.text());
}
```
On-Device AI (Hybrid)
Automatically switch between local Gemini Nano and cloud models based on device capability.
```JavaScript
import {getGenerativeModel, InferenceMode } from "firebase/ai";
const hybridModel = getGenerativeModel(ai, { mode: InferenceMode.PREFER_ON_DEVICE });
```

View File

@@ -0,0 +1,62 @@
---
description: Deploy and manage web apps with Firebase App Hosting. Use this skill when deploying Next.js/Angular apps with backends.
metadata:
github-path: skills/firebase-app-hosting-basics
github-ref: refs/heads/main
github-repo: https://github.com/firebase/agent-skills
github-tree-sha: 8e3fca710f4a8a2452a76a6e9bf0f5a666218090
name: firebase-app-hosting-basics
---
# App Hosting Basics
## Description
This skill enables the agent to deploy and manage modern, full-stack web applications (Next.js, Angular, etc.) using Firebase App Hosting.
**Important**: In order to use App Hosting, your Firebase project must be on the Blaze pricing plan. Direct the user to https://console.firebase.google.com/project/_/overview?purchaseBillingPlan=metered to upgrade their plan.
## Hosting vs App Hosting
**Choose Firebase Hosting if:**
- You are deploying a static site (HTML/CSS/JS).
- You are deploying a simple SPA (React, Vue, etc. without SSR).
- You want full control over the build and deploy process via CLI.
**Choose Firebase App Hosting if:**
- You are using a supported full-stack framework like Next.js or Angular.
- You need Server-Side Rendering (SSR) or ISR.
- You want an automated "git push to deploy" workflow with zero configuration.
## Deploying to App Hosting
### Deploy from Source
This is the recommended flow for most users.
1. Configure `firebase.json` with an `apphosting` block.
```json
{
"apphosting": {
"backendId": "my-app-id",
"rootDir": "/",
"ignore": [
"node_modules",
".git",
"firebase-debug.log",
"firebase-debug.*.log",
"functions"
]
}
}
```
2. Create or edit `apphosting.yaml`- see [Configuration](references/configuration.md) for more information on how to do so.
3. If the app needs safe access to sensitive keys, use `npx -y firebase-tools@latest apphosting:secrets` commands to set and grant access to secrets.
4. Run `npx -y firebase-tools@latest deploy` when you are ready to deploy.
### Automated deployment via GitHub (CI/CD)
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.

View File

@@ -0,0 +1,71 @@
# 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.

View File

@@ -0,0 +1,51 @@
# 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.

View File

@@ -0,0 +1,47 @@
# 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.

View File

@@ -0,0 +1,100 @@
---
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`.
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.
metadata:
github-path: skills/firebase-auth-basics
github-ref: refs/heads/main
github-repo: https://github.com/firebase/agent-skills
github-tree-sha: d9a5d375c71d4cf1f6482b8aa33231b205d41c8a
name: firebase-auth-basics
---
## 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"]
}
}
}
}
```
**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
2. Select your project.
3. Enable the desired Sign-in providers (e.g., Email/Password, Google).
### 2. Client Setup & Usage
**Web**
See [references/client_sdk_web.md](references/client_sdk_web.md).
**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).

View File

@@ -0,0 +1,157 @@
# 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
```

View File

@@ -0,0 +1,287 @@
# Firebase Authentication Web SDK
## Initialization
First, ensure you have initialized the Firebase App (see `firebase-basics` skill). Then, initialize the Auth service:
```javascript
import { getAuth } from "firebase/auth";
import { app } from "./firebase"; // Your initialized Firebase App
const auth = getAuth(app);
export { auth };
```
## Connect to Emulator
If you are running the Authentication emulator (usually on port 9099), connect to it immediately after initialization.
```javascript
import { getAuth, connectAuthEmulator } from "firebase/auth";
const auth = getAuth();
// Connect to emulator if running locally
if (location.hostname === "localhost") {
connectAuthEmulator(auth, "http://localhost:9099");
}
```
## Sign Up with Email/Password
```javascript
import { getAuth, createUserWithEmailAndPassword } from "firebase/auth";
const auth = getAuth();
createUserWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
const user = userCredential.user;
// ...
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
// ..
});
```
## Sign In with Google (Popup)
```javascript
import { getAuth, signInWithPopup, GoogleAuthProvider } from "firebase/auth";
const auth = getAuth();
const provider = new GoogleAuthProvider();
signInWithPopup(auth, provider)
.then((result) => {
// This gives you a Google Access Token. You can use it to access the Google API.
const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential.accessToken;
// The signed-in user info.
const user = result.user;
// ...
})
.catch((error) => {
// Handle Errors here.
const errorCode = error.code;
const errorMessage = error.message;
// ...
});
```
## Sign In with Facebook (Popup)
```javascript
import { getAuth, signInWithPopup, FacebookAuthProvider } from "firebase/auth";
const auth = getAuth();
const provider = new FacebookAuthProvider();
signInWithPopup(auth, provider)
.then((result) => {
// The signed-in user info.
const user = result.user;
// This gives you a Facebook Access Token. You can use it to access the Facebook API.
const credential = FacebookAuthProvider.credentialFromResult(result);
const accessToken = credential.accessToken;
})
.catch((error) => {
// Handle Errors here.
});
```
## Sign In with Apple (Popup)
```javascript
import { getAuth, signInWithPopup, OAuthProvider } from "firebase/auth";
const auth = getAuth();
const provider = new OAuthProvider('apple.com');
signInWithPopup(auth, provider)
.then((result) => {
const user = result.user;
// Apple credential
const credential = OAuthProvider.credentialFromResult(result);
const accessToken = credential.accessToken;
})
.catch((error) => {
// Handle Errors here.
});
```
## Sign In with Twitter (Popup)
```javascript
import { getAuth, signInWithPopup, TwitterAuthProvider } from "firebase/auth";
const auth = getAuth();
const provider = new TwitterAuthProvider();
signInWithPopup(auth, provider)
.then((result) => {
const user = result.user;
// Twitter credential
const credential = TwitterAuthProvider.credentialFromResult(result);
const token = credential.accessToken;
const secret = credential.secret;
})
.catch((error) => {
// Handle Errors here.
});
```
## Sign In with GitHub (Popup)
```javascript
import { getAuth, signInWithPopup, GithubAuthProvider } from "firebase/auth";
const auth = getAuth();
const provider = new GithubAuthProvider();
signInWithPopup(auth, provider)
.then((result) => {
const user = result.user;
const credential = GithubAuthProvider.credentialFromResult(result);
const token = credential.accessToken;
})
.catch((error) => {
// Handle Errors here.
});
```
## Sign In with Microsoft (Popup)
```javascript
import { getAuth, signInWithPopup, OAuthProvider } from "firebase/auth";
const auth = getAuth();
const provider = new OAuthProvider('microsoft.com');
signInWithPopup(auth, provider)
.then((result) => {
const user = result.user;
const credential = OAuthProvider.credentialFromResult(result);
const accessToken = credential.accessToken;
})
.catch((error) => {
// Handle Errors here.
});
```
## Sign In with Yahoo (Popup)
```javascript
import { getAuth, signInWithPopup, OAuthProvider } from "firebase/auth";
const auth = getAuth();
const provider = new OAuthProvider('yahoo.com');
signInWithPopup(auth, provider)
.then((result) => {
const user = result.user;
const credential = OAuthProvider.credentialFromResult(result);
const accessToken = credential.accessToken;
})
.catch((error) => {
// Handle Errors here.
});
```
## Sign In Anonymously
```javascript
import { getAuth, signInAnonymously } from "firebase/auth";
const auth = getAuth();
signInAnonymously(auth)
.then(() => {
// Signed in..
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
});
```
## Email Link Authentication
**1. Send Auth Link**
```javascript
import { getAuth, sendSignInLinkToEmail } from "firebase/auth";
const auth = getAuth();
const actionCodeSettings = {
// URL you want to redirect back to. The domain must be in the authorized domains list in Firebase Console.
url: 'https://www.example.com/finishSignUp?cartId=1234',
handleCodeInApp: true,
};
sendSignInLinkToEmail(auth, email, actionCodeSettings)
.then(() => {
// Save the email locally so you don't need to ask the user for it again
window.localStorage.setItem('emailForSignIn', email);
})
.catch((error) => {
// Error
});
```
**2. Complete Sign In (on landing page)**
```javascript
import { getAuth, isSignInWithEmailLink, signInWithEmailLink } from "firebase/auth";
const auth = getAuth();
if (isSignInWithEmailLink(auth, window.location.href)) {
let email = window.localStorage.getItem('emailForSignIn');
if (!email) {
email = window.prompt('Please provide your email for confirmation');
}
signInWithEmailLink(auth, email, window.location.href)
.then((result) => {
window.localStorage.removeItem('emailForSignIn');
// You can check result.user
})
.catch((error) => {
// Error
});
}
```
## Observe Auth State
Recommended way to get the current user. This listener triggers whenever the user signs in or out.
```javascript
import { getAuth, onAuthStateChanged } from "firebase/auth";
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
const uid = user.uid;
// ...
} else {
// User is signed out
// ...
}
});
```
## Sign Out
```javascript
import { getAuth, signOut } from "firebase/auth";
const auth = getAuth();
signOut(auth).then(() => {
// Sign-out successful.
}).catch((error) => {
// An error happened.
});
```

View File

@@ -0,0 +1,98 @@
# 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");
}
}
}
```

View File

@@ -0,0 +1,70 @@
# 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)")
}
```

View File

@@ -0,0 +1,38 @@
# 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;
```

View File

@@ -0,0 +1,106 @@
---
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.
metadata:
github-path: skills/firebase-basics
github-ref: refs/heads/main
github-repo: https://github.com/firebase/agent-skills
github-tree-sha: cc510f81744f87e8f462fca7d0c6bcc0c02de561
name: firebase-basics
---
# 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)
2. **Authentication:** Ensure you are logged in to Firebase so that commands
have the correct permissions. Run `npx -y firebase-tools@latest login`. For
environments without a browser (e.g., remote shells), use `npx -y
firebase-tools@latest login --no-localhost`.
- The command should output the current user.
- If you are not logged in, follow the interactive instructions from this
command to authenticate.
3. **Active Project:**
Most Firebase tasks require an active project context.
> [!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
> 2. **Create a new Firebase project**.
- **If using an existing Project ID:**
1. Check the current project by running `npx -y firebase-tools@latest use`.
2. If the command outputs `Active Project: <project-id>`, confirm with the user if this is the intended project.
3. 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.
2. **Prioritize official knowledge:** For any Firebase-related knowledge, consult the `developerknowledge_search_documents` MCP tool before falling back to Google Search or your internal knowledge base. Including "Firebase" in your search query significantly improves relevance.
3. **Follow Agent Skills for implementation guidance:** Skills provide opinionated workflows (CUJs), security rules, and best practices. Always consult them to understand *how* to implement Firebase features correctly instead of relying on general knowledge.
4. **Use Firebase MCP Server tools instead of direct API calls:** Whenever you need to interact with remote Firebase APIs (such as fetching Crashlytics logs or executing Data Connect queries), use the tools provided by the Firebase MCP Server instead of attempting manual API calls.
5. **Keep Plugin / Agent Skills updated:** Since Firebase best practices evolve quickly, 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)
6. **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.

View File

@@ -0,0 +1,34 @@
# 🛠️ 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>`
---

View File

@@ -0,0 +1,16 @@
# Exploring Commands
The Firebase CLI documents itself. Use help commands to discover functionality.
- **Global Help**: List all available commands and categories.
```bash
npx -y firebase-tools@latest --help
```
- **Command Help**: Get detailed usage for a specific command.
```bash
npx -y firebase-tools@latest [command] --help
# Example:
npx -y firebase-tools@latest deploy --help
npx -y firebase-tools@latest firestore:indexes --help
```

View File

@@ -0,0 +1,18 @@
# Initialization
Before initializing, check if you are already in a Firebase project directory by looking for `firebase.json`.
1. **Project Directory:**
Navigate to the root directory of the codebase.
*(Only if starting a completely new project from scratch without an existing codebase, create a directory first: `mkdir my-project && cd my-project`)*
2. **Initialize Services:**
Run the initialization command:
```bash
npx -y firebase-tools@latest init
```
The CLI will guide you through:
- Selecting features (Firestore, Functions, Hosting, etc.).
- Associating with an existing project or creating a new one.
- Configuring files (e.g. `firebase.json`, `.firebaserc`).

View File

@@ -0,0 +1,102 @@
# 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`.
2. **Download SDK**: Fetch the latest stable SDK from the [Flutter Archive](https://docs.flutter.dev/install/archive?tab=macos).
3. **Extract**: Unzip the SDK to a permanent directory (e.g., `~/development/flutter`).
4. **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
```
5. **Verify**: Run `flutter doctor` to ensure the SDK is correctly linked and initialized.
2. **Firebase CLI**: Ensure the Firebase CLI is available.
- Run `npx -y firebase-tools@latest --version`.
- Login with `npx -y firebase-tools@latest login`.
3. **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
> 2. 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`
2. **Crucial**: Re-run `flutterfire configure` to sync platform configurations.
3. 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.

View File

@@ -0,0 +1,85 @@
# 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`
2. Action: Read the Xcode project (`.pbxproj` or `Info.plist`) to determine the iOS bundle ID.
3. Register the iOS app: `npx -y firebase-tools@latest apps:create IOS <bundle-id>`
4. Fetch the config: `npx -y firebase-tools@latest apps:sdkconfig IOS <App-ID>`
5. 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
}
}
```

View File

@@ -0,0 +1,56 @@
# Firebase Local Environment Setup
This skill documents the bare minimum setup required for a full Firebase experience for the agent. Before starting to use any Firebase features, you MUST verify that each of the following steps has been completed.
## 1. Verify Node.js
- **Action**: Run `node --version`.
- **Handling**: Ensure Node.js is installed and the version is `>= 20`. If Node.js is missing or `< v20`, install it based on the operating system:
**Recommended: Use a Node Version Manager**
This avoids permission issues when installing global packages.
**For macOS or Linux:**
1. Guide the user to the [official nvm repository](https://github.com/nvm-sh/nvm#installing-and-updating).
2. Request the user to manually install `nvm` and reply when finished. **Stop and wait** for the user's confirmation.
3. Make `nvm` available in the current terminal session by sourcing the appropriate profile:
```bash
# For Bash
source ~/.bash_profile
source ~/.bashrc
# For Zsh
source ~/.zprofile
source ~/.zshrc
```
4. Install Node.js:
```bash
nvm install 24
nvm use 24
```
**For Windows:**
1. Guide the user to download and install [nvm-windows](https://github.com/coreybutler/nvm-windows/releases).
2. Request the user to manually install `nvm-windows` and Node.js, and reply when finished. **Stop and wait** for the user's confirmation.
3. After the user confirms, verify Node.js is available:
```bash
node --version
```
**Alternative: Official Installer**
1. Guide the user to download and install the LTS version from [nodejs.org](https://nodejs.org/en/download).
2. Request the user to manually install Node.js and reply when finished. **Stop and wait** for the user's confirmation.
## 2. Verify Firebase CLI
- **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.

View File

@@ -0,0 +1,27 @@
# 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
```
2. **Check Currently Installed Skills:** Check the contents of the skills directory to see what is currently installed:
```bash
ls -la ~/.agents/skills
```
3. **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`.
4. **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`.

View File

@@ -0,0 +1,46 @@
# Refresh Antigravity Local Environment
Follow these steps to refresh Antigravity's local environment, ensuring that their agent skills and plugins are fully up-to-date.
Antigravity relies on `npx skills`. Updates should prioritize the global level; if skills are installed at the project level, omit the `--global` flag.
1. **List Available Skills:** Identify all Firebase skills available in the repository:
```bash
npx -y skills add firebase/agent-skills --list
```
2. **Check Currently Installed Skills:** Determine which skills are already installed and linked at both project and global levels:
```bash
# Check project-level skills
npx -y skills list --agent antigravity
# Check global-level skills
npx -y skills list --agent antigravity --global
```
*Note: If a skill shows `Agents: not linked` in the output, it is installed but not currently available to Antigravity.*
3. **Add Missing or Unlinked Skills:** Compare the results from Step 1 and Step 2. For each missing or unlinked skill, follow the appropriate conditional instruction below:
- **IF any Firebase skills were found at the PROJECT level in Step 2:** Add the missing skill to the project level:
```bash
npx -y skills add firebase/agent-skills --agent antigravity --skill "<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
```
4. **Update Existing Skills:** Update all currently installed skills to their latest versions:
```bash
# Update project-level skills
npx -y skills update --agent antigravity --yes
# Update global-level skills
npx -y skills update --agent antigravity --global --yes
```

View File

@@ -0,0 +1,10 @@
# Refresh Claude Code Local Environment
Follow these steps to refresh Claude Code's local environment, ensuring that their agent skills and plugins are fully up-to-date.
Use Claude Code's native plugin manager instead of `npx`.
1. **Update the Plugin:** Run the specific CLI command to update the Firebase plugin:
```bash
claude plugin update firebase@firebase
```

View File

@@ -0,0 +1,11 @@
# Refresh Gemini CLI Local Environment
Follow these steps to refresh Gemini CLI's local environment, ensuring that their agent skills and plugins are fully up-to-date.
Use the native Gemini CLI extension manager instead of `npx`.
1. **Update the Extension:** Run the specific CLI command to update:
```bash
gemini extensions update firebase
```
*Note: If the extension is named differently, replace `firebase` with the correct name from `gemini extensions list`.*

View File

@@ -0,0 +1,48 @@
# Refresh Other Local Environment
Follow these steps to refresh the local environment of other agents, ensuring that their agent skills and plugins are fully up-to-date.
Other agents rely on `npx skills`. Updates should prioritize the global level; if skills are installed at the project level, omit the `--global` flag.
Replace `<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
```
2. **Check Currently Installed Skills:** Determine which skills are already installed and linked for the agent at both project and global levels:
```bash
# Check project-level skills
npx -y skills list --agent <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.*
3. **Add Missing or Unlinked Skills:** Compare the results from Step 1 and Step 2. For each missing or unlinked skill, follow the appropriate conditional instruction below:
- **IF any Firebase skills were found at the PROJECT level in Step 2:** Add the missing skill to the project level:
```bash
npx -y skills add firebase/agent-skills --agent <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
```
4. **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
```

View File

@@ -0,0 +1,19 @@
# 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.

View File

@@ -0,0 +1,63 @@
# Antigravity Setup
To get the most out of Firebase in Antigravity, follow these steps to install the agent skills and the MCP server.
### 1. Install and Verify Firebase Skills
Check if the skills are already installed before proceeding:
1. **Check Local skills**: Run `ls -d .agent/skills/firebase-basics` or `ls -d .agents/skills/firebase-basics`. If the directory exists, the skills are already installed locally.
2. **Check Global skills**: If not found locally, check the global installation by running:
```bash
npx skills list --global --agent antigravity
```
If the output includes `firebase-basics`, the skills are already installed globally.
3. **Install Skills**: If both checks fail, run the following command to install the Firebase agent skills:
```bash
npx skills add firebase/agent-skills --agent antigravity --skill "*"
```
*Note: Omit `--yes` and `--global` to choose the installation location manually. If prompted interactively in the terminal, ensure you send the appropriate user choices via standard input to complete the installation.*
4. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that `firebase-basics` is now available.
### 2. Configure and Verify Firebase MCP Server
The MCP server allows Antigravity to interact directly with Firebase projects. 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.*
2. **Check Existing Configuration**: Open `mcp_config.json` and check the `mcpServers` section for a `firebase` entry.
- It is already configured if the `command` is `"firebase"` OR if the `command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`.
- **Important**: If a valid `firebase` entry is found, the MCP server is already configured. **Skip step 3** and proceed directly to step 4.
**Example valid configurations**:
```json
"firebase": {
"command": "npx",
"args": ["-y", "firebase-tools@latest", "mcp"]
}
```
OR
```json
"firebase": {
"command": "firebase",
"args": ["mcp"]
}
```
3. **Add or Update Configuration**: If the `firebase` block is missing or incorrect, add it to the `mcpServers` object:
```json
"firebase": {
"command": "npx",
"args": [
"-y",
"firebase-tools@latest",
"mcp"
]
}
```
*CRITICAL: Merge this configuration into the existing `mcp_config.json` file. You MUST preserve any other existing servers inside the `mcpServers` object.*
4. **Verify Configuration**: Save the file and confirm the `firebase` block is present and properly formatted JSON.
### 3. Restart and Verify Connection
1. **Restart Antigravity**: Instruct the user to restart the Antigravity application. **Stop and wait** for their confirmation before proceeding.
2. **Confirm Connection**: Check the MCP server list in the Antigravity UI to confirm that the Firebase MCP server is connected.

View File

@@ -0,0 +1,30 @@
# Claude Code Setup
To get the most out of Firebase in Claude Code, follow these steps to install the agent skills and the MCP server.
## Recommended Method: Using Plugins
The recommended method is using the plugin marketplace to install both the agent skills and the MCP functionality.
### 1. Install and Verify Plugins
Check if the plugins are already installed before proceeding:
1. **Check Existing Skills**: Run `npx skills list --agent claude-code` to check for local skills. Run `npx skills list --global --agent claude-code` to check for global skills. Note whether the output includes `firebase-basics`.
2. **Check Existing MCP Configuration**: Run `claude mcp list -s user` and `claude mcp list -s project`. Note whether the output of either command includes `firebase`.
3. **Determine Installation Path**:
- If **both** skills and MCP configuration are found, the plugin is fully installed. **Stop here and skip all remaining setup steps in this document.**
- If **neither** are found, proceed to step 4.
- If **only one** is found (e.g., skills are installed but MCP is missing, or vice versa), **stop and prompt the user**. Explain the mixed state and ask if they want to proceed with installing the Firebase plugin before continuing to step 4.
4. **Add Marketplace**: Run the following command to add the marketplace (this uses the default User scope):
```bash
claude plugin marketplace add firebase/agent-skills
```
5. **Install Plugins**: Run the following command to install the plugin:
```bash
claude plugin install firebase@firebase
```
6. **Verify Installation**: Re-run the checks in steps 1 and 2 to confirm the skills and the MCP server are now available.
### 2. Restart and Verify Connection
1. **Restart Claude Code**: Instruct the user to restart Claude Code. **Stop and wait** for their confirmation before proceeding.

View File

@@ -0,0 +1,63 @@
# Cursor Setup
To get the most out of Firebase in Cursor, follow these steps to install the agent skills and the MCP server.
### 1. Install and Verify Firebase Skills
Check if the skills are already installed before proceeding:
1. **Check Local skills**: Run `npx skills list --agent cursor`. If the output includes `firebase-basics`, the skills are already installed locally.
2. **Check Global skills**: If not found locally, check the global installation by running:
```bash
npx skills list --global --agent cursor
```
If the output includes `firebase-basics`, the skills are already installed globally.
3. **Install Skills**: If both checks fail, run the following command to install the Firebase agent skills:
```bash
npx skills add firebase/agent-skills --agent cursor --skill "*"
```
*Note: Omit `--yes` and `--global` to choose the installation location manually. If prompted interactively in the terminal, ensure you send the appropriate user choices via standard input to complete the installation.*
4. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that `firebase-basics` is now available.
### 2. Configure and Verify Firebase MCP Server
The MCP server allows Cursor to interact directly with Firebase projects.
1. **Locate `mcp.json`**: Find the configuration file for your operating system:
- Global: `~/.cursor/mcp.json`
- Project: `.cursor/mcp.json`
*Note: If the directory or `mcp.json` file does not exist, create them and initialize the file with `{ "mcpServers": {} }` before proceeding.*
2. **Check Existing Configuration**: Open `mcp.json` and check the `mcpServers` section for a `firebase` entry.
- It is already configured if the `command` is `"firebase"` OR if the `command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`.
- **Important**: If a valid `firebase` entry is found, the MCP server is already configured. **Skip step 3** and proceed directly to step 4.
**Example valid configurations**:
```json
"firebase": {
"command": "npx",
"args": ["-y", "firebase-tools@latest", "mcp"]
}
```
OR
```json
"firebase": {
"command": "firebase",
"args": ["mcp"]
}
```
3. **Add or Update Configuration**: If the `firebase` block is missing or incorrect, add it to the `mcpServers` object:
```json
"firebase": {
"command": "npx",
"args": [
"-y",
"firebase-tools@latest",
"mcp"
]
}
```
*CRITICAL: Merge this configuration into the existing `mcp.json` file. You MUST preserve any other existing servers inside the `mcpServers` object.*
4. **Verify Configuration**: Save the file and confirm the `firebase` block is present and properly formatted JSON.
### 3. Restart and Verify Connection
1. **Restart Cursor**: Instruct the user to restart the Cursor application. **Stop and wait** for their confirmation before proceeding.
2. **Confirm Connection**: Check the MCP server list in the Cursor UI to confirm that the Firebase MCP server is connected.

View File

@@ -0,0 +1,39 @@
# Gemini CLI Setup
To get the most out of Firebase in the Gemini CLI, follow these steps to install the agent extension and the MCP server.
## Recommended: Installing Extensions
The best way to get both the agent skills and the MCP server is via the Gemini extension.
### 1. Install and Verify Firebase Extension
Check if the extension is already installed before proceeding:
1. **Check Existing Extensions**: Run `gemini extensions list`. If the output includes `firebase`, the extension is already installed.
2. **Install Extension**: If not found, run the following command to install the Firebase agent skills and MCP server:
```bash
gemini extensions install https://github.com/firebase/agent-skills
```
3. **Verify Installation**: Run the following checks to confirm installation:
- `gemini mcp list` -> Output should include `firebase-tools`.
- `gemini skills list` -> Output should include `firebase-basic`.
### 2. Restart and Verify Connection
1. **Restart Gemini CLI**: Instruct the user to restart the Gemini CLI if any new installation occurred. **Stop and wait** for their confirmation before proceeding.
---
## Alternative: Manual MCP Configuration (Project Scope)
If the user only wants to use the MCP server for the current project:
### 1. Configure and Verify Firebase MCP Server
1. **Check Existing Configuration**: Run `gemini mcp list`. If the output includes `firebase-tools`, the MCP server is already configured.
2. **Add the MCP Server**: If not found, run the following command to configure the Firebase MCP Server:
```bash
gemini mcp add -e IS_GEMINI_CLI_EXTENSION=true firebase npx -y firebase-tools@latest mcp
```
3. **Verify Configuration**: Re-run `gemini mcp list` to confirm `firebase-tools` is connected.
### 2. Restart and Verify Connection
1. **Restart Gemini CLI**: Instruct the user to restart the Gemini CLI. **Stop and wait** for their confirmation before proceeding.

View File

@@ -0,0 +1,70 @@
# GitHub Copilot Setup
To get the most out of Firebase with GitHub Copilot in VS Code, follow these steps to install the agent skills and the MCP server.
## Recommended: Global Setup
The agent skills and MCP server should be installed globally for consistent access across projects.
### 1. Install and Verify Firebase Skills
Check if the skills are already installed before proceeding:
1. **Check Local skills**: Run `npx skills list --agent github-copilot`. If the output includes `firebase-basics`, the skills are already installed locally.
2. **Check Global skills**: If not found locally, check the global installation by running:
```bash
npx skills list --global --agent github-copilot
```
If the output includes `firebase-basics`, the skills are already installed globally.
3. **Install Skills**: If both checks fail, run the following command to install the Firebase agent skills:
```bash
npx skills add firebase/agent-skills --agent github-copilot --skill "*"
```
*Note: Omit `--yes` and `--global` to choose the installation location manually. If prompted interactively in the terminal, ensure you send the appropriate user choices via standard input to complete the installation.*
4. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that `firebase-basics` is now available.
### 2. Configure and Verify Firebase MCP Server
The MCP server allows GitHub Copilot to interact directly with Firebase projects.
1. **Locate `mcp.json`**: Find the configuration file for your environment:
- Workspace: `.vscode/mcp.json`
- Global: User Settings `mcp.json` file.
*Note: If the `.vscode/` directory or `mcp.json` file does not exist, create them and initialize the file with `{ "mcp": { "servers": {} } }` before proceeding.*
2. **Check Existing Configuration**: Open the `mcp.json` file and check the `mcp.servers` object for a `firebase` entry.
- It is already configured if the `command` is `"firebase"` OR if the `command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`.
- **Important**: If a valid `firebase` entry is found, the MCP server is already configured. **Skip step 3** and proceed directly to step 4.
**Example valid configurations**:
```json
"firebase": {
"type": "stdio",
"command": "npx",
"args": ["-y", "firebase-tools@latest", "mcp"]
}
```
OR
```json
"firebase": {
"type": "stdio",
"command": "firebase",
"args": ["mcp"]
}
```
3. **Add or Update Configuration**: If the `firebase` block is missing or incorrect, add it to the `mcp.servers` object:
```json
"firebase": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"firebase-tools@latest",
"mcp"
]
}
```
*CRITICAL: Merge this configuration into the existing `mcp.json` file under the `mcp.servers` object. You MUST preserve any other existing servers inside `mcp.servers`.*
4. **Verify Configuration**: Save the file and confirm the `firebase` block is present and properly formatted JSON.
### 3. Restart and Verify Connection
1. **Restart VS Code**: Instruct the user to restart VS Code. **Stop and wait** for their confirmation before proceeding.
2. **Confirm Connection**: Check the MCP server list in the VS Code Copilot UI to confirm that the Firebase MCP server is connected.

View File

@@ -0,0 +1,65 @@
# Other Agents Setup
If you use another agent (like Windsurf, Cline, or Claude Desktop), follow these steps to install the agent skills and the MCP server.
## Recommended: Global Setup
The agent skills and MCP server should be installed globally for consistent access across projects.
### 1. Install and Verify Firebase Skills
Check if the skills are already installed before proceeding:
1. **Check Local skills**: Run `npx skills list --agent <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).
2. **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.
3. **Install Skills**: If both checks fail, run the following command to install the Firebase agent skills:
```bash
npx skills add firebase/agent-skills --agent <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.*
4. **Verify Installation**: Re-run the checks in steps 1 or 2 to confirm that `firebase-basics` is now available.
### 2. Configure and Verify Firebase MCP Server
The MCP server allows the agent to interact directly with Firebase projects.
1. **Locate MCP Configuration**: Find the configuration file for your agent (e.g., `~/.codeium/windsurf/mcp_config.json`, `cline_mcp_settings.json`, or `claude_desktop_config.json`).
*Note: If the document or its containing directory does not exist, create them and initialize the file with `{ "mcpServers": {} }` before proceeding.*
2. **Check Existing Configuration**: Open the configuration file and check the `mcpServers` section for a `firebase` entry.
- It is already configured if the `command` is `"firebase"` OR if the `command` is `"npx"` with `"firebase-tools"` and `"mcp"` in the `args`.
- **Important**: If a valid `firebase` entry is found, the MCP server is already configured. **Skip step 3** and proceed directly to step 4.
**Example valid configurations**:
```json
"firebase": {
"command": "npx",
"args": ["-y", "firebase-tools@latest", "mcp"]
}
```
OR
```json
"firebase": {
"command": "firebase",
"args": ["mcp"]
}
```
3. **Add or Update Configuration**: If the `firebase` block is missing or incorrect, add it to the `mcpServers` object:
```json
"firebase": {
"command": "npx",
"args": [
"-y",
"firebase-tools@latest",
"mcp"
]
}
```
*CRITICAL: Merge this configuration into the existing file. You MUST preserve any other existing servers inside the `mcpServers` object.*
4. **Verify Configuration**: Save the file and confirm the `firebase` block is present and properly formatted JSON.
### 3. Restart and Verify Connection
1. **Restart Agent**: Instruct the user to restart the agent application. **Stop and wait** for their confirmation before proceeding.
2. **Confirm Connection**: Check the MCP server list in the agent's UI to confirm that the Firebase MCP server is connected.

View File

@@ -0,0 +1,69 @@
# 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()}`);
});
}
```

View File

@@ -0,0 +1,38 @@
---
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`.
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.
metadata:
github-path: skills/firebase-crashlytics
github-ref: refs/heads/main
github-repo: https://github.com/firebase/agent-skills
github-tree-sha: 155d48db65cb4bc2e44f2cbdf9ec5c4adbf8ef9c
name: firebase-crashlytics
---
# 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)

View File

@@ -0,0 +1,122 @@
# 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")
}
```
2. 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")
}
```
2. 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)
```
2. Run your app on a device or emulator. The app should crash after a short delay.
3. Restart the app. The Crashlytics SDK will send the crash report to Firebase on the next app launch.
4. 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).

View File

@@ -0,0 +1,81 @@
# 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.
2. **Create Firebase Project:** If no project exists, create one:
`npx -y firebase-tools@latest projects:create <project-id> --display-name="My Awesome App"`
3. **Create Firebase App:** Register the iOS app with the discovered bundle ID:
`npx -y firebase-tools@latest apps:create IOS <bundle-id>`
4. **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`.
2. **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.
3. Restart the app. The Crashlytics SDK will send the crash report to Firebase on the next app launch.
4. 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).

View File

@@ -0,0 +1,158 @@
---
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.
metadata:
github-path: skills/firebase-data-connect-basics
github-ref: refs/heads/main
github-repo: https://github.com/firebase/agent-skills
github-tree-sha: bfbdd91790e2a2de14faef9a76a0a6c4e94f0c77
name: firebase-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
├── 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/`).
2. **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/advanced.md](reference/advanced.md) | `Vector`, `@col(dataType: "vector")` |
| **Full-Text Search** | [reference/advanced.md](reference/advanced.md) | `@searchable` |
| **Upserting Data** | [reference/operations.md](reference/operations.md) | `_upsert` mutations |
| **Complex Filters** | [reference/operations.md](reference/operations.md) | `_or`, `_and`, `_not`, `eq`, `contains` |
| **Transactions** | [reference/operations.md](reference/operations.md) | `@transaction`, `response` binding |
| **Environment Config** | [reference/config.md](reference/config.md) | `dataconnect.yaml`, `connector.yaml` |
| **Realtime Subscriptions** | [reference/realtime.md](reference/realtime.md) | `@refresh`, `subscribe()`, auto-refresh |
| **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.
2. Run `npx -y firebase-tools@latest init dataconnect`.
3. 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`.
2. Write schema and operations.
3. Run `npx -y firebase-tools@latest dataconnect:compile` or `npx -y firebase-tools@latest dataconnect:sdk:generate` to
validate them.
4. 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)**.

View File

@@ -0,0 +1,629 @@
# 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`.*

View File

@@ -0,0 +1,303 @@
# Advanced Features Reference
## Contents
- [Vector Similarity Search](#vector-similarity-search)
- [Full-Text Search](#full-text-search)
- [Cloud Functions Integration](#cloud-functions-integration)
- [Data Seeding & Bulk Operations](#data-seeding--bulk-operations)
---
## Vector Similarity Search
Semantic search using Vertex AI embeddings and PostgreSQL's `pgvector`.
### Schema Setup
```graphql
type Movie @table {
id: UUID! @default(expr: "uuidV4()")
title: String!
description: String
# Vector field for embeddings - size must match model output (768 for gecko)
descriptionEmbedding: Vector! @col(size: 768)
}
```
### Generate Embeddings in Mutations
Use `_embed` server value to auto-generate embeddings via Vertex AI:
```graphql
mutation CreateMovieWithEmbedding($title: String!, $description: String!)
@auth(level: USER) {
movie_insert(data: {
title: $title,
description: $description,
descriptionEmbedding_embed: {
model: "textembedding-gecko@003",
text: $description
}
})
}
```
### Similarity Search Query
SQL Connect generates `_similarity` fields for Vector columns:
```graphql
query SearchMovies($query: String!) @auth(level: PUBLIC) {
movies_descriptionEmbedding_similarity(
compare_embed: { model: "textembedding-gecko@003", text: $query },
method: L2, # L2, COSINE, or INNER_PRODUCT
within: 2.0, # Max distance threshold
limit: 5
) {
id
title
description
_metadata { distance } # See how close each result is
}
}
```
### Similarity Parameters
| Parameter | Description |
|-----------|-------------|
| `compare` | Raw Vector to compare against |
| `compare_embed` | Generate embedding from text via Vertex AI |
| `method` | Distance function: `L2`, `COSINE`, `INNER_PRODUCT` |
| `within` | Max distance (results further are excluded) |
| `where` | Additional filters |
| `limit` | Max results to return |
### Custom Embeddings
Pass pre-computed vectors directly:
```graphql
mutation StoreCustomEmbedding($id: UUID!, $embedding: Vector!) @auth(level: USER) {
movie_update(id: $id, data: { descriptionEmbedding: $embedding })
}
query SearchWithCustomVector($vector: Vector!) @auth(level: PUBLIC) {
movies_descriptionEmbedding_similarity(
compare: $vector,
method: COSINE,
limit: 10
) { id title }
}
```
---
## Full-Text Search
Fast keyword/phrase search using PostgreSQL's full-text capabilities.
### Enable with @searchable
```graphql
type Movie @table {
title: String! @searchable
description: String @searchable(language: "english")
genre: String @searchable
}
```
### Search Query
SQL Connect generates `_search` fields:
```graphql
query SearchMovies($query: String!) @auth(level: PUBLIC) {
movies_search(
query: $query,
queryFormat: QUERY, # QUERY, PLAIN, PHRASE, or ADVANCED
limit: 20
) {
id title description
_metadata { relevance } # Relevance score
}
}
```
### Query Formats
| Format | Description |
|--------|-------------|
| `QUERY` | Web-style (default): quotes, AND, OR supported |
| `PLAIN` | Match all words, any order |
| `PHRASE` | Match exact phrase |
| `ADVANCED` | Full tsquery syntax |
### Tuning Results
```graphql
query SearchWithThreshold($query: String!) @auth(level: PUBLIC) {
movies_search(
query: $query,
relevanceThreshold: 0.05, # Min relevance score
where: { genre: { eq: "Action" }},
orderBy: [{ releaseYear: DESC }]
) { id title }
}
```
### Supported Languages
`english` (default), `french`, `german`, `spanish`, `italian`, `portuguese`, `dutch`, `danish`, `finnish`, `norwegian`, `swedish`, `russian`, `arabic`, `hindi`, `simple`
---
## Cloud Functions Integration
Trigger Cloud Functions when mutations execute.
### Basic Trigger (Node.js)
```typescript
import { onMutationExecuted } from "firebase-functions/dataconnect";
import { logger } from "firebase-functions";
export const onUserCreate = onMutationExecuted(
{
service: "myService",
connector: "default",
operation: "CreateUser",
region: "us-central1" # Must match SQL Connect location
},
(event) => {
const variables = event.data.payload.variables;
const returnedData = event.data.payload.data;
logger.info("User created:", returnedData);
// Send welcome email, sync to analytics, etc.
}
);
```
### Basic Trigger (Python)
```python
from firebase_functions import dataconnect_fn, logger
@dataconnect_fn.on_mutation_executed(
service="myService",
connector="default",
operation="CreateUser"
)
def on_user_create(event: dataconnect_fn.Event):
variables = event.data.payload.variables
returned_data = event.data.payload.data
logger.info("User created:", returned_data)
```
### Event Data
```typescript
// event.authType: "app_user" | "unauthenticated" | "admin"
// event.authId: Firebase Auth UID (for app_user)
// event.data.payload.variables: mutation input variables
// event.data.payload.data: mutation response data
// event.data.payload.errors: any errors that occurred
```
### Filtering with Wildcards
```typescript
// Trigger on all User* mutations
export const onUserMutation = onMutationExecuted(
{ operation: "User*" },
(event) => { /* ... */ }
);
// Capture operation name
export const onAnyMutation = onMutationExecuted(
{ service: "myService", operation: "{operationName}" },
(event) => {
console.log("Operation:", event.params.operationName);
}
);
```
### Use Cases
- **Data sync**: Replicate to Firestore, BigQuery, external APIs
- **Notifications**: Send emails, push notifications on events
- **Async workflows**: Image processing, data aggregation
- **Audit logging**: Track all data changes
> ⚠️ **Avoid infinite loops**: Don't trigger mutations that would fire the same trigger. Use filters to exclude self-triggered events.
---
## Data Seeding & Bulk Operations
### Local Prototyping with _insertMany
```graphql
mutation SeedMovies @transaction {
movie_insertMany(data: [
{ id: "uuid-1", title: "Movie 1", genre: "Action" },
{ id: "uuid-2", title: "Movie 2", genre: "Drama" },
{ id: "uuid-3", title: "Movie 3", genre: "Comedy" }
])
}
```
### Reset Data with _upsertMany
```graphql
mutation ResetData {
movie_upsertMany(data: [
{ id: "uuid-1", title: "Movie 1", genre: "Action" },
{ id: "uuid-2", title: "Movie 2", genre: "Drama" }
])
}
```
### Clear All Data
```graphql
mutation ClearMovies {
movie_deleteMany(all: true)
}
```
### Production: Admin SDK Bulk Operations
```typescript
import { initializeApp } from 'firebase-admin/app';
import { getDataConnect } from 'firebase-admin/data-connect';
const app = initializeApp();
const dc = getDataConnect({ location: "us-central1", serviceId: "my-service" });
const movies = [
{ id: "uuid-1", title: "Movie 1", genre: "Action" },
{ id: "uuid-2", title: "Movie 2", genre: "Drama" }
];
// Bulk insert
await dc.insertMany("movie", movies);
// Bulk upsert
await dc.upsertMany("movie", movies);
// Single operations
await dc.insert("movie", movies[0]);
await dc.upsert("movie", movies[0]);
```
### Emulator Data Persistence
```bash
# Export emulator data
npx -y firebase-tools@latest emulators:export ./seed-data
# Start with saved data
npx -y firebase-tools@latest emulators:start --only dataconnect --import=./seed-data
```

View File

@@ -0,0 +1,267 @@
# Configuration Reference
## Contents
- [Project Structure](#project-structure)
- [dataconnect.yaml](#dataconnectyaml)
- [connector.yaml](#connectoryaml)
- [Firebase CLI Commands](#firebase-cli-commands)
- [Emulator](#emulator)
- [Deployment](#deployment)
---
## Project Structure
```
project-root/
├── firebase.json # Firebase project config
└── dataconnect/
├── dataconnect.yaml # Service configuration
├── schema/
│ └── schema.gql # Data model (types, relationships)
└── connector/
├── connector.yaml # Connector config + SDK generation
├── queries.gql # Query operations
└── mutations.gql # Mutation operations (optional separate file)
```
---
## dataconnect.yaml
Main 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
2. **Generate SQL diff**: `npx -y firebase-tools@latest dataconnect:sql:diff`
3. **Review migration**: Check breaking changes
4. **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
}
```

View File

@@ -0,0 +1,122 @@
# 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.
2. **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;
$$;
```

View File

@@ -0,0 +1,376 @@
# 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).
2. **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.
3. **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 })
}
```

View File

@@ -0,0 +1,179 @@
# 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.

View File

@@ -0,0 +1,278 @@
# Schema Reference
## Contents
- [Defining Types](#defining-types)
- [Core Directives](#core-directives)
- [Relationships](#relationships)
- [Data Types](#data-types)
- [Enumerations](#enumerations)
---
## Defining Types
Types with `@table` map to PostgreSQL tables. 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
}
}
}
```

View File

@@ -0,0 +1,121 @@
# 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"
});
```

View File

@@ -0,0 +1,107 @@
# 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`

View File

@@ -0,0 +1,116 @@
# 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`

View File

@@ -0,0 +1,140 @@
# 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`

View File

@@ -0,0 +1,124 @@
# 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`

View File

@@ -0,0 +1,289 @@
# Security Reference
## Contents
- [@auth Directive](#auth-directive)
- [Access Levels](#access-levels)
- [CEL Expressions](#cel-expressions)
- [@check and @redact](#check-and-redact)
- [Authorization Patterns](#authorization-patterns)
- [Anti-Patterns](#anti-patterns)
---
## @auth Directive
Every deployable query/mutation must have `@auth`. Without it, operations default to `NO_ACCESS`.
```graphql
query PublicData @auth(level: PUBLIC) { ... }
query UserData @auth(level: USER) { ... }
query AdminOnly @auth(expr: "auth.token.admin == true") { ... }
```
| Argument | Description |
|----------|-------------|
| `level` | Preset access level |
| `expr` | CEL expression (alternative to level) |
| `insecureReason` | Suppress deploy warning for PUBLIC/unfiltered USER |
---
## Access Levels
| Level | Who Can Access | CEL Equivalent |
|-------|----------------|----------------|
| `PUBLIC` | Anyone, authenticated or not | `true` |
| `USER_ANON` | Any authenticated user (including anonymous) | `auth.uid != nil` |
| `USER` | Authenticated users (excludes anonymous) | `auth.uid != nil && auth.token.firebase.sign_in_provider != 'anonymous'` |
| `USER_EMAIL_VERIFIED` | Users with verified email | `auth.uid != nil && auth.token.email_verified` |
| `NO_ACCESS` | Admin SDK only | `false` |
> **Important:** Levels like `USER` are starting points. Always add filters or expressions to verify the user can access specific data.
---
## CEL Expressions
### Available Bindings
| Binding | Description |
|---------|-------------|
| `auth.uid` | Current user's Firebase UID |
| `auth.token` | Auth token claims (see below) |
| `vars` | Operation variables (e.g., `vars.movieId`) |
| `request.time` | Server timestamp |
| `request.operationName` | "query" or "mutation" |
### auth.token Fields
| Field | Description |
|-------|-------------|
| `email` | User's email address |
| `email_verified` | Boolean: email verified |
| `phone_number` | User's phone |
| `name` | Display name |
| `sub` | Firebase UID (same as auth.uid) |
| `firebase.sign_in_provider` | `password`, `google.com`, `anonymous`, etc. |
| `<custom_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.

View File

@@ -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();
```

View File

@@ -0,0 +1,65 @@
---
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`.
description: Sets up, manages, and executes queries against Cloud Firestore database instances. 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, or checking indexes.
metadata:
github-path: skills/firebase-firestore
github-ref: refs/heads/main
github-repo: https://github.com/firebase/agent-skills
github-tree-sha: 8f3c31a1b64bb957c1d76a502da66b62c0a8141a
name: firebase-firestore
---
# 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>`
2. Ask the user which database instance they wish to target or if they would
prefer to create a new instance.
3. 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**: 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)

View File

@@ -0,0 +1,142 @@
# Firestore Enterprise Native Mode on Android (Kotlin)
This guide walks you through using the Cloud Firestore SDK in your Android app using Kotlin. The SDK for Firestore Enterprise Native Mode is the same as the standard Cloud Firestore SDK.
### 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
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. Basic CRUD Operations
The operations are identical to standard Firestore.
#### Add Data
```kotlin
val user = hashMapOf(
"first" to "Alan",
"last" to "Turing",
"born" to 1912
)
db.collection("users")
.add(user)
.addOnSuccessListener { documentReference ->
Log.d(TAG, "DocumentSnapshot added with ID: ${documentReference.id}")
}
.addOnFailureListener { e ->
Log.w(TAG, "Error adding document", e)
}
```
#### Read Data
```kotlin
db.collection("users")
.get()
.addOnSuccessListener { result ->
for (document in result) {
Log.d(TAG, "${document.id} => ${document.data}")
}
}
.addOnFailureListener { exception ->
Log.w(TAG, "Error getting documents.", exception)
}
```
#### Update Data
```kotlin
val userRef = db.collection("users").document("your-document-id")
userRef
.update("born", 1913)
.addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully updated!") }
.addOnFailureListener { e -> Log.w(TAG, "Error updating document", e) }
```
#### Delete Data
```kotlin
db.collection("users").document("your-document-id")
.delete()
.addOnSuccessListener { Log.d(TAG, "DocumentSnapshot successfully deleted!") }
.addOnFailureListener { e -> Log.w(TAG, "Error deleting document", e) }
```

View File

@@ -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)`

View File

@@ -0,0 +1,172 @@
# 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),
),
);
},
);
},
);
```

View File

@@ -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`

View File

@@ -0,0 +1,162 @@
# 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()
}
}
```

View File

@@ -0,0 +1,117 @@
# Provisioning Firestore Enterprise Native Mode
## Manual Initialization
Initialize the following firebase configuration files manually. Do not use `npx
-y firebase-tools@latest init`, as it expects interactive inputs.
1. **Create a Firestore Enterprise Database**: Create a Firestore Enterprise
database using the Firebase CLI.
2. **Create `firebase.json`**: This file contains database configuration for
the Firebase CLI.
3. **Create `firestore.rules`**: This file contains your security rules.
4. **Create `firestore.indexes.json`**: This file contains your index
definitions.
### 1. Create a Firestore Enterprise Database
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).

View File

@@ -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()}")
```

View File

@@ -0,0 +1,569 @@
## 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
2. **Document your findings** in a untracked file. Refer to this file when
generating the security rules.
#### Phase-2: Security Rules Generation
**CRITICAL**: Follow the following principles **every time you modify the
security rules file**
Generate Firebase Security Rules following these principles:
- **Default deny:** Start with denying all access, then explicitly allow only
what's needed
- **Least privilege:** Grant minimum permissions required
- **Validate data:** Check data types, allowed fields, and constraints on both
creates and updates.
- **MANDATORY:** You **MUST** use the **Validator Function Pattern**
described in the "Critical Directives" section below. This involves
defining a specific validation function (e.g., `isValidUser`) and
calling it in **BOTH** `create` and `update` rules.
- **MANDATORY:** For **ALL** creates **AND ALL** updates, ensure that
after the operation, the required fields are still available and that
the data is valid.
- **Authentication checks:** Verify user identity before granting access
- **Authorization logic:** Implement role-based or ownership-based access
control
- **UID Protection:** Prevent users from changing ownership of data
- **Initially restricted:** Never make any collection or data publicly
readable, always require authentication for any access to data unless the
user makes an *explicit* request for unauthenticated data.
This means the first firestore.rules file you generate must never have any
"allow read: true" statements.
**Structure Requirements:**
1. **Document assumed data models at the beginning of the rules file:**
```javascript
// ===============================================================
// Assumed Data Model
// ===============================================================
//
// This security rules file assumes the following data structures:
//
// Collection: [name]
// Document ID: [pattern]
// Fields:
// - field1: type (required/optional, constraints) - description
// - field2: type (required/optional, constraints) - description
// [List all fields with types, constraints, and whether immutable]
//
// [Repeat for all collections]
//
// ===============================================================
```
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`)?
2. **Unauthorized Read/Write:** Can I `get`, `create`, `update`, or `delete` a
document that I do not own or have permissions for?
3. **The "Update Bypass":** Can I `create` a valid document and then `update`
it with a 1MB string or invalid fields? (Tests if validation logic is
missing from `update`).
4. **Ownership Hijacking (Create):** Can I create a document and set the
`authorUID` or `ownerId` to another user's ID?
5. **Ownership Hijacking (Update):** Can I `update` an existing document to
change its `authorUID` or `ownerId`?
6. **Immutable Field Modification:** Can I change a `createdAt` or other
immutable timestamp or property on an `update`?
7. **Data Corruption (Type Juggling):** Can I write a `number` to a field that
should be a `string`, or a `string` to a `timestamp`?
8. **Validation Bypass (Create vs. Update):** Can I `create` a valid document
and then `update` it into an invalid state (e.g., remove a required field,
write a string that's too long)?
9. **Resource Exhaustion / DoS:** Can I write an enormous string (e.g., 1MB) to
any field that accepts a string or a massive array to a list field? Every
string field (e.g., `bio`, `url`, `name`) MUST have a `.size()` check. If
any are missing, it's a "Resource Exhaustion/DoS" risk.
10. **Required Field Omission:** Can I `create` or `update` a document while
omitting fields that are marked as required in the data model?
11. **Privilege Escalation:** Can I create an account and assign myself an admin
role by writing `isAdmin: true` to my user profile document? (Tests reliance
on document data vs. custom claims).
12. **Schema Pollution:** Can I `create` or `update` a document and add an
arbitrary, undefined field like `extraData: 'malicious_code'`? (Tests for
strict schema enforcement).
13. **Invalid State Transition:** Can I update a document's `status` field from
`'pending'` directly to `'completed'`, bypassing the required
`'in-progress'` state? (Tests business logic enforcement).
14. **Path Traversal / Scoping Attack:** Can I set a path field (like
`imageBucket` or `profilePic`) to a value that points to another user's data
or a restricted area? (Tests for regex path scoping).
15. **Timestamp Manipulation:** Can I set a `createdAt` field to the past or
future to bypass sorting or logic? (Tests for `request.time` validation).
16. **Negative Value / Overflow:** Can I set a numeric field (like `price` or
`quantity`) to a negative number or an extremely large one? (Tests for range
validation).
17. **The "Mixed Content" Leak:** Create a second user. Can User B read User A's
users document? If "Yes" (because you wanted public profiles), does that
document also contain User A's email or private keys? If both are true, the
rules are insecure.
18. **Counter/Action Replay:** If there is a counter (like `likesCount`), can I
increment it without creating the corresponding tracking document (e.g.,
inside `likes/{userId}`)? Can I increment it twice? (Tests for `getAfter()`
consistency checks).
19. **Orphaned Subcollection Access:** Can I read/write to a subcollection
(e.g., `users/123/posts/456`) if the parent document (`users/123`) does not
exist? (Tests for parent existence checks).
20. **Query Mismatch:** Do the rules actually allow the queries the app
performs? (e.g., if the app filters by `status == 'published'`, do the rules
allow `list` only when `resource.data.status == 'published'`?)
21. **Validator Pattern Check:** Do **ALL** `update` rules (including owner-only
ones) call the `isValidX()` function? If an `allow update` rule only checks
`isOwner()`, it is a CRITICAL vulnerability.
Document each attack attempt and whether it succeeded. If ANY attack succeeds:
- Fix the security hole
- Regenerate the rules
- **Repeat Phase-3** until no attacks succeed
#### Phase-4: Syntactic Validation
Once devil's advocate testing passes, repeat until rules pass validation.
**After all phases are complete, create or update the `firestore.rules` file.**
### Critical Constraints
1. **Never skip the devil's advocate phase** - this is your primary security
validation
2. **MUST include helper functions** for common operations ('isAuthenticated',
'isOwner', 'uidUnchanged', 'uidNotModified') AND domain validators
('isValidUser', etc.)
3. **MUST document assumed data models** at the beginning of the rules file
4. **Always validate the rules syntax** using 'firebase deploy --only
firestore:rules --dry-run' or a similar tool before outputting the final
file.
5. **Provide complete, runnable code** - no placeholders or TODOs
6. **Document all assumptions** about data structure or access patterns
7. **Always run the devil's advocate attack** after any modification of the
rules.
8. **Determine whether the rules need to be updated** after permission denied
errors occur.
9. **Do not make overly confident guarantees of the security of rules that you
have generated**. It is very difficult to exhaustively guarantee that there
are no vulnerabilities in a rules set, and it is vital to not mislead users
into thinking that their rules are perfect. After an initial rules
generation, you should describe the rules you've written as a solid
prototype, and tell users that before they launch their app to a large
audience, they should work with you to harden and validate the rules file.
Be clear that users should carefully review rules to ensure security.

View File

@@ -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.
2. **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.
3. **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());
}
});
});
```

View File

@@ -0,0 +1,189 @@
# 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) }
```

View File

@@ -0,0 +1,168 @@
# 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),
),
);
},
);
},
);
```

View File

@@ -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`

View File

@@ -0,0 +1,146 @@
# 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()
}
}
}
}
```

Some files were not shown because too many files have changed in this diff Show More