SKILL DETAIL
extension-to-functions-codebase
firebase/agent-skills/extension-to-functions-codebase
This skill guides the migration of a Firebase Extension into either a local Cloud Functions codebase or a publishable npm package. It leverages native Cloud Functions features such as declarative IAM, parameterized config, and SDK lifecycle hooks, and modernizes 1st Gen triggers to 2nd Gen. Migration workflows include Target A: Local Functions Codebase, outputting to functions/src/ with .env config; and Target B: Publishable npm Package, exporting V2 functions. Core rules include using requiresRole and requiresAPI for declarative IAM, avoiding .value() at module load scope, and preserving V1 concurrency cost by setting cpu: "gcf_gen1". Steps include: inventorying extension resources (params, APIs, roles, lifecycle events, and resources from extension.yaml), configuring package.json (name, engines, peerDependencies, exports map), upgrading triggers (e.g., onDocumentWritten, onTaskDispatched, onRequest), mapping lifecycle events to SDK hooks (afterFirstDeploy, afterRedeploy), and generating a README with installation instructions, re-export snippet, and parameter configuration reference.
Installation
npx skills add https://github.com/firebase/agent-skills --skill extension-to-functions-codebase
技能檔案
SKILL.md
最近同步 · 2026年8月29日
references/configuration-migration.md›
# Migrating Runtime Configurations (runWith)
In Cloud Functions for Firebase V1 (`firebase-functions/v1`), you configured
runtime settings like memory, timeout, and service accounts using `.runWith()`.
In V2 (`firebase-functions/v2`), `.runWith()` is removed and replaced by a more
flexible options system.
You can configure V2 functions in two ways: **Per-Function** (passing an options
object directly to the trigger) or **Globally** (`setGlobalOptions` at the top
of a file).
______________________________________________________________________
## 1. Per-Function Configuration
Pass the configuration options object as the **first argument** to the V2
trigger function. Per-function options always override any global defaults.
### V1 Legacy
```typescript
import * as functions from "firebase-functions";
export const processOrder = functions
.runWith({ memory: "2GB" })
.pubsub.topic("orders")
.onPublish((message, context) => { ... });
```
### V2 Modern Equivalent
```typescript
import { onMessagePublished } from "firebase-functions/v2/pubsub";
export const processOrder = onMessagePublished(
{
topic: "orders",
memory: "2GiB", // Options passed as the first argument!
},
({ message, context }) => { ... } // Destructuring shim pattern
);
```
> [!TIP] **Memory Unit Caveat**: V1 accepted `"1GB"`. V2 types strongly prefer
> IEC units like `"1GiB"`, `"2GiB"`, etc.
______________________________________________________________________
## 2. Global Configuration (`setGlobalOptions`)
Use `setGlobalOptions` at the top of your file when all or most functions in
that file share the exact same runtime requirements (e.g. identical region,
memory allocation, timeout, or service account). Individual functions can still
override specific settings by declaring per-function options.
### V1 Legacy
```typescript
import * as functions from "firebase-functions";
export const myFn = functions
.runWith({
memory: "1GB",
timeoutSeconds: 120,
serviceAccount: "[email protected]",
})
.https.onRequest((req, res) => { ... });
```
### V2 Modern Equivalent
```typescript
import { setGlobalOptions } from "firebase-functions/v2";
import { onRequest } from "firebase-functions/v2/https";
// Set global defaults for all functions defined after this call in this file
setGlobalOptions({
memory: "1GiB", // Note: GiB instead of GB is preferred in V2 types
timeoutSeconds: 120,
serviceAccount: "[email protected]",
});
export const myFn = onRequest((req, res) => { ... });
```
______________________________________________________________________
## Common Property Translations
| V1 Property | V2 Property | Notes |
| :--------------------------- | :--------------------------- | :-------------------------------------------------------------- |
| `memory` | `memory` | Use `"1GiB"` instead of `"1GB"`. |
| `timeoutSeconds` | `timeoutSeconds` | Same. |
| `ingressSettings` | `ingressSettings` | Same. |
| `vpcConnector` | `vpcConnector` | Same. |
| `vpcConnectorEgressSettings` | `vpcConnectorEgressSettings` | Same. |
| `serviceAccount` | `serviceAccount` | Same. |
| `secrets` | `secrets` | Same. |
| `failurePolicy` | `retry` | Renamed to boolean `retry: true/false` in V2 Eventarc triggers. |
______________________________________________________________________
## 3. Migrating Environment Configurations (`functions.config()`)
In V1, you used `functions.config()` to access environment configuration. In V2,
this is replaced by **Parameterized Configuration**.
### Deterministic Rules for Migration
Follow these rules to ensure a deterministic and safe migration:
#### Typing & Exports
- **Numbers**: If the value is used as a number, use `defineInt` or
`defineNumber`.
- **Secrets**: If the key contains "KEY", "SECRET", "TOKEN", or "PASSWORD", use
`defineSecret()` or `defineJsonSecret()`.
- *Note*: Secrets MUST be explicitly bound to the function that uses them in
the options object (e.g., `{ secrets: [myKey, myJsonSecret] }`). Both
`SecretParam` and `JsonSecretParam` are supported in the `secrets` array.
- **Lists**: Use `defineList` for comma-separated lists.
- **JSON**: Use `defineJSON` for JSON strings.
- **Buckets**: If the param is a storage bucket, set `input: { text: {} }` or
bucket selector.
- **Input Validation**: Use `nonEmpty: true` inside `input.text` or
`input.multiSelect` to enforce non-empty parameter input during CLI prompting
(e.g. `defineString("PARAM", { input: { text: { nonEmpty: true } } })`).
- **Type Annotations**: Import parameter types directly from
`firebase-functions/params` (e.g.
`import type { StringParam, SecretParam, JsonSecretParam, IntParam } from "firebase-functions/params"`).
#### Initialization & Scope
- **Global Initialization**: If a variable was initialized globally in V1 (e.g.,
`const client = new Client(functions.config().key)`), you must split it to
have declaration at global scope and initialization inside `onInit`:
```typescript
import { onInit } from "firebase-functions/v2";
const myKey = defineSecret("MY_KEY");
let client: Client;
onInit(() => {
client = new Client(myKey.value());
});
```
#### Advanced Interpolation & Logic
- **String Interpolation**: Use the `expr` tagged template literal from
`firebase-functions/params` (e.g., `` `expr`every ${period} days` ``) instead
of standard template literals when constructing dynamic strings with
parameters. Do NOT call `.value()` inside `expr`.
- **Logic Operators**: Use expressions like
`projectID.equals('prod').thenElse(1, 0)` for logical operations instead of
ternary operators on `.value()`.
#### Built-ins
- Prefer built-in variables like `databaseURL`, `projectID`, `gcloudProject`,
`storageBucket` rather than defining new params for these values.
references/destructuring-shim.md›
# Architectural Deep Dive: Destructuring Compatibility Shim
The Destructuring Compatibility Shim is a **Zero-Touch Logic Migration**
pattern. It allows you to upgrade a function's infrastructure to V2 (and take
advantage of GCF 2nd Gen runtimes) without rewriting any of your internal
business logic.
______________________________________________________________________
## How it Works
When you migrate a V1 function to V2, the signature changes from two parameters
`(data, context)` to a single `CloudEvent` object.
Instead of manually rewriting all usages of `context.params` or `message.json`
inside the function, you use JavaScript's **Object Destructuring** in the
signature.
### Example Transformation
#### Step 1: Legacy V1
```typescript
export const processOrder = functions.pubsub.topic("orders").onPublish((message, context) => {
const orderId = message.json.id;
console.log(`Processing order ${orderId} at ${context.timestamp}`);
});
```
#### Step 2: Modern V2 + Shim
We change the trigger to `onMessagePublished`, and instead of accepting `event`,
we destructure `{ message, context }` directly:
```typescript
export const processOrder = onMessagePublished("orders", ({ message, context }) => {
const orderId = message.json.id; // Legacy logic remains untouched!
console.log(`Processing order ${orderId} at ${context.timestamp}`);
});
```
### Why This Works
The Firebase Functions SDK uses a utility called `addV1Compat` to attach these
properties via **Lazy Getters** on the `CloudEvent` object for standard event
triggers. When you attempt to destructure `{ message, context }` from the event,
the SDK transparently maps the V2 event properties back into V1-compatible
objects on the fly! This feature is available in modern V2 environments
supported by the SDK.
______________________________________________________________________
## Provider Mapping Examples
Here are the exact destructuring patterns for every supported V2 provider:
### 1. Cloud Firestore
- **Created / Deleted** triggers:
```typescript
// V2: onDocumentCreated, onDocumentDeleted
export const processDoc = onDocumentCreated("users/{id}", ({ snapshot, context }) => { ... });
```
- **Updated / Written** triggers:
```typescript
// V2: onDocumentUpdated, onDocumentWritten
export const processDoc = onDocumentUpdated("users/{id}", ({ change, context }) => { ... });
```
### 2. Cloud Storage
- **All** triggers (`onObjectFinalized`, `onObjectDeleted`, `onObjectArchived`,
`onObjectMetadataUpdated`):
```typescript
export const processFile = onObjectFinalized(({ object, context }) => { ... });
```
### 3. Realtime Database
- **Created / Deleted** triggers:
```typescript
export const processData = onValueCreated("/users/{id}", ({ snapshot, context }) => { ... });
```
- **Updated / Written** triggers:
```typescript
export const processData = onValueWritten("/users/{id}", ({ change, context }) => { ... });
```
### 4. Remote Config
- **Updated** triggers:
```typescript
export const processConfig = onConfigUpdated(({ version, context }) => { ... });
```
______________________________________________________________________
## Best Practices for AI Agents
1. **Rely on the shim for complex logic.** When converting functions with
extensive or complex internal business logic, prefer using the destructuring
shim (`{ shimmedKey, context }`) by default to minimize risk and avoid
introducing subtle bugs.
1. **Clean-room rewrites.** If the function body is very simple, or if the user
explicitly asks for a comprehensive code modernization/cleanup, you can ask
the user if they would prefer a full clean-room rewrite to native V2 event
properties.
1. **Always type-check (`tsc`) after the rewrite.** If the types are wrong, the
shim might not be fully supported for that specific provider yet.
1. **HTTPS Callables (Flattened Context)**: Unlike event triggers, Callables do
**not** use `V1Compat` or a `context` object. Instead, all context properties
are flattened onto the request object.
- **V1 Priority**: `(data, context) => { ... }`
- **V2 Equivalent**: `({ data, auth, app }) => { ... }`
______________________________________________________________________
## Related Migrations
For a complete guide on migrating runtime options and `functions.config()` to V2
Parameterized Configuration, refer to
[configuration-migration.md](configuration-migration.md).
references/signature-mapping.md›
# Firebase Functions V1 vs V2 Signature Mapping
This reference maps legacy V1 functions to their modern V2 equivalents. When
using the compatibility shim, you can destructure the V2 event object using the
exact parameter names from the legacy V1 trigger signature (`change`,
`snapshot`, `message`, `object`) alongside `context`.
______________________________________________________________________
## Cloud Firestore
| V1 Trigger | V2 Equivalent | Destructuring Pattern |
| :-------------------------------- | :-------------------- | :------------------------ |
| `firestore.document().onWrite()` | `onDocumentWritten()` | `({ change, context })` |
| `firestore.document().onCreate()` | `onDocumentCreated()` | `({ snapshot, context })` |
| `firestore.document().onUpdate()` | `onDocumentUpdated()` | `({ change, context })` |
| `firestore.document().onDelete()` | `onDocumentDeleted()` | `({ snapshot, context })` |
______________________________________________________________________
## Cloud Pub/Sub
| V1 Trigger | V2 Equivalent | Destructuring Pattern |
| :--------------------------- | :--------------------- | :----------------------- |
| `pubsub.topic().onPublish()` | `onMessagePublished()` | `({ message, context })` |
| `pubsub.schedule().onRun()` | `onSchedule()` | Access `event` directly |
> [!NOTE] Scheduled functions moved from the `pubsub` namespace to the
> `scheduler` namespace in V2.
______________________________________________________________________
## Realtime Database
| V1 Trigger | V2 Equivalent | Destructuring Pattern |
| :-------------------------- | :----------------- | :------------------------ |
| `database.ref().onWrite()` | `onValueWritten()` | `({ change, context })` |
| `database.ref().onCreate()` | `onValueCreated()` | `({ snapshot, context })` |
| `database.ref().onUpdate()` | `onValueUpdated()` | `({ change, context })` |
| `database.ref().onDelete()` | `onValueDeleted()` | `({ snapshot, context })` |
______________________________________________________________________
## Cloud Storage
| V1 Trigger | V2 Equivalent | Destructuring Pattern |
| :------------------------------------ | :-------------------------- | :---------------------- |
| `storage.object().onArchive()` | `onObjectArchived()` | `({ object, context })` |
| `storage.object().onDelete()` | `onObjectDeleted()` | `({ object, context })` |
| `storage.object().onFinalize()` | `onObjectFinalized()` | `({ object, context })` |
| `storage.object().onMetadataUpdate()` | `onObjectMetadataUpdated()` | `({ object, context })` |
______________________________________________________________________
## HTTP / Callables
| V1 Trigger | V2 Equivalent | Destructuring Pattern |
| :------------------ | :------------------ | :----------------------------- |
| `https.onRequest()` | `https.onRequest()` | Standard Express `(req, res)` |
| `https.onCall()` | `https.onCall()` | Destructure `({ data, auth })` |
> [!IMPORTANT] **HTTP Callables do NOT use the Destructuring Shim.** In V2, the
> handler receives a single `CallableRequest` object (not a `CloudEvent`). You
> should destructure properties like `data`, `auth`, and `app` directly from it.
> The traditional `context` object is **unavailable**.
______________________________________________________________________
## Auth (Blocking)
| V1 Trigger | V2 Equivalent | Destructuring Pattern |
| :--------------------------- | :------------------------------ | :---------------------- |
| `auth.user().beforeSignIn()` | `identity.beforeUserSignedIn()` | Access `event` directly |
| `auth.user().beforeCreate()` | `identity.beforeUserCreated()` | Access `event` directly |
> [!NOTE] Auth Blocking triggers moved to the `identity` namespace in V2.
______________________________________________________________________
## Cloud Tasks
| V1 Trigger | V2 Equivalent | Destructuring Pattern |
| :------------------------------- | :------------------- | :---------------------- |
| `tasks.taskQueue().onDispatch()` | `onTaskDispatched()` | Access `event` directly |
SKILL.md›
---
name: extension-to-functions-codebase
description: Skill for converting an installed Firebase Extension (or extension source) into a standalone Cloud Functions for Firebase codebase or publishable npm package, including V1 to V2 trigger upgrades, lifecycle hooks, and declarative security
metadata:
category: Serverless
---
# Extension to Functions Codebase & npm Package Migration
## Overview
Migrates a Firebase Extension into either:
1. **A local Cloud Functions codebase** (`functions/src/` for app integration).
1. **A publishable npm package** (reusable open-source package exporting V2
functions).
Leverages native Cloud Functions features (declarative IAM, Parameterized
Config, SDK Lifecycle Hooks) and modernizes 1st Gen triggers to 2nd Gen using
the Destructuring Compatibility Shim.
______________________________________________________________________
## Target Migration Workflows
- **Target A: Local Functions Codebase** (End-User App Integration)
- Output: Code under `functions/src/`. Config in `.env`.
- Deployment: `firebase deploy --only functions`.
- **Target B: Publishable npm Package / Shareable Package**
- Output: Reusable npm package exporting V2 functions.
- Configuration: `package.json` specifying `exports` map,
`engines: { "node": ">=22" }`, and
`peerDependencies: { "firebase-functions": ">=6.0.0" }`.
- Usage: Consumers install package and re-export functions in `index.ts`
(`export * from "<package-name>"`).
______________________________________________________________________
## Core Rules & Constraints
### 1. Declarative IAM & APIs (Zero-Local-Overhead)
Use native SDK declarations instead of manual `gcloud` scripts or console
instructions:
- Use `requiresRole("roles/...")` for required GCP IAM permissions.
- Use `requiresAPI("service.googleapis.com", "Description")` for Google APIs.
### 2. Global Parameter Access Restriction
- **Never call `.value()` at top-level module load scope.**
- Initialize global SDK instances inside `onInit()` or lazy getters:
```typescript
import { defineString } from "firebase-functions/params";
import { onInit } from "firebase-functions/v2";
const dataset = defineString("DATASET_ID");
let client: BigQuery;
onInit(() => {
client = new BigQuery({ datasetId: dataset.value() });
});
```
### 3. V2 Concurrency & Cost Parity
V2 enables concurrency (up to 80 requests). To preserve V1 single-concurrency
pricing, set `cpu: "gcf_gen1"`.
______________________________________________________________________
## Step-by-Step Migration Execution
### Step 1: Inventory Extension Resources
1. **`extension.yaml`**:
- `params` → `defineString`, `defineInt`, `defineBoolean`, `defineSecret`.
- `apis` → `requiresAPI(...)`.
- `roles` → `requiresRole(...)`.
- `lifecycleEvents` → `afterFirstDeploy` & `afterRedeploy`.
- `resources` → Upgrade 1st Gen triggers to 2nd Gen (`onDocumentWritten`,
`onTaskDispatched`, `onRequest`).
1. **Files & Scripts**: Preserve devDependencies, test framework (`jest`), and
test scripts.
### Step 2: Configure `package.json`
- Set `name: "<package-name>"`, `engines: { "node": ">=22" }`.
- Set `peerDependencies`:
```json
"peerDependencies": {
"firebase-admin": "^11.0.0 || ^12.0.0",
"firebase-functions": ">=6.0.0"
}
```
- Configure `exports` map targeting ESM/CommonJS and TypeScript declarations
(`lib/index.js`, `lib/index.d.ts`).
### Step 3: Upgrade Triggers from V1 to V2
- Firestore: Use `onDocumentWritten` from `firebase-functions/v2/firestore`.
- Tasks: Use `onTaskDispatched` from `firebase-functions/v2/tasks`. Remove
`EXT_INSTANCE_ID` when enqueueing tasks.
- HTTP: Use `onRequest` from `firebase-functions/v2/https`.
- Apply Destructuring Compatibility Shim (`{ change, context }`,
`{ snapshot, context }`) where legacy 1st Gen handlers expect
`(change, context)`.
### Step 4: Convert Lifecycle Events
Map extension lifecycle events to SDK lifecycle hooks in `src/index.ts`:
- `onInstall` → `afterFirstDeploy({ task: { function: "initTask" } })`
- `onUpdate` / `onConfigure` →
`afterRedeploy({ task: { function: "setupTask" } })`
### Step 5: Package README & Export Instructions
Generate `README.md` containing:
1. Installation instructions (`npm install`).
1. Re-export snippet (`export * from "<package-name>"`).
1. Parameterized Configuration `.env` reference table.
1. What Changed (Extension vs Package) comparison table.
_Reminder: NEVER execute `npm publish`._