Migrate Firebase Extensions to a self-created function kit

Select migration path: Migrate to function kits on npm Migrate to self-created function kit

If a publisher hasn't created an official replacement kit distributed on npm, this guide walks you through the steps to fork their extension and set it up as a local function kit.

Check for known migration limitations

Before you start migrating an extension instance, check whether your setup uses any of the following features that require a workaround or aren't yet supported in function kits:

  • Custom Docker repositories and KMS keys require a manual workaround Cloud Functions for Firebase doesn't support replacement system parameters for configuring a custom Docker repository or Customer-Managed Encryption Key (KMS key). If your extension configures either of these parameters, refer to the FAQ workaround.

Before you begin

You need to set up the Firebase CLI and initialize a Firebase project. When using the CLI, make sure you're using firebase-tools version >= 15.32.0, which has the new migration and function kit commands.

Required account permissions and roles

Depending on what needs to be created and configured by the Firebase CLI during migration, the account you're using to authenticate with Firebase and Google Cloud must have the following roles:

  • roles/firebaseextensions.editor
  • roles/cloudbuild.builds.editor
  • roles/artifactregistry.writer
  • roles/run.developer
  • roles/iam.serviceAccountUser
  • roles/iam.serviceAccountCreator
  • roles/cloudfunctions.admin (if you need to do setIamPermissions for public endpoints)
  • roles/secretmanager.admin (if using secrets)
  • roles/serviceusage.serviceUsageAdmin (if you need to enable new APIs)

We recommend using an account that has installed extensions and deployed functions before, since most of these permissions will already have been granted. If your migrating account needs more roles, follow the Google Cloud IAM instructions to add them.

Upgrade your extension instance to the latest version

You must update your extension to the latest version to minimize the difference between your extension instance and its replacement kit. If your extension isn't upgraded, there may be significant, breaking changes between your extension instance and its kit replacement. The exported configuration may not match what the kit expects because of parameter changes across versions.

Use one of the following options to update your extension, depending on where it was installed:

  • From the Firebase console
  • From the Firebase CLI using:
    • firebase ext:update <extension-instance-id> --project <project-id> firebase deploy --only extensions --project <project-id>

If you skip this step, the CLI prompts you to upgrade when exporting configuration if your extension isn't on the latest version.

Fork the extension into a local function kit

Before you start converting an extension to a local function kit, make sure that the extension source code is inside your Firebase project. To do this, clone the extension repository from GitHub, create a directory inside your Firebase project root, and copy the extension's functions/ folder and extension.yaml into it:

mkdir -p path/to/kit
cp -r /path/to/extension-source/functions/* path/to/kit/
cp /path/to/extension-source/extension.yaml path/to/kit/.

Follow Steps 1 through 8 from the publisher migration guide to migrate your extension source code to a 2nd gen function. Then continue with the following steps.

Make your local kit support exported function region and advanced parameters

In a local function kit, the Firebase CLI doesn't generate an index.ts file to set up the package and configure it to use migrated system parameters. To use function region and advanced parameters that were configured for your extension, set up your index.ts file to read the format exported by firebase ext:export --mode functions into an environment variable file.

Specifically, in the top-level index.ts file that exports your functions, define a parameter for FUNCTION_DEFAULT_REGION and call setGlobalOptions with environment variables of the form EXT_MIGRATED_SYSTEM_<GLOBAL_OPTION>, similar to the index-kit-migration.ts template used by the CLI:

import { setGlobalOptions } from "firebase-functions";
import { MemoryOption, VpcEgressSetting, IngressSetting } from "firebase-functions/v2/options";
import { defineString } from "firebase-functions/params";

export const regionParam = defineString("FUNCTION_DEFAULT_REGION", {
  input: { text: { nonEmpty: true } },
  description: "Global default region where functions should be deployed. Can be overridden per-function.",
});

setGlobalOptions({
  region: regionParam,
  memory: (process.env.EXT_MIGRATED_SYSTEM_MEMORY as MemoryOption) ?? undefined,
  timeoutSeconds: process.env.EXT_MIGRATED_SYSTEM_TIMEOUTSECONDS
    ? Number(process.env.EXT_MIGRATED_SYSTEM_TIMEOUTSECONDS)
    : undefined,
  vpcConnectorEgressSettings:
    process.env.EXT_MIGRATED_SYSTEM_VPCCONNECTOREGRESSSETTINGS &&
    process.env.EXT_MIGRATED_SYSTEM_VPCCONNECTOREGRESSSETTINGS !== "VPC_CONNECTOR_EGRESS_SETTINGS_UNSPECIFIED"
      ? (process.env.EXT_MIGRATED_SYSTEM_VPCCONNECTOREGRESSSETTINGS as VpcEgressSetting)
      : undefined,
  vpcConnector: process.env.EXT_MIGRATED_SYSTEM_VPCCONNECTOR ?? undefined,
  maxInstances: process.env.EXT_MIGRATED_SYSTEM_MAXINSTANCES
    ? Number(process.env.EXT_MIGRATED_SYSTEM_MAXINSTANCES)
    : undefined,
  minInstances: process.env.EXT_MIGRATED_SYSTEM_MININSTANCES
    ? Number(process.env.EXT_MIGRATED_SYSTEM_MININSTANCES)
    : undefined,
  ingressSettings: (process.env.EXT_MIGRATED_SYSTEM_INGRESSSETTINGS as IngressSetting) ?? undefined,
  // Parses a comma-separated string of key:value pairs into a key-value object
  // (for example, "key1:value1,key2:value2" -> { key1: "value1", key2: "value2" }).
  labels: process.env.EXT_MIGRATED_SYSTEM_LABELS
    ? process.env.EXT_MIGRATED_SYSTEM_LABELS.split(",").reduce<Record<string, string> | undefined>(
        (acc, curr) => {
          const [key, value] = curr.split(":");
          const trimmedKey = key?.trim();
          const trimmedValue = value?.trim();
          if (!trimmedKey || !trimmedValue) {
            return acc;
          }
          acc = acc ?? {};
          acc[trimmedKey] = trimmedValue;
          return acc;
        },
        undefined,
      )
    : undefined,
});

// Re-export all functions so the Firebase CLI can deploy them
export * from "./your-functions";

Test your kit prior to migration

You now have a local function kit 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 before migrating your production extension instances to it.

First, add your fork as a local kit, configure it, and deploy it to a test project. Local function kits must live inside your Firebase project, so if the cloned extension repository is outside your Firebase project, move it inside the project directory. Then run the following kit installation command to install it as a local kit:

firebase functions:kits:install --directory <path-to-your-fork> --project <test-project-id>

This command guides you through picking a kit ID, an instance ID, and a configuration for your first test instance. It then modifies your firebase.json file to register a local kit pointing to your forked directory, with configurations for each instance stored in a .env file at function-kits/<kit-id>/config-<instance-id>.

Deploy your local kit 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:<kit-instance-id> --project <test-project-id>

Worked example: Stream Cloud Firestore to BigQuery (firestore-bigquery-export)

Verify the Cloud Firestore to BigQuery sync end-to-end:

  1. In the Cloud Firestore page of the Firebase console, create the collection you set as COLLECTION_PATH (users) if it doesn't already exist.
  2. Create a document named bigquery-mirror-test containing any fields with any values.
  3. 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`
    
  4. 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`
    
  5. Delete the bigquery-mirror-test document in Cloud Firestore. It disappears from the latest view, and a DELETE event 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 kit-<kit-instance-id>-fsexportbigquery, not ext-<instanceId>-fsexportbigquery. Look for that name in the Cloud Functions dashboard and logs.
  • Your code runs in the Firebase Local Emulator Suite as standard functions. You can set parameter values to use in the emulator with .env.local. You can also unit test your code using the firebase-functions-test SDK 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 <kit-instance-id>. The task is idempotent, so rerunning it reconciles the dataset, table, and views.
  • Param values come from .env rather than the installation form, so reruns of firebase deploy are non-interactive once .env is complete.

(Optional) Clean up from testing

If you want to remove this test instance after testing, uninstall it:

firebase functions:kits:uninstall --instance <kit-instance-id> --project <test-project-id>

This deletes all cloud resources created by deploying the kit and removes its instance configuration. If you only have one instance of the kit, this also removes the kit entry from firebase.json. It does not delete your local source code directory. When you install the kit for your production migration, you can pick a kit ID again.

Migrate from extensions to your local kit

Now that your local kit is tested, you can migrate your live deployed extension instance.

1. Install the replacement function kit instance

Install your local function kit, passing --no-configure to skip manual configuration so that the next step can export your existing extension configuration directly into this kit instance:

firebase functions:kits:install --no-configure --directory <path-to-your-fork> --project <project-id>

2. Configure the function kit instance identically to the extension

You need to customize this kit instance with a configuration identical to the extension it's replacing. You can export your extension instance configuration into a .env file, which stores parameter, environment variable, and secret reference configuration data for all Cloud Functions, including kits. To export it directly into the configuration file of your kit, run:

firebase ext:export --mode functions --instance <extension-instance-id> --kit-instance <kit-instance-id> --project <project-id>

At the end of this step, the configuration information for this instance is stored in a project-specific .env file in your instance config directory, such as: function-kits/<kit-name>/config-<instance-id>/.env.<project-id>

3. Deploy and verify the kit replacement

Now that the kit is installed and available as a set of functions, you can deploy the kit replacement. Function kits work like standard functions, where each kit instance acts as a separate codebase for organizing your functions. You can choose to deploy all of your functions or just a specific kit instance. While migrating a single extension instance, deploy only that kit instance.

If your kit uses any new parameters that weren't present in the extension instance you migrated from, the Firebase CLI prompts you for them at the beginning of the deployment process. This isn't expected in this worked example from an up-to-date firestore-bigquery-export extension, but many kits prompt for a new parameter for any event trigger source used by the kit. As part of this migration, updated kits use 2nd gen functions where extensions previously used 1st gen functions. In 2nd gen, functions are located near their event sources and added as an additional parameter. In future updates, if new parameters are added, the CLI prompts you on the next deployment.

Worked example:

firebase deploy --only functions:firestore-bigquery-export --project my-project

Output:

=== Deploying to 'my-project'...
i  deploying functions
i  functions: Loaded environment variables from function-kits/firestore-bigquery-export/config-firestore-bigquery-export/.env.my-project
i  functions: ensuring required API bigquery.googleapis.com is enabled...
i  functions: ensuring required API cloudtasks.googleapis.com is enabled...
✔  functions: required APIs are enabled
i  functions: granting declarative IAM roles to managed service account:
   - BigQuery Data Editor
   - BigQuery User
   - Cloud Datastore User
   - Eventarc Event Receiver
   - roles/run.invoker
✔  functions: successfully granted IAM roles
i  functions: creating Node.js 22 (2nd Gen) function kit-firestore-bigquery-export-fsexportbigquery(us-central1)...
i  functions: creating Node.js 22 (2nd Gen) function kit-firestore-bigquery-export-initBigQuerySync(us-central1)...
i  functions: creating Node.js 22 (2nd Gen) function kit-firestore-bigquery-export-setupBigQuerySync(us-central1)...
✔  functions[kit-firestore-bigquery-export-fsexportbigquery(us-central1)] Successful create operation.
✔  functions[kit-firestore-bigquery-export-initBigQuerySync(us-central1)] Successful create operation.
✔  functions[kit-firestore-bigquery-export-setupBigQuerySync(us-central1)] Successful create operation.
i  functions: Executing afterFirstDeploy lifecycle hook targeting: kit-firestore-bigquery-export-initBigQuerySync...
✔  functions: Successfully queued task for lifecycle hook kit-firestore-bigquery-export-initBigQuerySync in queue projects/my-project/locations/us-central1/queues/kit-firestore-bigquery-export-initBigQuerySync.
✔  Deploy complete!

To verify that the firebase deploy of the kit had no errors, check the deployment logs to see if any lifecycle hooks were triggered. Popular extensions, such as Stream Cloud Firestore to BigQuery, use lifecycle hooks. The following is an example of how a lifecycle hook looks when triggered:

i  functions: Executing afterFirstDeploy lifecycle hook targeting: kit-firestore-bigquery-export-initBigQuerySync...
✔  functions: Successfully queued task for lifecycle hook kit-firestore-bigquery-export-initBigQuerySync in queue projects/my-project/locations/europe-west1/queues/kit-firestore-bigquery-export-initBigQuerySync.
i  functions: View logs for afterFirstDeploy at: https://console.cloud.google.com/logs/query;query=resource.type%3D%22cloud_run_revision%22%0Aresource.labels.service_name%3D%22kit-firestore-bigquery-export--initbigquerysync%22%0Aresource.labels.location%3D%22europe-west1%22;project=my-project

These log messages confirm the following:

  • A lifecycle hook was found and executed.
  • A task was queued in the lifecycle hook's associated task queue.
  • A link to Cloud Logging was provided so you can validate that the task completed without errors.

Follow the logs link to the Google Cloud console to validate that there are no errors in the logs and your task queue event was successfully processed. If the lifecycle event didn't execute successfully, you can retrigger it by running:

firebase functions:lifecycle:run <hook-name> <codebase>

If you're deploying a function kit instance for the first time, run:

firebase functions:lifecycle:run afterFirstDeploy <kit-instance-id>

If at any time during validation you decide you want to stop or undo this migration, you can uninstall the kit using the instructions in Uninstall the extension.

4. Uninstall the extension

When you've verified your deployed function kit, you can uninstall your extension so that you're not duplicating its behavior once for the kit and once for the extension. You can uninstall all extensions from the Firebase CLI regardless of how you installed them if you pass the --immediate flag:

firebase ext:uninstall <extension-instance-id> --project <project-id> --immediate

Worked example:

firebase ext:uninstall firestore-bigquery-export --project my-project --immediate

Output:

i  extensions: uninstalling firestore-bigquery-export...
i  extensions: deleting extension instance resources in project my-project...
✔  extensions: successfully uninstalled firestore-bigquery-export

Advanced migrations

You can have extensions in multiple Firebase projects that you want to manage with a single codebase. For example, if you deploy the same infrastructure to a testing environment and a production environment, each of which has a documents Cloud Firestore instance that you export to BigQuery, you might have two instances of the firestore-bigquery-export extension installed:

  • export-documents-testing
  • export-documents-production

If you migrated these two extension instances to two function kit instances in a single codebase when working with the Firebase CLI and deployed using firebase deploy --project testing and firebase deploy --project production, each deploy would create two instances in both the testing and production environments.

Instead, replace the two extension instances with one function kit instance of firestore-bigquery-export deployed to multiple projects, where each project has its own configuration. Your configuration directory for the instance should look like the following:

  • config-export-documents/
    • .env.testing
    • .env.production

Each deployment to testing and production creates one instance of your kit with the corresponding configuration. The existing CLI commands create this setup as long as you pass the --project flag in each invocation of ext:migrate or functions:kits:install.

Worked example:

firebase functions:kits:install --package @firebase-function-kits/firestore-bigquery-export --project testing --no-configure --template migration
✔ What would you like to name this kit? firestore-bigquery-export
✔ What would you like to name this instance? export-documents
✔  Wrote function-kits/firestore-bigquery-export/source/package.json
✔  Wrote function-kits/firestore-bigquery-export/source/tsconfig.json
✔  Wrote function-kits/firestore-bigquery-export/source/.gitignore
✔  Wrote function-kits/firestore-bigquery-export/source/src/index.ts
i  functions: Running npm install
✔  Wrote configuration info to firebase.json
✔  functions: Function kit firestore-bigquery-export successfully installed.
# This creates the export-documents instance with an empty .env.testing file
# for the testing project. Now populate it via export:
firebase ext:export --mode functions --instance export-documents-testing \
  --kit-instance export-documents --project testing

# Repeat the export for production into the same kit instance to create
# .env.production from the export-documents-prod instance:
firebase ext:export --mode functions --instance export-documents-prod \
  --kit-instance export-documents --project production

You now have a single kit instance configured to deploy to your testing and production projects with their respective configurations. If you create an instance in the testing project and run the functions:kits:install command for the same package in the production project, you're prompted with the option to reuse the instance configured for testing or install a second instance.