firebase/extension-to-functions-codebase
Skill for converting an installed Firebase Extension (or extension source) to a standalone Cloud Functions for Firebase codebase or publishable npm package, including upgrading triggers from V1 to V2 and configuring lifecycle hooks and declarative security
npx skills add https://github.com/firebase/agent-skills --skill extension-to-functions-codebase
This skill guides the agent in migrating a Firebase Extension repository or
instance into either:
firebase-functions,for end-user app integration), or
publishers distributing reusable V2 functions).
It leverages native Cloud Functions for Firebase GA capabilities to handle
permissions, dependencies, and lifecycle hooks natively in code, and provides
instructions for modernizing legacy V1 triggers to V2 using the Destructuring
Compatibility Shim.
______________________________________________________________________
Activate this skill when a user asks to:
codebase.
source package).
______________________________________________________________________
Before starting, determine the target destination with the developer:
functions/src/ folder.defineString, defineSecret, etc.in .env.
firebase deploy --only functions.package.json with exports map and firebase-functionsdeclared in dependencies (or peerDependencies).
npm i <package-name>) and re-exportfunctions in their index.ts.
______________________________________________________________________
repository:
git cp (or copy files and commit) to copy the extension's sourcedirectory to the target directory.
"Copying [extension-name] extension to [directory] in preparation for rewrite"
______________________________________________________________________
Assume Cloud Functions for Firebase Workload Identities, Declarative Security,
and SDK Lifecycle Hooks are fully GA.
gcloudIAM commands or create service accounts.
APIs in the cloud console.
requiresAPI and requiresRole imports from theSDK.
.value() on any parameter at global scope.declare the variable globally and initialize it inside the onInit()
callback:
import { defineString } from "firebase-functions/params";
import { onInit } from "firebase-functions/v2";
const bqDataset = defineString("DATASET_ID");
let bqClient: BigQuery;
onInit(() => {
bqClient = new BigQuery({ datasetId: bqDataset.value() });
});
When upgrading triggers to V2:
set cpu: "gcf_gen1" in the function's options object.
______________________________________________________________________
Conduct a complete inventory of everything the extension declares, ships, and
documents so that nothing is lost during migration:
extension.yaml:params: Convert to Functions params (defineString, defineSecret,etc.).
apis: Convert to requiresAPI(...) declarations.roles: Convert to requiresRole(...) declarations.lifecycleEvents (onInstall, onUpdate, onConfigure): Convert toafterFirstDeploy and afterRedeploy hooks.
resources: Note all function triggers to convert from 1st gen(firebase-functions/v1) to 2nd gen (firebase-functions/v2), including
standard event triggers, HTTP handlers, and task queues
(onTaskDispatched).
functions/: Source code, triggers, helpers, and task queue handlers.README.md, PREINSTALL.md, and POSTINSTALL.md.scripts/: Note any backfill, import, or helper scripts shipped with theextension.
package.jsonCreate an npm package for the migrated extension code (either at project root or
in a dedicated workspace directory):
name: "<package-name>").devDependencies,test runners (jest, ts-jest, @types/jest, mocha, @types/mocha), and
test scripts ("test": "...") from the legacy extension
(functions/package.json or root package.json). Do not drop test frameworks
or type definitions.
firebase-functions (e.g. ^7.0.0) in dependencies (orpeerDependencies if creating a lightweight middleware package where the root
consumer manages the runtime version):
{
"name": "<package-name>",
"version": "1.0.0",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
}
},
"engines": {
"node": ">=22"
},
"dependencies": {
"firebase-admin": "^12.0.0",
"firebase-functions": "^7.0.0"
}
}
Move the extension's function source into the package's src/ folder:
functions that end users can re-export from their entrypoint (index.ts).
export * from "<package-name>";
// Or named exports:
// export { syncV2, initBigQuerySync } from "<package-name>";
*Note*: A bare import (import "<package-name>") is not enough. The Firebase
CLI only deploys functions exported from the user's root entry file.
Convert each exported 1st gen function trigger (onWrite, onRequest,
tasks.taskQueue().onDispatch) to its 2nd gen equivalent (onDocumentWritten,
onRequest, onTaskDispatched from firebase-functions/v2/...):
({ shimmedKey, context }) to preserve V1 business logic without rewriting
function bodies. See signature-mapping.md
and destructuring-shim.md.
Authentication Triggers (auth.user().onCreate(), auth.user().onDelete()),
instruct the agent to check live whether a 2nd gen alternative exists in the
installed firebase-functions package or live documentation. If 2nd gen Auth
triggers are not yet supported for those events, warn the user clearly and
halt or refuse the migration for those specific triggers until a V2
alternative becomes available.
Each parameter in extension.yaml becomes a Parameterized Configuration call:
string ->defineString("PARAM_NAME", { label: "...", description: "...", default: "..." })
secret -> defineSecret("PARAM_NAME")int -> defineInt("PARAM_NAME", { label: "...", default: 123 })boolean -> defineBoolean("PARAM_NAME", { default: true })select / multiSelect -> map options into input:defineString("PARAM_NAME", { input: { select: { options: [{ value: "val", label: "Val" }] } } })
validationRegex -> map into text input options:defineString("PARAM_NAME", { input: { text: { validationRegex: "^[a-z]+$" } } })
required: true / Non-empty validation -> map nonEmpty: true into inputoptions:
defineString("PARAM_NAME", { input: { text: { nonEmpty: true } } })
paramName.value().(COLLECTION_PATH, DATASET_ID, etc.) so that existing values carry over
seamlessly in .env.
queue.enqueue(...))If your extension code enqueues tasks onto its own queue using the Admin SDK
(getFunctions().taskQueue(...)):
plus process.env.EXT_INSTANCE_ID.
(EXT_INSTANCE_ID) entirely. The Admin SDK automatically targets the current
codebase:
// Before (extension runtime):
// const queue = getFunctions().taskQueue(`locations/${region}/functions/syncBigQuery`, process.env.EXT_INSTANCE_ID);
// After (npm package):
const queue = getFunctions().taskQueue(`locations/${region}/functions/syncBigQuery`);
await queue.enqueue(taskData);
For parameters declared with type: secret:
import { defineSecret } from "firebase-functions/params";
const apiKey = defineSecret("API_KEY");
export const fn = onRequest({ secrets: [apiKey] }, handler);
Replace apis and roles from extension.yaml with declarative code in your
entry file:
import { requiresAPI, requiresRole } from "firebase-functions";
requiresAPI("bigquery.googleapis.com", "Needed to write changelog rows");
requiresRole("roles/bigquery.dataEditor");
requiresRole("roles/bigquery.user");
At deploy time, the Firebase CLI automatically grants these roles to the managed
runtime service account and enables the required APIs.
afterFirstDeploy & afterRedeploy)Replace lifecycleEvents (onInstall, onUpdate, onConfigure) with SDK
lifecycle hooks:
initBigQuerySync, setupBigQuerySync) to V2onTaskDispatched from firebase-functions/v2/tasks (removing legacy
getExtensions().runtime().setProcessingState(...) calls).
import { afterFirstDeploy, afterRedeploy } from "firebase-functions/v2";
// Replaces onInstall:
afterFirstDeploy({
task: {
function: "runInitialSetup",
body: {}
}
});
// Replaces onUpdate & onConfigure:
afterRedeploy({
task: {
function: "runInitialSetup",
body: { reconcile: true }
}
});
firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME
firebase functions:lifecycle:run afterRedeploy CODEBASE_NAME
README.md)Preserve whatever documentation, secrets, and setup instructions the original
extension already documented in README.md (and PREINSTALL.md /
POSTINSTALL.md), updating them as needed:
extension.yaml installation prompts or ext-*.env files with standard
.env Parameterized Configuration setup matching the defineString
parameters.
shown (export * from "<package-name>";) so users know how to expose the
functions in their root index.ts.
tables or generic installation steps if the setup is self-evident in the code
or already covered by existing README sections.
npm run build (tsc) to ensure noTypeScript compilation errors in src/.
__tests__/, test/) are present in theextension:
@types/jest (or the original test framework types) are presentin devDependencies so test files type-check cleanly.
destructured event object
(({ change: mockChange, context: mockContext }) instead of positional
arguments (mockChange, mockContext)).
npm test or type-check test files (npx tsc --noEmit) to verifyzero regressions.
______________________________________________________________________
Prepare Firebase Extensions for migration to Cloud Functions
[email protected]firebase-extensions-migrator-support-external+subscribe@google.com
destructuring-shim.md for details on event
property translation.
signature-mapping.md for V1 vs V2 trigger
definitions and shim keys.
configuration-migration.md for
runWith options, params, and secret bindings.
Take firebase/extension-to-functions-codebase from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npm, npx.
Without those the skill loads but fails at the first command.