Site | NPM Module | GitHub Repo
Backend Manager (BEM) is an NPM module for Firebase developers that instantly implements powerful backend features including authentication, rate limiting, analytics, and more.
Installation
npm install backend-manager
Requirements:
- Node.js 22
- Firebase project with Firestore and Authentication enabled
service-account.json - Firebase service account credentials
backend-manager-config.json - BEM configuration file
Quick Start
Create functions/index.js:
const Manager = (new (require('backend-manager'))).init(exports, {
setupFunctionsIdentity: true,
});
const { functions } = Manager.libraries;
exports.myEndpoint = functions
.runWith({ memory: '256MB', timeoutSeconds: 120 })
.https.onRequest((req, res) => Manager.Middleware(req, res).run('myEndpoint', { }));
Create functions/routes/myEndpoint/index.js:
function Route() {}
Route.prototype.main = async function (assistant) {
const Manager = assistant.Manager;
const user = assistant.usage.user;
const settings = assistant.settings;
assistant.log('Request data:', assistant.request.data);
assistant.respond({ success: true, timestamp: new Date().toISOString() });
};
module.exports = Route;
Create functions/schemas/myEndpoint/index.js:
module.exports = function (assistant) {
return {
defaults: {
message: {
types: ['string'],
default: 'Hello World',
},
},
};
};
Run the setup command:
npx bm setup
Initialization Options
const Manager = (new (require('backend-manager'))).init(exports, options);
initialize | true | Initialize Firebase Admin SDK |
projectType | 'firebase' | 'firebase' for Cloud Functions, 'custom' for Express server |
setupFunctions | true | Setup built-in Cloud Functions (bm_api, etc.) |
setupFunctionsIdentity | true | Setup auth event functions (onCreate, onDelete, beforeCreate, beforeSignIn) |
setupFunctionsLegacy | false | Setup legacy admin functions |
setupServer | true | Setup custom Express server for routes |
routes | '/routes' | Directory for custom route handlers |
schemas | '/schemas' | Directory for schema definitions |
resourceZone | 'us-central1' | Firebase/GCP region |
sentry | true | Enable Sentry error tracking |
serviceAccountPath | 'service-account.json' | Path to Firebase service account |
backendManagerConfigPath | 'backend-manager-config.json' | Path to BEM config file |
initializeLocalStorage | false | Initialize local lowdb storage on startup |
checkNodeVersion | true | Validate Node.js version on startup |
express.bodyParser.json | { limit: '100kb' } | Express JSON body parser options |
express.bodyParser.urlencoded | { limit: '100kb', extended: true } | Express URL-encoded options |
Configuration File
Create backend-manager-config.json in your functions directory:
{
brand: {
id: 'my-app',
name: 'My Brand',
url: 'https://example.com',
contact: {
email: 'support@example.com',
},
images: {
wordmark: 'https://example.com/wordmark.png',
brandmark: 'https://example.com/brandmark.png',
combomark: 'https://example.com/combomark.png',
},
},
sentry: {
dsn: 'https://xxx@xxx.ingest.sentry.io/xxx',
},
google_analytics: {
id: 'G-XXXXXXXXXX',
secret: 'your-ga4-secret',
},
backend_manager: {
key: 'your-admin-key',
namespace: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
},
firebaseConfig: {
apiKey: 'xxx',
authDomain: 'project-id.firebaseapp.com',
projectId: 'project-id',
storageBucket: 'project-id.appspot.com',
messagingSenderId: '123456789',
appId: '1:123:web:456',
measurementId: 'G-XXXXXXXXXX',
},
}
Creating Custom Functions
Routes
Routes handle HTTP requests. Create files in your routes/ directory:
Structure:
routes/{name}/index.js - Handles all HTTP methods
routes/{name}/get.js - Handles GET requests only
routes/{name}/post.js - Handles POST requests only
- (also supports
put.js, delete.js, patch.js)
Route File Pattern:
function Route() {}
Route.prototype.main = async function (assistant) {
const Manager = assistant.Manager;
const usage = assistant.usage;
const user = assistant.usage.user;
const analytics = assistant.analytics;
const settings = assistant.settings;
const data = assistant.request.data;
const body = assistant.request.body;
const query = assistant.request.query;
const headers = assistant.request.headers;
const method = assistant.request.method;
const geolocation = assistant.request.geolocation;
const client = assistant.request.client;
if (!user.authenticated) {
return assistant.respond('Authentication required', { code: 401 });
}
if (!user.roles.admin) {
return assistant.respond('Admin required', { code: 403 });
}
analytics.event('my_event', { action: 'test' });
await usage.validate('requests');
usage.increment('requests');
await usage.update();
assistant.respond({ success: true, data: settings });
};
module.exports = Route;
Schemas
Schemas define and validate request parameters with defaults and plan-based limits:
module.exports = function (assistant, settings, options) {
const user = options.user;
return {
defaults: {
message: {
types: ['string'],
default: 'Hello',
required: false,
},
count: {
types: ['number'],
default: 10,
min: 1,
max: 100,
},
format: {
types: ['string'],
default: 'json',
required: (assistant, settings) => settings.output === 'file',
clean: (value) => value.toLowerCase().trim(),
},
},
premium: {
count: {
types: ['number'],
default: 100,
max: 1000,
},
},
};
};
Schema Property Options:
types | string[] | Allowed types: 'string', 'number', 'boolean', 'object', 'array' |
default | any | Default value if not provided |
required | boolean | function | Whether the field is required |
clean | RegExp | function | Sanitize/transform the value |
min | number | Minimum value (for numbers) |
max | number | Maximum value (for numbers) |
available | boolean | Whether the field is available |
Middleware Options
Manager.Middleware(req, res).run('routeName', {
authenticate: true,
setupAnalytics: true,
setupUsage: true,
setupSettings: true,
schema: 'routeName',
parseMultipartFormData: true,
routesDir: '/routes',
schemasDir: '/schemas',
});
Hook System
Intercept and modify bm_api requests before/after processing:
const Manager = (new (require('backend-manager'))).init(exports, {});
Manager.handlers.bm_api = function (mod, position) {
const assistant = mod.assistant;
return new Promise(async function(resolve, reject) {
const command = mod.assistant.request.data.command || '';
const payload = mod.assistant.request.data.payload || {};
assistant.log('Intercepted bm_api', position, command, payload);
if (command === 'user:sign-up') {
if (position === 'pre') {
assistant.log('Pre sign-up hook');
} else if (position === 'post') {
assistant.log('Post sign-up hook');
}
}
if (command === '*') {
if (position === 'pre') {
} else if (position === 'post') {
}
}
return resolve();
});
};
Built-in Functions
HTTP API (bm_api)
The main API endpoint accepts commands in the format category:action:
{
"command": "general:generate-uuid",
"payload": {
"version": "4"
},
"apiKey": "optional-api-key"
}
Available Commands:
admin | firestore-write, firestore-read, firestore-query, database-write, database-read, send-email, send-notification, payment-processor, backup, cron, create-post, edit-post, get-stats, run-hook, sync-users, write-repo-content |
user | sign-up, delete, oauth2, resolve, get-subscription-info, get-active-sessions, sign-out-all-sessions, create-custom-token, regenerate-api-keys, submit-feedback, validate-settings |
general | generate-uuid, send-email, fetch-post |
handler | create-post |
firebase | get-providers |
test | authenticate, webhook, lab, redirect |
special | setup-electron-manager-client |
Auth Events
bm_authBeforeCreate | beforeUserCreated | Runs before user creation, can block signup |
bm_authBeforeSignIn | beforeUserSignedIn | Runs before sign-in, can block login |
bm_authOnCreate | onCreate | Runs after user creation, creates user document |
bm_authOnDelete | onDelete | Runs when user is deleted, cleanup |
Firestore Events
bm_notificationsOnWrite | onWrite | Triggers on notifications/{id} changes |
Cron Jobs
bm_cronDaily | Every 24 hours | Runs daily jobs from cron/daily/ and hooks/cron/daily/ |
Creating Custom Cron Jobs:
Create hooks/cron/daily/myJob.js in your functions directory:
function Job() {}
Job.prototype.main = function () {
const self = this;
const Manager = self.Manager;
const assistant = self.assistant;
return new Promise(async function(resolve, reject) {
assistant.log('Running my daily job...');
return resolve();
});
};
module.exports = Job;
Helper Classes
Assistant
Handles request/response lifecycle, authentication, and logging.
const assistant = Manager.Assistant({ req, res });
const user = await assistant.authenticate();
assistant.request.data;
assistant.request.body;
assistant.request.query;
assistant.request.headers;
assistant.request.method;
assistant.request.geolocation;
assistant.request.client;
assistant.respond({ success: true });
assistant.respond({ success: true }, { code: 201 });
assistant.respond('https://example.com', { code: 302 });
assistant.errorify('Something went wrong', { code: 500, sentry: true });
assistant.respond(new Error('Bad request'), { code: 400 });
assistant.log('Info message');
assistant.warn('Warning message');
assistant.error('Error message');
assistant.debug('Debug message');
assistant.isDevelopment();
assistant.isProduction();
assistant.isTesting();
const { fields, files } = await assistant.parseMultipartFormData();
User
Creates user objects with default properties:
const userProps = Manager.User(existingData, { defaults: true }).properties;
{
auth: { uid, email, temporary },
plan: {
id: 'basic',
status: 'active',
expires: { timestamp, timestampUNIX },
trial: { activated, expires: {...} },
limits: {},
payment: { processor, orderId, resourceId, frequency, active, startDate, updatedBy }
},
roles: { admin, betaTester, developer },
affiliate: { code, referrals, referrer },
activity: { lastActivity, created, geolocation, client },
api: { clientId, privateKey },
usage: { requests: { period, total, last } },
personal: { birthday, gender, location, name, company, telephone },
oauth2: {}
}
userProps.resolve();
userProps.merge(otherUser);
Analytics
Send events to Google Analytics 4:
const analytics = Manager.Analytics({
assistant: assistant,
uuid: user.auth.uid,
});
analytics.event('purchase', {
item_id: 'product-123',
value: 29.99,
currency: 'USD',
});
Auto-tracked User Properties:
app_version, device_category, operating_system, platform
authenticated, plan_id, plan_trial_activated, activity_created
country, city, language, age, gender
Usage
Track and limit API usage:
const usage = await Manager.Usage().init(assistant, {
app: 'my-app',
key: 'custom-key',
whitelistKeys: ['admin-key'],
unauthenticatedMode: 'firestore',
refetch: false,
log: true,
});
const currentUsage = usage.getUsage('requests');
const limit = usage.getLimit('requests');
await usage.validate('requests');
usage.increment('requests', 1);
usage.set('requests', 0);
await usage.update();
usage.addWhitelistKeys(['another-key']);
Middleware
Process requests through the middleware pipeline:
exports.myEndpoint = functions
.https.onRequest((req, res) => Manager.Middleware(req, res).run('myEndpoint', {
authenticate: true,
setupAnalytics: true,
setupUsage: true,
setupSettings: true,
schema: 'myEndpoint',
}));
The middleware automatically:
- Parses multipart form data
- Logs request details
- Loads route handler (method-specific or index.js)
- Authenticates user
- Initializes usage tracking
- Sets up analytics
- Resolves settings from schema
- Calls your route handler
Settings
Resolve and validate request settings against a schema:
const settings = Manager.Settings().resolve(assistant, schema, inputSettings, {
dir: '/schemas',
schema: 'mySchema',
user: user,
checkRequired: true,
});
const timestamp = Manager.Settings().constant('timestamp');
const timestampUNIX = Manager.Settings().constant('timestampUNIX');
const timestampFULL = Manager.Settings().constant('timestampFULL');
Utilities
Batch operations and helper functions:
const utilities = Manager.Utilities();
const results = await utilities.iterateCollection(
async ({ docs }, batch, totalCount) => {
for (const doc of docs) {
}
return { processed: docs.length };
},
{
collection: 'users',
batchSize: 1000,
maxBatches: 10,
where: [{ field: 'plan.id', operator: '==', value: 'premium' }],
orderBy: { field: 'activity.created.timestamp', direction: 'desc' },
startAfter: 'lastDocId',
log: true,
}
);
await utilities.iterateUsers(
async ({ users, pageToken }, batch) => {
for (const user of users) {
}
},
{
batchSize: 1000,
maxBatches: Infinity,
log: true,
}
);
const { document, user } = await utilities.getDocumentWithOwnerUser('posts/abc123', {
owner: 'owner.uid',
resolve: {
schema: 'posts',
assistant: assistant,
checkRequired: false,
},
});
const id = utilities.randomId({ size: 14 });
const doc = await utilities.get('users/abc123', {
maxAge: 1000 * 60 * 5,
format: 'data',
});
Metadata
Add timestamps and tags to documents:
const metadata = Manager.Metadata(document);
document.metadata = metadata.set({ tag: 'my-operation' });
Local Storage
Persistent JSON storage using lowdb:
const storage = Manager.storage({
name: 'myStorage',
temporary: false,
clear: true,
log: false,
});
storage.set('key', 'value').write();
const value = storage.get('key').value();
storage.set('nested.path', { data: true }).write();
Authentication
BEM supports multiple authentication methods (checked in order):
-
Bearer Token (JWT)
Authorization: Bearer <firebase-id-token>
-
API Key
{ apiKey: 'user-private-key' }
{ authenticationToken: 'user-private-key' }
-
Backend Manager Key (Admin access)
{ backendManagerKey: 'your-backend-manager-key' }
-
Session Cookie
Cookie: __session=<firebase-id-token>
Authenticated User Object:
const user = await assistant.authenticate();
{
authenticated: true,
auth: { uid: 'abc123', email: 'user@example.com' },
roles: { admin: false, betaTester: false, developer: false },
plan: { id: 'basic', status: 'active', ... },
api: { clientId: '...', privateKey: '...' },
}
CLI Commands
BEM includes a CLI for development and deployment:
npm install -g backend-manager
npx backend-manager <command>
bem setup | Run Firebase project setup and validation |
bem serve | Start local Firebase emulator |
bem deploy | Deploy functions to Firebase |
bem test [paths...] | Run integration tests |
bem emulators | Start Firebase emulators (keep-alive mode) |
bem version, bem v | Show BEM version |
bem clear | Clear cache and temp files |
bem install, bem i | Install BEM (local or production) |
bem clean:npm | Clean and reinstall npm modules |
bem firestore:indexes:get | Get Firestore indexes |
bem cwd | Show current working directory |
Environment Variables
Set these in your functions/.env file:
BACKEND_MANAGER_KEY | Admin authentication key |
BEM attaches metadata to responses:
bm-properties: {"code":200,"tag":"functionName/executionId","usage":{...},"schema":{...}}
Testing
BEM includes an integration test framework that runs against Firebase emulators.
Running Tests
npx bm emulators
npx bm test
npx bm test
Filtering Tests
npx bm test rules/
npx bm test bem:rules/
npx bm test project:rules/
npx bm test user/ admin/
Test Locations
- BEM core tests:
test/
- Project tests:
functions/test/bem/
Use bem: or project: prefix to filter by source.
Writing Tests
Suite - Sequential tests with shared state (stops on first failure):
module.exports = {
description: 'User signup flow with affiliate tracking',
type: 'suite',
tests: [
{
name: 'verify-referrer-exists',
async run({ firestore, assert, state, accounts }) {
state.referrerUid = accounts.referrer.uid;
const doc = await firestore.get(`users/${state.referrerUid}`);
assert.ok(doc, 'Referrer should exist');
},
},
{
name: 'call-user-signup-with-affiliate',
async run({ http, assert, state }) {
const response = await http.as('referred').command('user:sign-up', {
attribution: { affiliate: { code: 'TESTREF' } },
});
assert.isSuccess(response);
},
},
],
};
Group - Independent tests (continues even if one fails):
module.exports = {
description: 'Admin Firestore write operation',
type: 'group',
tests: [
{
name: 'admin-auth-succeeds',
auth: 'admin',
async run({ http, assert }) {
const response = await http.command('admin:firestore-write', {
path: '_test/doc',
document: { test: 'value' },
});
assert.isSuccess(response);
},
},
{
name: 'unauthenticated-rejected',
auth: 'none',
async run({ http, assert }) {
const response = await http.command('admin:firestore-write', {
path: '_test/doc',
document: { test: 'value' },
});
assert.isError(response, 401);
},
},
],
};
Auth levels: none, user/basic, admin, premium-active, premium-expired
See CLAUDE.md for complete test API documentation.
Final Words
If you are still having difficulty, we would love for you to post a question to the Backend Manager issues page. It is much easier to answer questions that include your code and relevant files! So if you can provide them, we'd be extremely grateful (and more likely to help you find the answer!)
Projects Using this Library
Somiibo: A Social Media Bot with an open-source module library.
JekyllUp: A website devoted to sharing the best Jekyll themes.
Slapform: A backend processor for your HTML forms on static sites.
SoundGrail Music App: A resource for producers, musicians, and DJs.
Hammock Report: An API for exploring and listing backyard products.
Ask us to have your project listed! :)
License
ISC