Cloud Functions Secrets, Rotation, and Leak Response for Solo Flutter + Firebase Founders in 2026

By gspteck Editorial · Published 2026-10-07 · Last verified 2026-10-07

Most solo Flutter + Firebase apps end up with a handful of real secrets on the backend: a payment provider key, a webhook signing key, an AI or email API token, maybe an OAuth client secret. They usually get added in a hurry, live in whatever place worked first, and never get rotated. This guide covers where each kind of configuration belongs in Cloud Functions for Firebase, how to bind secrets with least privilege, a rotation routine that doesn’t take your webhooks down, and what to do in the first hour after a leak. It pairs with our Firebase Auth and API key abuse hardening guide, which explains why the Firebase web API key in your app is not a secret and these values are.

Know what is actually a secret

Start by sorting configuration into three buckets, because each one has a different home:

The Firebase guide Configure your environment is explicit that values in .env files should not be considered a secure way to store sensitive information, especially if those files are checked into source control. It also warns that the legacy functions.config() API is deprecated and that new deployments using it will fail after March 2027, so if an older project still reads keys from runtime config, migrating now saves you a forced fix later.

One rule for the Flutter side: no backend secret ever goes into the app bundle, --dart-define values, or Remote Config. Anything shipped to devices can be extracted. If the app needs a privileged action, call a function that holds the secret.

Three columns showing where configuration belongs in a Firebase backend: parameters or .env files for non-sensitive settings, Secret Manager for API keys and webhook signing keys bound per function, and never in the Flutter app bundle, Git or CI logs, or functions.config()
Sort configuration first: non-sensitive settings in params or .env, real secrets in Secret Manager.

Bind secrets per function, not per project

Cloud Functions for Firebase integrates Secret Manager through defineSecret() (or the secrets option). Set a value with the CLI, then list the secret on each function that needs it:

// functions/src/index.ts
import { onRequest } from "firebase-functions/v2/https";
import { defineSecret } from "firebase-functions/params";

const webhookKey = defineSecret("PAYMENTS_WEBHOOK_KEY");

export const paymentsWebhook = onRequest(
  { secrets: [webhookKey] },
  (req, res) => {
    const key = webhookKey.value();
    // verify signature with key ...
    res.sendStatus(200);
  }
);

The docs note that only functions that include a secret in their secrets option can read it; any other function gets an undefined value. That is useful, so lean into it:

Two practical traps we hit in our own pipelines. First, secrets are read when an instance cold-starts, so read them inside the handler or an onInit() callback, never at module top level during deploy. Second, the stored value is the exact bytes you provided. If you set a secret with --data-file from a file that ends in a newline, the newline is part of the key. Every system that signs or verifies with that key must use the identical bytes, or HMAC checks fail with no obvious cause.

Three cards on least privilege for secrets: bind secrets only to functions that need them, run sensitive functions under their own service account with accessor rights on the specific secret, and keep separate staging and production values with audit logs
Least privilege at three levels: function bindings, service account identity, and project separation.

A rotation routine that doesn’t break webhooks

Changing a secret is easy. Changing it while two systems depend on it is where outages come from. The Firebase docs list the rule that drives everything: whenever you set a new value, you must redeploy every function that references that secret for it to pick up the latest version. For a shared key such as a webhook HMAC secret, use an overlap window:

  1. Accept both values. Deploy verification code that checks the current key and, if that fails, a second ..._PREVIOUS secret. Keep this logic small and well tested.
  2. Add the new version. Run firebase functions:secrets:set NAME (or --data-file), and copy the old value into the previous slot.
  3. Redeploy every function that binds the secret. Check logs for verification failures.
  4. Switch the signer (the payment provider, your publisher script, the partner) to the new value.
  5. Retire the old value once traffic signed with it stops: clear the previous slot and redeploy.

Two things to know about versions. The Firebase CLI tries to clean up unused secret versions after a deploy, so don’t rely on an old version staying available; keep the overlap in your own code. And the CLI gives you functions:secrets:get to list versions and functions:secrets:prune to remove secrets no function references.

How often? For a solo team, rotate on a calendar (every 90 days is a reasonable default for webhook and API keys), whenever someone with access leaves, and immediately after any suspected exposure. Secret Manager’s rotation schedules don’t change values for you. They publish a SECRET_ROTATE message to Pub/Sub on schedule, and you decide what happens next. Even if you rotate by hand, a scheduled notification is a dependable reminder.

Four-step zero-downtime rotation: accept the current or previous key, set a new secret version, redeploy every function that binds it, then switch signers and retire the old value
Rotate shared keys with an overlap window so webhooks keep verifying during the switch.

Leak response: the first hour

Leaks for small teams usually come from a committed .env file, a pasted log, a screenshot, or a CI job that echoes variables. Turn on GitHub secret scanning and push protection for your repositories so many of these are caught before or soon after a push. When something does leak:

  1. Revoke at the provider first. Rotating in Secret Manager does nothing if the old key still works at Stripe, your email service, or the AI API.
  2. Create the new version and redeploy the bound functions, using the overlap routine if a partner signs with the key.
  3. Disable the old Secret Manager version, then destroy it once you are sure nothing needs it. Google documents both steps in disable a secret version and destroy a secret version.
  4. Clean the exposure. Remove the value from the repo history, CI logs, and any tickets or chats. Treat it as burned regardless.
  5. Check the damage. Review provider usage and billing, and Secret Manager’s audit logging for unexpected access. Our cost alerts and budgets guide helps you notice abuse quickly.
Grid of six leak response steps: revoke at the provider, set a new version, redeploy bound functions, disable then destroy the old version, clean repo, CI, and log history, and review billing and audit logs
First-hour leak response: revoke upstream first, then rotate, disable, clean up, and review usage.

Cost and housekeeping

Secret Manager is a paid service with a free tier. According to the Firebase docs, it currently allows six active secret versions and 10,000 access operations per month at no cost, and function instances read their bound secrets on every cold start. If you have many related values for one provider, defineJsonSecret() stores them as a single JSON secret, which keeps the count down. Check current numbers on Secret Manager pricing.

Quarterly secrets review (20 minutes)

Put the review on the same calendar as your Firestore rules review so both happen together.

Key takeaways

Sources

More from gspteck: Remote Config kill switches and staged rollouts.