Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions skills/firebase-v1-v2-migration/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: firebase-v1-v2-migration
description: Use this skill when a user wants to upgrade their legacy Firebase Functions from V1 (GCF 1st Gen) to V2 (GCF 2nd Gen) safely without rewriting their internal business logic. This skill relies on the Destructuring Compatibility Shim.
---
# 🚀 Supported Migration Strategies

This skill currently supports the **In-Place Migration** strategy.

### 🚜 In-Place Migration (Standard)
The agent modifies the existing V1 code file directly to use V2 syntax and overwrites the deployment slot.
* **Pros**: Fast, simple, clean repository history.
* **Cons**: No safety net during deployment. If the V2 deployment fails, you must rollback using Git.

*Note: For complex or zero-downtime migrations (e.g. Side-by-Side deployment), refer to external orchestration skills.*

# Prerequisites
Please ensure the workspace is ready for V2 before attempting a code migration:
1. **Configuration Check**: Ensure the workspace has transitioned away from functions.config() to Parameterized Configuration or standard environment variables.
2. **Dependencies**: The project must be using firebase-functions version that supports V2 (>= 4.0.0).

# 🔍 Pre-Migration Checklist
Before modifying any code, the agent should run a quick scan:
1. **Scan for legacy configs**: Run a `grep` or text search for usages of `functions.config()`.
- **Action**: If found, **stop and warn the user** that these configs will evaluate to `undefined` in V2 unless they migrate to Parameterized Configuration or standard `.env` variables first.

# Principles of Safe Migration
Always follow these principles to ensure zero-touch logic migration:
1. **Use Context-Aware Editing over Global Regex**: Never use naive find-and-replace. Rely on syntax-aware editing (such as an AI agent reading the file context and making precise edits, or tools like ts-morph/ast parsers) to ensure context isolation.
2. **Signature Modernization with Destructuring**: Do NOT rewrite the internal variable usages of context or params inside the function body. Instead, use JavaScript's native object destructuring in the new V2 signature parameters. (Note: For `https.onCall`, the context shim is not available; you should destructure `auth` and `data` directly from the request object instead of expecting a `.context` property).

### 🛡️ Example Transformation

#### Before (V1 Legacy)
```typescript
import * as functions from "firebase-functions";
export const processOrder = functions.pubsub.topic("orders").onPublish((message, context) => {
const orderId = message.json.id;
console.log(`Processing order ${orderId} at ${context.timestamp}`);
});
```

#### After (V2 Target - Safe Migration)
```typescript
import { onMessagePublished } from "firebase-functions/v2/pubsub";
// Using direct object destructuring in the signature!
export const processOrder = onMessagePublished("orders", ({ message, context }) => {
const orderId = message.json.id; // Legacy logic remains untouched!
console.log(`Processing order ${orderId} at ${context.timestamp}`);
});
```

# Verification
After making any migration edits, immediately run the following verification steps:
1. Run `npm run build` to ensure the TypeScript compiler is happy with the types and parameters.
2. Run `npm test` to verify no regressions occurred in existing unit tests.

> [!WARNING]
> **Test Signature Mismatch**: The destructuring shim changes the function signature from two arguments `(data, context)` to a single destructured object `({ message, context })`.
>
> Existing V1 unit tests that invoke the function with two parameters separately (e.g., `myFn(mockData, mockContext)`) **will fail** because the function treats `mockData` as the entire event object. You will need to update test calls to pass a single object.
>
> **Crucial**: The key name in the test mock must match the specific **Shimmed Key** for that trigger (e.g., `change` for `onDocumentWritten`, `snapshot` for `onDocumentCreated`, `message` for PubSub, or `object` for Storage).
>
> Example: `myFn({ change: mockChange, context: mockContext })` or `myFn({ message: mockMessage, context: mockContext })`. See [signature-mapping.md](references/signature-mapping.md) for the exact keys.

# 💸 Performance & Cost Considerations

In Firebase Functions V2, you can handle multiple requests concurrently per instance (up to 1,000 requests, default 80 if CPU >= 1). However, enabling concurrency requires assigning at least 1 full CPU.

* **V1 Cost Parity**: If you want to keep V1 fractional CPU pricing (and disable concurrency), you must explicitly set `cpu: "gcf_gen1"`.
* **Modernization**: If you want to take advantage of Concurrency, you must assign at least 1 CPU.

See [configuration-migration.md](references/configuration-migration.md) for how to set these options.

# References
- **Deep Dive into Shims**: See the architectural choices for the shim in [destructuring-shim.md](references/destructuring-shim.md).
- **Function Name Mapping**: See the V1 vs V2 function signature mapping table in [signature-mapping.md](references/signature-mapping.md).
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Migrating Runtime Configurations (runWith)

In Firebase Functions V1, you configured runtime settings like memory, timeout, and service accounts using `.runWith()`. In V2, `.runWith()` is removed and replaced by a more flexible options system.

You can configure V2 functions in two ways: **Globally** (for all functions in a file) or **Per-Function**.

---

## 🌍 1. Global Configuration (`setGlobalOptions`)

Use `setGlobalOptions` at the top of your file to set defaults for all functions defined after it.

### V1 Legacy

```typescript
import * as functions from "firebase-functions";

export const myFn = functions
.runWith({
memory: "1GB",
timeoutSeconds: 120,
serviceAccount: "custom-sa@my-project.iam.gserviceaccount.com",
})
.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 this file
setGlobalOptions({
memory: "1GiB", // Note: GiB instead of GB is preferred in V2 types
timeoutSeconds: 120,
serviceAccount: "custom-sa@my-project.iam.gserviceaccount.com",
});

export const myFn = onRequest((req, res) => { ... });
```

---

## 🎯 2. Per-Function Configuration

Pass the configuration object as the **first argument** to the V2 trigger function.

### V1 Legacy

```typescript
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.

---

## ⚠️ 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. |
92 changes: 92 additions & 0 deletions skills/firebase-v1-v2-migration/references/destructuring-shim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# 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 has been updated to provide **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!

---

## 📖 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. **Do NOT attempt to rewrite variable access inside the body.** It is error-prone and time-consuming.
2. **Rely on the shim by default.** Only attempt a pure V2 rewrite if the user explicitly asks for "clean-room" modernization.
3. **Always type-check (`tsc`) after the rewrite.** If the types are wrong, the shim might not be fully supported for that specific provider yet!
4. **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 }) => { ... }`
81 changes: 81 additions & 0 deletions skills/firebase-v1-v2-migration/references/signature-mapping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Firebase Functions V1 vs V2 Signature Mapping

This reference maps legacy V1 functions to their modern V2 equivalents. It includes the **Shimmed Parameter Key** you should use when destructuring the V2 event object to preserve V1 business logic.

---

## 🔥 Cloud Firestore

| V1 Trigger | V2 Equivalent | Shimmed Key | Destructuring Pattern |
| :--- | :--- | :--- | :--- |
| `firestore.document().onWrite()` | `onDocumentWritten()` | `change` | `({ change, context })` |
| `firestore.document().onCreate()` | `onDocumentCreated()` | `snapshot` | `({ snapshot, context })` |
| `firestore.document().onUpdate()` | `onDocumentUpdated()` | `change` | `({ change, context })` |
| `firestore.document().onDelete()` | `onDocumentDeleted()` | `snapshot` | `({ snapshot, context })` |

---

## 📨 Cloud Pub/Sub

| V1 Trigger | V2 Equivalent | Shimmed Key | Destructuring Pattern |
| :--- | :--- | :--- | :--- |
| `pubsub.topic().onPublish()` | `onMessagePublished()` | `message` | `({ message, context })` |
| `pubsub.schedule().onRun()` | `scheduler.onSchedule()` | **N/A** | Access `event` directly |

> [!NOTE]
> Scheduled functions moved from the `pubsub` namespace to the `scheduler` namespace in V2.

---

## 💾 Realtime Database

| V1 Trigger | V2 Equivalent | Shimmed Key | Destructuring Pattern |
| :--- | :--- | :--- | :--- |
| `database.ref().onWrite()` | `onValueWritten()` | `change` | `({ change, context })` |
| `database.ref().onCreate()` | `onValueCreated()` | `snapshot` | `({ snapshot, context })` |
| `database.ref().onUpdate()` | `onValueUpdated()` | `change` | `({ change, context })` |
| `database.ref().onDelete()` | `onValueDeleted()` | `snapshot` | `({ snapshot, context })` |

---

## 🗄️ Cloud Storage

| V1 Trigger | V2 Equivalent | Shimmed Key | Destructuring Pattern |
| :--- | :--- | :--- | :--- |
| `storage.object().onArchive()` | `onObjectArchived()` | `object` | `({ object, context })` |
| `storage.object().onDelete()` | `onObjectDeleted()` | `object` | `({ object, context })` |
| `storage.object().onFinalize()` | `onObjectFinalized()` | `object` | `({ object, context })` |
| `storage.object().onMetadataUpdate()` | `onObjectMetadataUpdated()` | `object` | `({ object, context })` |

---

## 🌐 HTTP / Callables

| V1 Trigger | V2 Equivalent | Shimmed Key | Destructuring Pattern |
| :--- | :--- | :--- | :--- |
| `https.onRequest()` | `https.onRequest()` | **N/A** | Standard Express `(req, res)` |
| `https.onCall()` | `https.onCall()` | **N/A** | 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 | Shimmed Key | Destructuring Pattern |
| :--- | :--- | :--- | :--- |
| `auth.user().beforeSignIn()` | `identity.beforeUserSignedIn()` | **N/A** | Access `event` directly |
| `auth.user().beforeCreate()` | `identity.beforeUserCreated()` | **N/A** | Access `event` directly |

> [!NOTE]
> Auth Blocking triggers moved to the `identity` namespace in V2.

---

## ⏰ Cloud Tasks

| V1 Trigger | V2 Equivalent | Shimmed Key | Destructuring Pattern |
| :--- | :--- | :--- | :--- |
| `tasks.taskQueue().onDispatch()` | `tasks.onTaskDispatched()` | **N/A** | Access `event` directly |
Loading