nextjs-adapter-firebase-functions
v0.1.0
Published
Firebase Cloud Functions adapter for Next.js 16.2 adapter API
Maintainers
Readme
nextjs-adapter-firebase-functions
Firebase Cloud Functions adapter for Next.js 16.2 custom adapter API.
Deploys your Next.js app as a single Firebase Cloud Function (v2) using onRequest. All routes — App Router, Pages Router, API routes, middleware, image optimization — are handled by one function via next().getRequestHandler().
Built with Claude Code
Requirements
- Next.js
>=16.2.0 - Node.js
22 - Firebase CLI (
npm install -g firebase-tools) - A Firebase project with Cloud Functions enabled
Installation
npm install nextjs-adapter-firebase-functionsSetup
1. Zero config
Point adapterPath at the adapter. The zero-config default covers most apps:
// next.config.ts
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
export default {
adapterPath: require.resolve('nextjs-adapter-firebase-functions'),
}2. With options
To configure secrets, function settings, or env vars, use a dedicated adapter config file (adapter.config.ts, .mjs, or .js — any extension works):
// adapter.config.ts
import { createFirebaseAdapter } from 'nextjs-adapter-firebase-functions'
export default createFirebaseAdapter({
functionName: 'nextjs',
secrets: ['DATABASE_URL', 'STRIPE_SECRET_KEY'],
functionConfig: {
region: 'europe-west1',
memory: '2GiB',
timeoutSeconds: 120,
minInstances: 1,
},
env: {
NEXT_PUBLIC_APP_URL: 'https://myapp.com',
},
})// next.config.ts
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
export default {
adapterPath: require.resolve('./adapter.config'),
}With Payload CMS (or other next.config.ts wrappers)
adapterPath works even when your config is wrapped by withPayload() or similar:
// next.config.ts
import { withPayload } from '@payloadcms/next/withPayload'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const dirname = path.dirname(fileURLToPath(import.meta.url))
export default withPayload({
adapterPath: path.resolve(dirname, './adapter.config.mjs'),
})If you cannot modify next.config.ts (e.g. in CI), set NEXT_ADAPTER_PATH as an environment variable instead:
NEXT_ADAPTER_PATH="$(node -e "console.log(require.resolve('nextjs-adapter-firebase-functions'))")" \
next buildBuild
Turbopack: Next.js 16+ uses Turbopack by default. Turbopack creates
.next/node_modules/with machine-specific symlinks that break on Cloud Run. Always build with--webpackfor Firebase deploys.
Monorepo: Only the project's own
package.jsondependenciesare included in the generatedfunctions/package.json. Workspace root deps are intentionally excluded to avoid pulling in unrelated sibling packages. The workspace rootnode_modulesis still scanned for native binary sub-packages (e.g.sharp), so hoisted native modules are handled correctly.
next build --webpackThe adapter runs after the Next.js build and writes its output to firebase-dist/ (configurable via outDir):
firebase-dist/
functions/
.next/ # copied from .next/ (cache/, dev/, trace excluded)
public/ # copied from public/ (if present)
index.js # Firebase Function entry (generated, CJS)
firebase-params.js # Secret/param definitions (generated, CJS)
runtime-next-config.json # Runtime Next.js config
deployment-manifest.json # Adapter metadata
package.json # Functions package (generated)
.gitignore
firebase.json # Written to project root if absentDeployment
firebase deploy --only functions --project my-project-idDuring firebase deploy, you will be prompted to set any secret values not yet stored in Cloud Secret Manager.
Recommended: use predeploy in firebase.json
Add a predeploy hook so firebase deploy triggers the build automatically:
{
"functions": {
"source": "firebase-dist/functions",
"predeploy": ["next build --webpack"]
}
}Then deploy with:
firebase deploy --only functions --project my-project-idFirebase Environment Utility
Import from the environment subpath to read the active Firebase project. Useful for conditional config based on the active environment (e.g. production vs staging).
Requires firebase-tools as a peer dependency (npm install --save-dev firebase-tools).
import { getFirebaseEnvironment } from 'nextjs-adapter-firebase-functions/environment'
const env = await getFirebaseEnvironment()
// env?.projectId → "my-firebase-project"
// env?.alias → "default"
// env?.root → "/path/to/project"Example — conditional minInstances based on alias:
// adapter.config.ts
import { getFirebaseEnvironment } from 'nextjs-adapter-firebase-functions/environment'
import { createFirebaseAdapter } from 'nextjs-adapter-firebase-functions'
const env = await getFirebaseEnvironment()
export default createFirebaseAdapter({
secrets: ['DATABASE_URL', 'PAYLOAD_SECRET'],
functionConfig: {
region: 'europe-west1',
minInstances: env?.alias === 'production' ? 1 : 0,
},
})Requires a
.firebasercin the project or workspace root. Runfirebase use <project>once to create it.
Options Reference
FirebaseAdapterOptions
interface FirebaseAdapterOptions {
outDir?: string
functionName?: string
secrets?: (string | SecretParam)[]
params?: Record<string, FirebaseParamDefinition>
env?: Record<string, string>
envFilePath?: string
functionConfig?: FirebaseFunctionsHttpsOptions
}outDir
Type: string Default: 'firebase-dist'
Directory (relative to project root) where the adapter writes its output.
functionName
Type: string Default: 'nextjs'
The exported Firebase Function name. Appears in the Firebase console and emulator URL.
createFirebaseAdapter({ functionName: 'server' })
// URL: https://{region}-{project}.cloudfunctions.net/serverfunctionConfig
Type: FirebaseFunctionsHttpsOptions
Options spread directly into onRequest(). All Firebase Functions v2 HttpsOptions fields are accepted.
createFirebaseAdapter({
functionConfig: {
region: 'europe-west1',
timeoutSeconds: 120, // default: 60
memory: '2GiB', // default: '1GiB'
minInstances: 1, // default: 0 (cold start on first request)
maxInstances: 100,
concurrency: 80, // default: 80
invoker: 'public', // default: 'public'
labels: { team: 'frontend' },
},
})memory values: '128MiB' | '256MiB' | '512MiB' | '1GiB' | '2GiB' | '4GiB' | '8GiB' | '16GiB' | '32GiB'
invoker: 'public' (anyone can call) | 'private' (requires auth) | string[] (service accounts)
secrets
Type: (string | SecretParam)[]
Cloud Secret Manager secrets to bind to the function. Accepts plain name strings or SecretParam objects from defineSecret(). At runtime, bound secrets are available as process.env.SECRET_NAME.
// Plain strings
createFirebaseAdapter({
secrets: ['DATABASE_URL', 'STRIPE_SECRET_KEY'],
})// SecretParam objects (e.g. shared across multiple functions)
import { defineSecret } from 'firebase-functions/params'
const DATABASE_URL = defineSecret('DATABASE_URL')
const STRIPE_SECRET_KEY = defineSecret('STRIPE_SECRET_KEY')
export default createFirebaseAdapter({
secrets: [DATABASE_URL, STRIPE_SECRET_KEY],
})Access in your Next.js code without any imports:
// app/api/charge/route.ts
export async function POST(req: Request) {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
}Setting secrets:
firebase functions:secrets:set DATABASE_URL
# prompts for value, stores in Cloud Secret Managerparams
Type: Record<string, FirebaseParamDefinition>
Non-sensitive configuration values (feature flags, API endpoints, limits). Available as process.env.KEY at runtime.
createFirebaseAdapter({
params: {
NEXT_PUBLIC_API_URL: { type: 'string', default: 'https://api.example.com' },
ITEMS_PER_PAGE: { type: 'int', default: 20 },
ENABLE_ANALYTICS: { type: 'boolean', default: true },
},
})interface FirebaseParamDefinition {
type: 'string' | 'int' | 'boolean'
default?: string | number | boolean
description?: string
}env
Type: Record<string, string>
Inline environment variables written to functions/.env. Available as process.env.KEY at runtime and in the emulator.
envFilePath
Type: string
Path to a .env file (relative to project root) merged into functions/.env. Inline env values take precedence on conflict.
createFirebaseAdapter({ envFilePath: '.env.production' })Output File Details
functions/index.js
Generated Firebase Function entry — re-generated on every build, do not edit manually.
- Function export name is baked in at build time from
functionName - Next.js initialization is deferred to the first request, with automatic retry on transient failures
- Cloud Functions pre-reads all request bodies into
req.rawBody— the generated entry restores the stream so Next.js App Router can read it correctly
functions/firebase-params.js
Generated secret/param definitions. Can be required from other functions in the same codebase to share secrets:
const { DATABASE_URL } = require('./firebase-params')
exports.myOtherFunction = onRequest({ secrets: [DATABASE_URL] }, handler)firebase.json
Written to the project root if it doesn't already exist. If you already have a firebase.json, it is never overwritten — merge the generated settings manually.
Testing
Unit tests
node --import tsx/esm --test test/unit/*.test.tsE2E tests — Next.js starter
Scaffolds create-next-app, builds with the adapter, deploys to the Firebase emulator:
ADAPTER_DIR="$(pwd)" bash scripts/e2e-deploy-starter.shE2E tests — Payload CMS
Scaffolds a Payload CMS blank app, builds with the adapter, deploys to the Firebase emulator. Uses mongodb-memory-server — no external MongoDB required:
ADAPTER_DIR="$(pwd)" bash scripts/e2e-deploy-payload.shBoth scripts accept:
| Variable | Default | Description |
|---|---|---|
| ADAPTER_DIR | (required) | Path to this repo root |
| NEXT_TEST_DIR | temp dir | Directory to scaffold into |
| FIREBASE_PROJECT_ID | (required) | Firebase project ID for emulator |
License
MIT
