This guide shows you how to migrate your extensions from the deprecated Firebase Extensions environment to a function that your users install and deploy in their own Cloud Functions for Firebase (2nd gen) codebase.
This is the recommended migration path. Firebase will maintain a list of extensions with official npm equivalents; this guide walks you through creating yours.
Throughout this guide, the Stream Cloud Firestore to
BigQuery
extension (firestore-bigquery-export) is used as a running example. Each
section ends with a Worked example that shows what that extension looked
like before the migration, and what it looks like after, as the
@firebase-function-kits/firestore-bigquery-export
package.
Sign up to get more information and help on migrating extensions
If you have questions on how to migrate from Firebase Extensions, you can reach us at firebase-extensions-migrator-support-external@google.com. We will also email this group as we update the guide with more information on packaging, testing, and distributing your 2nd gen functions.
To join this group, send a message to firebase-extensions-migrator-support-external+subscribe@google.com, which will respond with a membership request email. You must reply to that email, not click the "Join This Group" button.
Before you begin
To complete this migration as written, you'll use the following features of Cloud Functions:
Parameterized Configuration. Each param that you declare in
extension.yamlbecomes a defined parameter in your package code.Declarative IAM roles and required APIs. Each role you declare in
extension.yamlbecomes arequiresRole(...)call, and each API becomes arequiresAPI(...)call in your package code. At deploy time, the Firebase CLI grants the declared roles to a managed runtime service account and enables the declared APIs on your behalf.Lifecycle events for Cloud Functions codebases. Cloud Functions codebases now support lifecycle events analogous to Firebase Extensions. Declare install-time and update-time setup with the lifecycle hooks
afterFirstDeploy(...)andafterRedeploy(...). These replace thelifecycleEventsthat you declare inextension.yaml.
Migrate Firebase Extensions source to a 2nd gen function
(Optional) Automated migration with the Firebase Agent Skill
You can automate Steps 1 through 8 (inventorying resources, trigger
upgrades, param and secret conversions, declarative IAM, lifecycle hooks, and
generating the package README) using the official
extension-to-functions-codebase AI
agent skill.
Install the skill
If you or your AI coding assistant (Gemini in Firebase, Cursor, Claude Code, GitHub Copilot) haven't installed the skill yet, run the following command using the skills CLI:
npx skills add firebase/agent-skills --skill extension-to-functions-codebase
Once the skill is installed in your project, your AI coding assistant automatically follows its migration rules and transformation steps. You can use the following prompt:
"Please migrate this Firebase Extension into a publishable 2nd Gen Function Kit
package following the instructions in the extension-to-functions-codebase
skill."
1. Inventory the extension
Start by taking an inventory of your extension: a complete list of everything the extension declares, ships, and documents, so that every behavior has a defined destination in the 2nd gen function and nothing is lost in the migration.
Review each of the following, and note what you find:
extension.yaml, which declares your params, functions, events, IAM roles, required APIs, secrets, and lifecycle hooks.functions/, which contains your function code, dependencies, build configuration, triggers, and task queue functions.README.md,PREINSTALL.md, andPOSTINSTALL.md, which contain setup steps, warnings, and billing notes.scripts/, which contains any import, backfill, IAM, repair, or migration utilities, and any other tooling that you ship alongside the extension.
Then, for each item in
extension.yaml, decide where it
goes in the npm package:
Convert user configuration into Cloud Functions params (Step 4).
Convert secrets into Cloud Functions secrets (Step 4).
Convert IAM roles into
requiresRole(...)declarations (Step 6).Convert required Google APIs into
requiresAPI(...)declarations where appropriate (Step 6).Convert install and update hooks into
afterFirstDeploy(...)andafterRedeploy(...)declarations (Step 7).Convert instance IDs from
EXT_INSTANCE_IDtoFIREBASE_KIT_INSTANCE_ID(Step 4).
Worked example: Stream Cloud Firestore to BigQuery
Reading firestore-bigquery-export/extension.yaml and functions/ produces
this inventory:
In extension.yaml |
Count / value | Where it goes |
|---|---|---|
params |
25 (COLLECTION_PATH, DATASET_ID, TABLE_ID, DATASET_LOCATION, VIEW_TYPE, …) |
Cloud Functions params (Step 4) |
apis |
bigquery.googleapis.com |
requiresAPI(...) (Step 6) |
roles |
bigquery.dataEditor, datastore.user, bigquery.user |
requiresRole(...) (Step 6) |
resources |
1 event trigger (fsexportbigquery) + task queue functions (initBigQuerySync, setupBigQuerySync) |
Exported package functions (Step 3) |
lifecycleEvents |
onInstall → initBigQuerySync; onUpdate / onConfigure → setupBigQuerySync |
afterFirstDeploy / afterRedeploy (Step 7) |
| Instance ID | Not used (no EXT_INSTANCE_ID reads) |
Nothing to migrate |
scripts/ |
import/ (backfill), gen-schema-view/ |
Kept as scripts (out of scope here) |
Analysis. The extension declares no type: secret params, so there's
nothing to migrate for secrets in Step 4. The
event trigger is already 2nd gen; only the task queue functions are still 1st
gen (relevant in Step 3).
2. Update package.json
Update your extension's package.json file. If you're migrating one extension,
this can be the root package.json. If you're migrating many extensions in one
repository, give each extension its own package.
Minimum SDK versions: Declare
firebase-functions >=
7.4.0 and firebase-admin >=
14.2.0 as dependencies. Declare your version of firebase-functions as a peer
dependency as well, so that your users' Cloud Functions project has the same
version of the SDK that your library was written with.
{
"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"
},
"peerDependencies": {
"firebase-functions": "^7.4.0"
},
"dependencies": {
"firebase-functions": "^7.4.0",
"firebase-admin": "^14.2.0"
}
}
Worked example: Stream Cloud Firestore to BigQuery
Before. The extension's functions/package.json is private, names the
extension ID, and declares firebase-functions as a direct dependency:
{
"name": "firestore-bigquery-export",
"main": "lib/index.js",
"private": true,
"dependencies": {
"@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
"firebase-admin": "^14.2.0",
"firebase-functions": "^6.3.2"
}
}
After. A publishable package: scoped name, an exports map, and
firebase-functions moved to peerDependencies:
{
"name": "@firebase-function-kits/firestore-bigquery-export",
"version": "0.1.0",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
}
},
"engines": {
"node": "22"
},
"peerDependencies": {
"firebase-functions": "^7.4.0"
},
"dependencies": {
"@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
"firebase-admin": "^14.2.0",
"firebase-functions": "^7.4.0"
}
}
3. Upgrade functions from 1st gen to 2nd gen
If your extension still exports 1st gen functions, convert each trigger to its
2nd gen equivalent. Import from the firebase-functions/... modules, and pass
runtime settings in the trigger options.
See the Cloud Functions 2nd gen upgrade guide. Notably, you can minimize rewrite efforts by using 2nd gen patched event destructuring and avoid rewriting your function logic because the 2nd gen SDK exposes the v1 parameters as fields in the event object, allowing you to use destructured/named parameters and keep your business logic unchanged.
Before. 1st gen:
import * as functions from "firebase-functions/v1";
export const sync = functions.firestore
.document("{collectionId}/{documentId}")
.onWrite(async (change, context) => {
await handleWrite(change.before, change.after, context.params);
});
After. 2nd gen:
import { onDocumentWritten } from "firebase-functions/firestore";
export const syncV2 = onDocumentWritten(
{ document: "{collectionId}/{documentId}" },
async ({ change, context }) =>
await handleWrite(change.before, change.after, context.params)
);
See the Cloud Functions version comparison for a comprehensive list of differences between 1st gen and 2nd gen functions.
4. Convert extension params and secrets
Params
Each param that you declare in extension.yaml becomes a Cloud Functions
param.
Convert direct environment reads:
const collectionPath = process.env.COLLECTION_PATH;
into Cloud Functions params:
import { defineString } from "firebase-functions/params";
import { onDocumentWritten } from "firebase-functions/firestore";
const collectionPath = defineString("COLLECTION_PATH");
// Pass the param directly when used as a placeholder (e.g. trigger path)
export const sync = onDocumentWritten(
{ document: collectionPath },
async (event) => {
// Call .value() to read the string inside a handler
const path = collectionPath.value();
await handleWrite(path, event);
}
);
Use collectionPath.value() to read the string inside a handler; use
collectionPath directly where a placeholder is expected, such as a function
trigger path.
The Firebase CLI discovers your params and reads their values from .env,
.env.<projectId>, or prompts your users during deploy. Keep the same param
names, so that values from an existing installation carry over.
It is important that you do not change the param names declared in your code at all. Extension migration preserves existing end-user param values automatically, but only when the names are unchanged.
Worked example: Stream Cloud Firestore to BigQuery
Before. A param declared in extension.yaml, read as a raw environment
variable in config.ts:
# extension.yaml
- param: COLLECTION_PATH
label: Collection path
type: string
required: true
// functions/src/config.ts
collectionPath: process.env.COLLECTION_PATH,
After. One defineString; the CLI discovers it and reads from .env:
// src/config.ts
import { defineString } from "firebase-functions/params";
collectionPath: defineString("COLLECTION_PATH", {
label: "Collection path",
// We now support "nonEmpty: true" to ensure a value other than the empty string
// is entered, analogous to "required: true" in extension.yaml
input: { text: { nonEmpty: true } }
}),
The param name is unchanged, so an existing .env keeps working.
Instance ID
Extensions read their instance ID from EXT_INSTANCE_ID, injected by the
Extensions runtime. Function kits read their instance ID from
FIREBASE_KIT_INSTANCE_ID, which the Firebase CLI sets for each kit
instance to the instance's key in the instances map in firebase.json. The
CLI provides it during deploy-time discovery, in the emulator, and to the
deployed functions.
The instance ID isn't a param, so don't declare it with defineString. In fact,
FIREBASE_... is a reserved prefix in .env files, so users won't be able to
set it or override it there. The values the CLI injects are not visible to the
params system. Read it directly from the environment:
// Before
const instanceId = process.env.EXT_INSTANCE_ID;
// After
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;
The variable will only be set when your package is deployed as a kit. If your code is also deployed as a standalone codebase (see Step 9), either treat it as optional or fail fast with a clear message when it's missing. If your extension exposed the instance ID as a user-facing param, you must remove that param, as the Firebase CLI owns the value now.
Worked example: Delete User Data
(The Stream Cloud Firestore to BigQuery extension doesn't read its instance ID, so there is nothing to migrate there. The Delete User Data extension uses it to name its Pub/Sub topics.)
Before. Read as a raw environment variable in config.ts with the ext-
prefix that Extensions used for its resources:
// functions/src/config.ts
discoveryTopic: `ext-${process.env.EXT_INSTANCE_ID}-discovery`,
deletionTopic: `ext-${process.env.EXT_INSTANCE_ID}-deletion`,
After. A plain process.env read of FIREBASE_KIT_INSTANCE_ID used for
two ordinary params so that users can override the topic names:
// src/config.ts
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;
// Non-empty defaults so Pub/Sub trigger bindings resolve during deploy
// discovery without freezing an empty topic name into the manifest.
discoveryTopicName: defineString("DISCOVERY_TOPIC_NAME", {
default: `kit-${instanceId}-discovery`,
}),
deletionTopicName: defineString("DELETION_TOPIC_NAME", {
default: `kit-${instanceId}-deletion`,
}),
The defaults must be non-empty because trigger bindings get resolved at
discovery time. An empty default would be written into the deploy manifest as
the topic name. The kit is also defensive against running outside of the context
of a kit. If the variable is missing, the module-level default would evaluate to
kit-undefined-discovery, so the config loader fails with an explanatory error
instead:
// ...
const instanceId = process.env.FIREBASE_KIT_INSTANCE_ID;
if (!instanceId) {
throw new Error(
"FIREBASE_KIT_INSTANCE_ID is not set. It is provided automatically to " +
"kit instances by firebase-tools >= 15.32.0; deploy or emulate this " +
"kit with a supported CLI version."
);
}
// ...
This check runs when a handler first resolves its config, so a missing variable
produces a clear runtime error rather than functions silently bound to
kit-undefined-* topics. To reject the deploy itself, perform the check at the
module scope so it runs during discovery. Because the CLI derives the instance
ID from firebase.json, there is no configurable INSTANCE_ID, and nothing to
keep in sync across multiple instances.
Secrets
In extension.yaml, you declare secrets with type: secret. The Extensions
runtime stores and binds them, so your extension code can read
process.env.PARAM_NAME directly. In a typical Cloud Functions codebase,
you declare and bind each secret explicitly:
import { defineSecret } from "firebase-functions/params";
import { onRequest } from "firebase-functions/https";
const apiKey = defineSecret("API_KEY");
export const fn = onRequest({ secrets: [apiKey] }, handler);
Once your extension is migrated to an npm package/kit, secret references are
managed in the end user's .env file. It is important that you do not
change the secret names declared in your code at all. During migration,
end-user secrets are migrated accordingly.
Worked example: Trigger Email From Cloud Firestore
Before. MAIL_COLLECTION and SMTP_PASSWORD read as raw environment
variables in config.ts:
# extension.yaml
- param: MAIL_COLLECTION
label: Email documents collection
type: string
default: mail
required: true
- param: SMTP_PASSWORD
label: SMTP password
type: secret
// functions/src/config.ts
mailCollection: process.env.MAIL_COLLECTION,
smtpPassword: process.env.SMTP_PASSWORD,
After. One defineString and one defineSecret; the CLI discovers both
and reads from .env:
import { defineString, defineSecret } from "firebase-functions/params";
import { onDocumentWritten } from "firebase-functions/firestore";
const mailCollection = defineString("MAIL_COLLECTION", {
label: "Email documents collection",
default: "mail"
});
const smtpPassword = defineSecret("SMTP_PASSWORD", { label: "SMTP password" });
export const processQueue = onDocumentWritten(
{ document: `${mailCollection}/{documentId}`, secrets: [smtpPassword] },
async (event) => {
const collection = mailCollection.value();
const password = smtpPassword.value();
// ...
}
);
5. Migrate internal task-queue calls
Some extensions enqueue work onto their own task queues from inside their
function code, using the Firebase Admin SDK. This is different from receiving a
dispatched task (covered in the Upgrade functions and Convert lifecycle
hooks sections). Here your code is the producer that calls
queue.enqueue(...).
Previous versions of the Admin SDK required extensions to pass their own
extension instance ID as a second parameter to target a Task Queue function in
the same extension. As of firebase-admin 14.2.0, this is neither required nor
recommended. The Task Queue API now targets task queues in the same context (for
example, an extension or kit) by default. It is safe and encouraged to remove
this parameter in your code both as an extension and as standalone functions.
Removing this parameter ensures portability and forwards compatibility.
Everything else about the enqueue call — the
locations/<region>/functions/<name> resource path, the task payload, and your
retry logic — stays the same.
See Enqueue functions with Cloud Tasks for more details on enqueueing functions with Cloud Tasks.
Before. 1st gen extension:
import { getFunctions } from "firebase-admin/functions";
const queue = getFunctions().taskQueue(
`locations/${config.location}/functions/syncBigQuery`,
process.env.EXT_INSTANCE_ID, // extension instance ID, injected by the runtime
);
await queue.enqueue(taskData);
After. 2nd gen extension:
import { getFunctions } from "firebase-admin/functions";
const queue = getFunctions().taskQueue(
`locations/${process.env.FUNCTION_REGION}/functions/syncBigQuery`
);
await queue.enqueue(taskData);
If your enqueue call targets a prefixed codebase, the discovered function name
is prefixed too (for example, orders-syncBigQuery); see
Review and install the replacement function kit instance
and Test as a function kit.
6. Declare required APIs and IAM roles
Move your extension's IAM and API requirements out of extension.yaml and into
code:
import { requiresAPI, requiresRole } from "firebase-functions";
requiresAPI("bigquery.googleapis.com", "Needed to write changelog rows");
requiresRole("roles/bigquery.dataEditor");
requiresRole("roles/bigquery.user");
With declarative security, the Firebase CLI creates or updates a managed runtime service account for the codebase, and grants it the union of all declared roles. Document for your users that all functions in the codebase run with those roles, unless the final API supports a narrower model.
Worked example: Stream Cloud Firestore to BigQuery
Before. Declared in extension.yaml; the Extensions runtime enabled the
API and granted the roles to a managed account:
apis:
- apiName: bigquery.googleapis.com
roles:
- role: bigquery.dataEditor
- role: datastore.user
- role: bigquery.user
After. Declared in code with requiresAPI and requiresRole:
import { requiresAPI, requiresRole } from "firebase-functions";
requiresAPI(
"bigquery.googleapis.com",
"Needed to write changelog rows and views"
);
requiresRole("roles/bigquery.dataEditor");
requiresRole("roles/datastore.user");
requiresRole("roles/bigquery.user");
7. Convert lifecycle hooks
If your extension calls getExtensions().runtime() (for example,
setProcessingState or setFatalError), delete those calls, as they throw an
error if called from a normally deployed 2nd gen function. Lifecycle state is
now driven by afterFirstDeploy and afterRedeploy, where this state tracking
is not used.
Firebase Extensions can run setup when a user installs, updates, or reconfigures an extension. In your npm package, declare equivalent lifecycle actions in code.
For one-time setup:
import { afterFirstDeploy } from "firebase-functions/lifecycle";
import { onTaskDispatched } from "firebase-functions/tasks";
export const runInitialSetup = onTaskDispatched(async (request) => {
await initializeResources(request.data);
});
afterFirstDeploy({
task: {
function: "runInitialSetup",
body: {}
}
});
For config or code updates:
import { afterRedeploy } from "firebase-functions/lifecycle";
afterRedeploy({
task: {
function: "runInitialSetup",
body: { reconcile: true }
}
});
Make your lifecycle actions idempotent. Your users may need to rerun them manually if dispatch or execution fails:
firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME
firebase functions:lifecycle:run afterRedeploy CODEBASE_NAME
Worked example: Stream Cloud Firestore to BigQuery
Before. lifecycleEvents in extension.yaml, driven by the Extensions
runtime:
lifecycleEvents:
onInstall:
function: initBigQuerySync
processingMessage: Configuring BigQuery Sync.
onUpdate:
function: setupBigQuerySync
processingMessage: Configuring BigQuery Sync
onConfigure:
function: setupBigQuerySync
processingMessage: Configuring BigQuery Sync
After. Declared in code; the task provisions BigQuery on first deploy:
import { afterFirstDeploy, afterRedeploy } from "firebase-functions/lifecycle";
afterFirstDeploy({ task: { function: "initBigQuerySync" } });
afterRedeploy({ task: { function: "setupBigQuerySync" } });
Provisioning is idempotent, so a rerun reconciles the dataset, table, and views.
Users can rerun manually with firebase functions:lifecycle:run afterFirstDeploy
CODEBASE_NAME.
8. Document setup for your users
Write a package README that explains, at a minimum:
- The
.envvalues that the package requires. - The secrets that the package requires, and how to migrate existing secret values.
- The IAM roles that the package declares with
requiresRole(...). - The Google APIs that the package enables or requires.
- The lifecycle hooks that the package declares, and how to rerun them manually.
- Billing notes.
- What changed compared to the original extension.
- How the package obtains its instance ID (
FIREBASE_KIT_INSTANCE_IDset by the CLI) and that all the instance's functions are deployed with akit-<instanceId>-prefix.
Worked example: Stream Cloud Firestore to BigQuery
The package README ships a concrete "what changed" table:
| Concern | As the extension | As @firebase-function-kits/firestore-bigquery-export |
|---|---|---|
| Config | Extension params | Cloud Functions params via .env |
| IAM | Granted by Extensions | requiresRole(...), applied at deploy |
| Provisioning | Lifecycle task by Extensions | afterFirstDeploy / afterRedeploy task |
| Function names | ext-<instanceId>-fsexportbigquery |
fsexportbigquery (optionally prefixed) |
| Instance ID | EXT_INSTANCE_ID injected by Extensions |
FIREBASE_KIT_INSTANCE_ID, set by the CLI from firebase.json |
9. Test your 2nd gen function
You should now have a 2nd gen function that, when deployed, behaves identically to a new installation of your extension. The next step is to verify and fix any issues accidentally introduced along the way.
To call setGlobalOptions
to set global options such as a default region or CPU, you must do so only when
deploying your kit as a standalone 2nd gen function. When your kit is installed
as an npm package, your users call setGlobalOptions in their wrapping code to
configure these parameters, and they will get warnings if this happens twice.
You can guard this call by checking for the FIREBASE_KIT_INSTANCE_ID
environment variable:
import { setGlobalOptions } from "firebase-functions";
if (!process.env.FIREBASE_KIT_INSTANCE_ID) {
setGlobalOptions({
region: "us-east1",
maxInstances: 10,
});
}
Make sure you are using
firebase-tools
>= 15.32.0, and deploy your converted 2nd gen function into a test project
with the appropriate resources to test its behavior. If you already have a test
project set up from testing your extension, run the following command:
firebase deploy --only functions
Fill out the resulting wizard prompting you for parameter values the same way you would have filled out the installation form in the Firebase console for the extension.
Worked example: Stream Cloud Firestore to BigQuery
We verify the Cloud Firestore to BigQuery sync end-to-end:
- In the Cloud Firestore page of the Firebase console, create the collection
you set as
COLLECTION_PATH(users) if it doesn't already exist. - Create a document named
bigquery-mirror-testcontaining any fields with any values. In the BigQuery page of the Google Cloud console, query the raw changelog table. It should contain a single row logging the document creation:
SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`Query the latest view, which should return the latest change event for the only document present (
bigquery-mirror-test):SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`Delete the
bigquery-mirror-testdocument in Cloud Firestore. It disappears from the latest view, and aDELETEevent is appended to the raw changelog table.You can inspect the full history of a single document with:
SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog` WHERE document_name = "bigquery-mirror-test" ORDER BY timestamp ASC
Differences from testing the extension:
- The trigger deploys as
fsexportbigquery(no prefix when deploying as a typical 2nd gen function and not a kit), notext-<instanceId>-fsexportbigquery. Look for that name in the Cloud Functions dashboard and logs. - Your code now runs in the Firebase Local Emulator Suite as normal functions.
You can set the value of parameters to use in the emulator with
.env.local. You can also unit test your code using thefirebase-functions-testSDK as described in Unit testing of Cloud Functions. - Provisioning is no longer driven by the Extensions runtime. If the changelog
table is missing after deploy, rerun the setup task manually:
firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME. The task is idempotent, so rerunning it reconciles the dataset, table, and views. - Param values come from
.envrather than the installation form, so reruns offirebase deployare non-interactive once.envis complete.
10. Publish your function kit on npm
Once you've validated your conversion from extension to 2nd gen function, you can publish a release candidate to npm for end-to-end testing using one of the following guides:
When your kit is published to npm, it can be installed with
firebase functions:kits:install and listed as the
official replacement for your extension.
We highly recommend publishing a release candidate first. Kits are installed by
package name and version, so a pre-release lets you test the real installation
flow against the registry without exposing an unfinished package to users who
install with the default latest tag.
Before you publish:
- Choose a package name. Both scoped and unscoped names work (see the guides
above). Note that scoped packages are private by default, so pass
--access public. - Build and check what ships.
mainandtypespoint at your compiled output (lib/in our worked example), so that directory must be included in the published tar file. Use.npmignoreor afilesallow-list, and inspect the result withnpm pack --dry-run. AprepublishOnlyscript that runs your build prevents publishing stale output.
package.json additions that make publishing safe by default:
{
"files": ["lib", "README.md", "CHANGELOG.md"],
"publishConfig": { "access": "public", "tag": "next" },
"scripts": {
"build": "tsc -b",
"prepublishOnly": "npm run build && npm test"
}
}
The field "publishConfig": { "tag": "next" } ensures that a plain npm
publish never overwrites latest.
Cutting a release candidate:
For example, to increment the version locally from 0.0.2-rc.3 to 0.0.2-rc.4
(this commits and tags in Git if package.json is at the repository root):
npm version prerelease --preid rc
To publish the release candidate and register @your-org/your-kit@0.0.2-rc.4 on
npm under the next tag:
npm publish
The npm website can take a few minutes to show the new version; npm view reads
the registry directly:
npm view @your-org/your-kit versions dist-tags
Once Step 11 and Step 12 of this guide are complete, you can promote the package to a stable version:
npm version 0.0.2
npm publish --tag latest
npm dist-tag add @your-org/your-kit@0.0.2 next
Note on npm-shrinkwrap.json: We highly recommend including an
npm-shrinkwrap.json file with your package; the CLI warns users on install if
you don't. It ensures users use the exact dependencies you tested against and
helps protect against supply chain attacks. However, a shrinkwrap is applied
verbatim in your users' projects, including during the Cloud Functions build
(npm ci), where dev-only entries can fail with EBADPLATFORM. You may need to
strip "dev": true entries and devDependencies from the published shrinkwrap
copy.
Worked example: Stream Cloud Firestore to BigQuery
The kit's package.json at the time of its fourth release candidate:
{
"name": "@firebase-function-kits/firestore-bigquery-export",
"version": "0.0.2-rc.4",
"repository": {
"type": "git",
"url": "https://github.com/firebase/extensions.git",
"directory": "kits/firestore-bigquery-export"
},
"main": "lib/index.js",
"types": "lib/index.d.ts",
"engines": { "node": "22" },
"scripts": { "build": "tsc -b" }
}
Note that in this case, the kit lives in a monorepo, so it is important to
include repository.directory for the npm registry link to point at the correct
folder. Its CHANGELOG.md holds notes for the pending release.
11. Test as a function kit
Once you've published your kit, we recommend testing your kit using npm.
Make sure you are using
firebase-tools
version >= 15.32.0 and install the kit:
firebase functions:kits:install --package <your-package-name>@<your-prerelease-version>
This downloads your package from npm, sets it up inside a new source directory for your kit, and walks you through configuring the first instance similar to the extensions installation flow. Once you've installed and set up your package locally, run a deploy to create the resources in your Google Cloud project:
firebase deploy --only functions:<your-kit-instance-id>
After installation, the Firebase CLI prints a similar deploy command with the exact instance ID that you chose during installation.
Validate your kit again using the instructions in
Step 9. Test your 2nd gen function. Now that you're
deploying using kits, your functions are prefixed and named
kit-<instance-id>-<method-name>. This allows kits to have multiple instances,
deploying the same function multiple times in a project, each with a unique
name.
12. Test migration replacement
You can set up a working extension instance and then use the
user migration guide
(using either firebase ext:migrate --package
or the function kits CLI commands)
to finish testing your function kit as a migration replacement.
13. Notify users and Google of your official extension replacement
Once your function kit replacement is ready and available as an npm package that
users should migrate to, inform both your users and Google of this official
replacement. Update the README.md file in the GitHub repository that hosts
your extension with the following information:
<!-- FIREBASE_EXTENSION_REPLACEMENT: extension="<your-extesion-id>" package="<your-npm-package-name>" -->
> [!WARNING]
> **Deprecation Notice:** The Firebase Extension `<your-extension>` is deprecated. Migrate to the [<your-npm-package-name>](<link-to-your-npm-package>) package.
Google scans known extension READMEs for comments like <!--
FIREBASE_EXTENSION_REPLACEMENT: extension="firebase/firestore-bigquery-export"
package="@firebase-function-kits/firestore-bigquery-export" --> and uses this
to populate our official registry of replacements stored in the firebase-tools
repository as
replacements.json.
You can also check replacements.json to see which README.md will be scanned
for your extension. The official list of replacements is updated weekly.
Worked example: Stream Cloud Firestore to BigQuery
The firestore-bigquery-export extension
README.md
contains:
<!-- FIREBASE_EXTENSION_REPLACEMENT: extension="firebase/firestore-bigquery-export" package="@firebase-function-kits/firestore-bigquery-export" -->
> [!WARNING]
> **Deprecation Notice:** The Firebase Extension `firebase/firestore-bigquery-export` is deprecated. Please migrate to the [`@firebase-function-kits/firestore-bigquery-export`](https://www.npmjs.com/package/@firebase-function-kits/firestore-bigquery-export) package.