{"id":15067601,"url":"https://github.com/itw-creative-works/backend-manager","last_synced_at":"2026-05-24T10:00:43.074Z","repository":{"id":45556044,"uuid":"228178670","full_name":"itw-creative-works/backend-manager","owner":"itw-creative-works","description":"Backend manager by ITW Creative Works. Built to optimize Firebase development.","archived":false,"fork":false,"pushed_at":"2026-05-22T00:43:13.000Z","size":2923,"stargazers_count":2,"open_issues_count":14,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-22T08:17:18.869Z","etag":null,"topics":["api","backend","express","firebase","js"],"latest_commit_sha":null,"homepage":"https://itwcreativeworks.com","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/itw-creative-works.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2019-12-15T12:14:22.000Z","updated_at":"2026-05-22T00:40:36.000Z","dependencies_parsed_at":"2025-01-29T02:27:53.262Z","dependency_job_id":"d881c13b-ad87-4202-b415-019c6f8a56ad","html_url":"https://github.com/itw-creative-works/backend-manager","commit_stats":{"total_commits":347,"total_committers":2,"mean_commits":173.5,"dds":"0.10374639769452454","last_synced_commit":"88187408c1206fc28d20ef815246c5ff3d50bc57"},"previous_names":[],"tags_count":51,"template":false,"template_full_name":null,"purl":"pkg:github/itw-creative-works/backend-manager","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itw-creative-works%2Fbackend-manager","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itw-creative-works%2Fbackend-manager/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itw-creative-works%2Fbackend-manager/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itw-creative-works%2Fbackend-manager/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/itw-creative-works","download_url":"https://codeload.github.com/itw-creative-works/backend-manager/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itw-creative-works%2Fbackend-manager/sbom","scorecard":{"id":497698,"data":{"date":"2025-08-11","repo":{"name":"github.com/itw-creative-works/backend-manager","commit":"f16ecade5fb95d240ce253dbeae00419a95761cf"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.7,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Maintained","score":7,"reason":"9 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 7","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":0,"reason":"21 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-7v5v-9h63-cj86","Warn: Project is vulnerable to: GHSA-h5c3-5r3r-rr8q","Warn: Project is vulnerable to: GHSA-rmvr-2pp2-xj38","Warn: Project is vulnerable to: GHSA-xx4v-prfh-6cgc","Warn: Project is vulnerable to: GHSA-wf5p-g6vw-rhxx","Warn: Project is vulnerable to: GHSA-jr5f-v2jv-69x6","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-pxg6-pf52-xh8x","Warn: Project is vulnerable to: GHSA-3wf4-68gx-mph8","Warn: Project is vulnerable to: GHSA-jchw-25xp-jwwc","Warn: Project is vulnerable to: GHSA-cxjh-pqwp-8mfp","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-qrpm-p2h7-hrv2","Warn: Project is vulnerable to: GHSA-mwcw-c2x4-8c55","Warn: Project is vulnerable to: GHSA-h755-8qp9-cq85","Warn: Project is vulnerable to: GHSA-p8p7-x288-28g6","Warn: Project is vulnerable to: GHSA-f5x3-32g6-xq36","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-19T20:55:03.526Z","repository_id":45556044,"created_at":"2025-08-19T20:55:03.526Z","updated_at":"2025-08-19T20:55:03.526Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33429192,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T22:14:44.296Z","status":"online","status_checked_at":"2026-05-24T02:00:06.296Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","backend","express","firebase","js"],"created_at":"2024-09-25T01:25:09.210Z","updated_at":"2026-05-24T10:00:43.058Z","avatar_url":"https://github.com/itw-creative-works.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://itwcreativeworks.com\"\u003e\n    \u003cimg src=\"https://cdn.itwcreativeworks.com/assets/itw-creative-works/images/logo/itw-creative-works-brandmark-black-x.svg\" width=\"100px\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/github/package-json/v/itw-creative-works/backend-manager.svg\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\"https://img.shields.io/librariesio/release/npm/backend-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/bundlephobia/min/backend-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/codeclimate/maintainability-percentage/itw-creative-works/backend-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/npm/dm/backend-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/node/v/backend-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/website/https/itwcreativeworks.com.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/github/license/itw-creative-works/backend-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/github/contributors/itw-creative-works/backend-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/github/last-commit/itw-creative-works/backend-manager.svg\"\u003e\n  \u003cbr\u003e\n  \u003cbr\u003e\n  \u003ca href=\"https://itwcreativeworks.com\"\u003eSite\u003c/a\u003e | \u003ca href=\"https://www.npmjs.com/package/backend-manager\"\u003eNPM Module\u003c/a\u003e | \u003ca href=\"https://github.com/itw-creative-works/backend-manager\"\u003eGitHub Repo\u003c/a\u003e\n  \u003cbr\u003e\n  \u003cbr\u003e\n  \u003cstrong\u003eBackend Manager (BEM)\u003c/strong\u003e is an NPM module for Firebase developers that instantly implements powerful backend features including authentication, rate limiting, analytics, and more.\n\u003c/p\u003e\n\n## Installation\n\n```bash\nnpm install backend-manager\n```\n\n**Requirements:**\n- Node.js 22\n- Firebase project with Firestore and Authentication enabled\n- `service-account.json` - Firebase service account credentials\n- `backend-manager-config.json` - BEM configuration file\n\n## Quick Start\n\nCreate `functions/index.js`:\n\n```javascript\nconst Manager = (new (require('backend-manager'))).init(exports, {\n  setupFunctionsIdentity: true,\n});\nconst { functions } = Manager.libraries;\n\n// Create a custom function\nexports.myEndpoint = functions\n  .runWith({ memory: '256MB', timeoutSeconds: 120 })\n  .https.onRequest((req, res) =\u003e Manager.Middleware(req, res).run('myEndpoint', { /* options */ }));\n```\n\nCreate `functions/routes/myEndpoint/index.js`:\n\n```javascript\nfunction Route() {}\n\nRoute.prototype.main = async function (assistant) {\n  const Manager = assistant.Manager;\n  const user = assistant.usage.user;\n  const settings = assistant.settings;\n\n  assistant.log('Request data:', assistant.request.data);\n\n  // Return response\n  assistant.respond({ success: true, timestamp: new Date().toISOString() });\n};\n\nmodule.exports = Route;\n```\n\nCreate `functions/schemas/myEndpoint/index.js`:\n\n```javascript\nmodule.exports = function (assistant) {\n  return {\n    defaults: {\n      message: {\n        types: ['string'],\n        default: 'Hello World',\n      },\n    },\n  };\n};\n```\n\nRun the setup command:\n\n```bash\nnpx mgr setup\n```\n\n## Initialization Options\n\n```javascript\nconst Manager = (new (require('backend-manager'))).init(exports, options);\n```\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `initialize` | `true` | Initialize Firebase Admin SDK |\n| `projectType` | `'firebase'` | `'firebase'` for Cloud Functions, `'custom'` for Express server |\n| `setupFunctions` | `true` | Setup built-in Cloud Functions (`bm_api`, etc.) |\n| `setupFunctionsIdentity` | `true` | Setup auth event functions (onCreate, onDelete, beforeCreate, beforeSignIn) |\n| `setupFunctionsLegacy` | `false` | Setup legacy admin functions |\n| `setupServer` | `true` | Setup custom Express server for routes |\n| `routes` | `'/routes'` | Directory for custom route handlers |\n| `schemas` | `'/schemas'` | Directory for schema definitions |\n| `resourceZone` | `'us-central1'` | Firebase/GCP region |\n| `sentry` | `true` | Enable Sentry error tracking |\n| `serviceAccountPath` | `'service-account.json'` | Path to Firebase service account |\n| `backendManagerConfigPath` | `'backend-manager-config.json'` | Path to BEM config file |\n| `initializeLocalStorage` | `false` | Initialize local lowdb storage on startup |\n| `checkNodeVersion` | `true` | Validate Node.js version on startup |\n| `express.bodyParser.json` | `{ limit: '100kb' }` | Express JSON body parser options |\n| `express.bodyParser.urlencoded` | `{ limit: '100kb', extended: true }` | Express URL-encoded options |\n\n## Configuration File\n\nCreate `backend-manager-config.json` in your functions directory:\n\n```json5\n{\n  brand: {\n    id: 'my-app',\n    name: 'My Brand',\n    url: 'https://example.com',\n    contact: {\n      email: 'support@example.com',\n    },\n    images: {\n      wordmark: 'https://example.com/wordmark.png',\n      brandmark: 'https://example.com/brandmark.png',\n      combomark: 'https://example.com/combomark.png',\n    },\n  },\n  sentry: {\n    dsn: 'https://xxx@xxx.ingest.sentry.io/xxx',\n  },\n  googleAnalytics: {\n    id: 'G-XXXXXXXXXX',\n    secret: 'your-ga4-secret',\n  },\n  backend_manager: {\n    key: 'your-admin-key',\n    namespace: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',\n  },\n  firebaseConfig: {\n    apiKey: 'xxx',\n    authDomain: 'project-id.firebaseapp.com',\n    projectId: 'project-id',\n    storageBucket: 'project-id.appspot.com',\n    messagingSenderId: '123456789',\n    appId: '1:123:web:456',\n    measurementId: 'G-XXXXXXXXXX',\n  },\n}\n```\n\n## Creating Custom Functions\n\n### Routes\n\nRoutes handle HTTP requests. Create files in your `routes/` directory:\n\n**Structure:**\n- `routes/{name}/index.js` - Handles all HTTP methods\n- `routes/{name}/get.js` - Handles GET requests only\n- `routes/{name}/post.js` - Handles POST requests only\n- (also supports `put.js`, `delete.js`, `patch.js`)\n\n**Route File Pattern:**\n\n```javascript\nfunction Route() {}\n\nRoute.prototype.main = async function (assistant) {\n  // Access Manager and helpers\n  const Manager = assistant.Manager;\n  const usage = assistant.usage;\n  const user = assistant.usage.user;\n  const analytics = assistant.analytics;\n  const settings = assistant.settings;\n\n  // Access request data\n  const data = assistant.request.data;       // Merged body + query\n  const body = assistant.request.body;       // POST body\n  const query = assistant.request.query;     // Query params\n  const headers = assistant.request.headers;\n  const method = assistant.request.method;\n  const geolocation = assistant.request.geolocation; // { ip, country, region, city, latitude, longitude }\n  const client = assistant.request.client;   // { userAgent, language, platform, mobile }\n\n  // Check authentication\n  if (!user.authenticated) {\n    return assistant.respond('Authentication required', { code: 401 });\n  }\n\n  // Check admin role\n  if (!user.roles.admin) {\n    return assistant.respond('Admin required', { code: 403 });\n  }\n\n  // Track analytics\n  analytics.event('my_event', { action: 'test' });\n\n  // Validate usage limits\n  await usage.validate('requests');\n  usage.increment('requests');\n  await usage.update();\n\n  // Send response\n  assistant.respond({ success: true, data: settings });\n};\n\nmodule.exports = Route;\n```\n\n### Schemas\n\nSchemas define and validate request parameters with defaults and plan-based limits:\n\n```javascript\nmodule.exports = function (assistant, settings, options) {\n  const user = options.user;\n\n  return {\n    // Default values for all plans\n    defaults: {\n      message: {\n        types: ['string'],\n        default: 'Hello',\n        required: false,\n      },\n      count: {\n        types: ['number'],\n        default: 10,\n        min: 1,\n        max: 100,\n      },\n      format: {\n        types: ['string'],\n        default: 'json',\n        // Dynamic required based on other settings\n        required: (assistant, settings) =\u003e settings.output === 'file',\n        // Clean/sanitize input\n        clean: (value) =\u003e value.toLowerCase().trim(),\n      },\n    },\n\n    // Override defaults for premium plan\n    premium: {\n      count: {\n        types: ['number'],\n        default: 100,\n        max: 1000,\n      },\n    },\n  };\n};\n```\n\n**Schema Property Options:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `types` | `string[]` | Allowed types: `'string'`, `'number'`, `'boolean'`, `'object'`, `'array'` |\n| `default` | `any` | Default value if not provided |\n| `required` | `boolean \\| function` | Whether the field is required |\n| `clean` | `RegExp \\| function` | Sanitize/transform the value |\n| `min` | `number` | Minimum value (for numbers) |\n| `max` | `number` | Maximum value (for numbers) |\n| `available` | `boolean` | Whether the field is available |\n\n### Middleware Options\n\n```javascript\nManager.Middleware(req, res).run('routeName', {\n  authenticate: true,           // Authenticate user (default: true)\n  setupAnalytics: true,         // Initialize analytics (default: true)\n  setupUsage: true,             // Initialize usage tracking (default: true)\n  setupSettings: true,          // Resolve settings from schema (default: true)\n  schema: 'routeName',          // Schema file to use (default: same as route)\n  parseMultipartFormData: true, // Parse multipart uploads (default: true)\n  routesDir: '/routes',         // Custom routes directory\n  schemasDir: '/schemas',       // Custom schemas directory\n});\n```\n\n## Hook System\n\nIntercept and modify `bm_api` requests before/after processing:\n\n```javascript\nconst Manager = (new (require('backend-manager'))).init(exports, {});\n\nManager.handlers.bm_api = function (mod, position) {\n  const assistant = mod.assistant;\n\n  return new Promise(async function(resolve, reject) {\n    const command = mod.assistant.request.data.command || '';\n    const payload = mod.assistant.request.data.payload || {};\n\n    assistant.log('Intercepted bm_api', position, command, payload);\n\n    // Handle specific commands\n    if (command === 'user:sign-up') {\n      if (position === 'pre') {\n        // Before sign-up: validate, modify payload, etc.\n        assistant.log('Pre sign-up hook');\n      } else if (position === 'post') {\n        // After sign-up: send notifications, etc.\n        assistant.log('Post sign-up hook');\n      }\n    }\n\n    // Handle all commands\n    if (command === '*') {\n      if (position === 'pre') {\n        // Before any command\n      } else if (position === 'post') {\n        // After any command\n      }\n    }\n\n    return resolve();\n  });\n};\n```\n\n## Built-in Functions\n\n### HTTP API (`bm_api`)\n\nThe main API endpoint accepts commands in the format `category:action`:\n\n```javascript\n// POST to https://us-central1-{project}.cloudfunctions.net/bm_api\n{\n  \"command\": \"general:generate-uuid\",\n  \"payload\": {\n    \"version\": \"4\"\n  },\n  \"apiKey\": \"optional-api-key\"\n}\n```\n\n**Available Commands:**\n\n| Category | Commands |\n|----------|----------|\n| `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` |\n| `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` |\n| `general` | `generate-uuid`, `send-email`, `fetch-post` |\n| `handler` | `create-post` |\n| `firebase` | `get-providers` |\n| `test` | `authenticate`, `webhook`, `lab`, `redirect` |\n| `special` | `setup-electron-manager-client` |\n\n### Auth Events\n\n| Function | Trigger | Description |\n|----------|---------|-------------|\n| `bm_authBeforeCreate` | `beforeUserCreated` | Runs before user creation, can block signup |\n| `bm_authBeforeSignIn` | `beforeUserSignedIn` | Runs before sign-in, can block login |\n| `bm_authOnCreate` | `onCreate` | Runs after user creation, creates user document |\n| `bm_authOnDelete` | `onDelete` | Runs when user is deleted, cleanup |\n\n### Firestore Events\n\n| Function | Trigger | Description |\n|----------|---------|-------------|\n| `bm_notificationsOnWrite` | `onWrite` | Triggers on `notifications/{id}` changes |\n\n### Cron Jobs\n\n| Function | Schedule | Description |\n|----------|----------|-------------|\n| `bm_cronDaily` | Every 24 hours | Runs daily jobs from `cron/daily/` and `hooks/cron/daily/` |\n\n**Creating Custom Cron Jobs:**\n\nCreate `hooks/cron/daily/myJob.js` in your functions directory:\n\n```javascript\nfunction Job() {}\n\nJob.prototype.main = function () {\n  const self = this;\n  const Manager = self.Manager;\n  const assistant = self.assistant;\n\n  return new Promise(async function(resolve, reject) {\n    assistant.log('Running my daily job...');\n\n    // Your job logic here\n\n    return resolve();\n  });\n};\n\nmodule.exports = Job;\n```\n\n## Marketing \u0026 Campaigns\n\nBuilt-in marketing system with multi-provider support (SendGrid + Beehiiv + FCM push).\n\n- **Contact management** — add, sync, remove contacts across providers with custom field syncing\n- **Campaign CRUD** — `POST/GET/PUT/DELETE /marketing/campaign` with calendar-backed scheduling\n- **Recurring campaigns** — seasonal sales, newsletters with automatic sendAt advancement\n- **Newsletter generator** — AI-assembled newsletters from parent server content sources\n- **Segment SSOT** — 22 segment definitions resolved to provider IDs at runtime\n- **UTM auto-tagging** — brand domain links tagged automatically in marketing + transactional emails\n- **Contact pruning** — monthly 2-stage re-engagement + deletion of inactive contacts\n- **Template variables** — `{brand.name}`, `{holiday.name}`, `{season.name}`, `{date.*}` resolved at send time\n\nConfigure via `marketing` section in `backend-manager-config.json`. See CLAUDE.md for full documentation.\n\n## Marketing Consent\n\nGDPR/CASL-compliant consent capture and cross-provider unsubscribe sync.\n\n- **Two-checkbox signup form** — separate legal (required) and marketing (optional) consent\n- **Canonical user-doc shape** — `consent.{legal,marketing}.{status, grantedAt, revokedAt}` with full audit metadata (timestamp, source, IP, exact label text)\n- **Server-authoritative timestamps** — client timestamps ignored, defending against clock manipulation\n- **Account-page toggle** — `/account` notifications section lets logged-in users opt in/out, hits both SendGrid + Beehiiv\n- **HMAC unsubscribe links** — email-footer one-click flow continues to work\n- **Provider webhook receivers** — `POST /marketing/webhook?provider=sendgrid|beehiiv\u0026key=X` catches unsubscribe / spam / bounce events from SendGrid and Beehiiv, writes the user doc + syncs to the OTHER provider\n- **Parent forwarder** — single public webhook endpoint (`/marketing/webhook/forward`) on the parent BEM fans out to every brand's child BEM so each one updates its own Firestore\n- **Guard against marketing sync without consent** — signup route and `email.add()` short-circuit when `consent.marketing.status !== 'granted'`\n\nSee [docs/consent.md](docs/consent.md) for the full architecture, source enum reference, migration script template, and provider configuration steps.\n\n## Helper Classes\n\n### Assistant\n\nHandles request/response lifecycle, authentication, and logging.\n\n```javascript\nconst assistant = Manager.Assistant({ req, res });\n\n// Authentication\nconst user = await assistant.authenticate();\n// Returns: { authenticated, auth: { uid, email }, roles, plan, ... }\n\n// Request data\nassistant.request.data;        // Merged body + query\nassistant.request.body;        // POST body\nassistant.request.query;       // Query params\nassistant.request.headers;     // Request headers\nassistant.request.method;      // HTTP method\nassistant.request.geolocation; // { ip, country, region, city, latitude, longitude }\nassistant.request.client;      // { userAgent, language, platform, mobile }\n\n// Response\nassistant.respond({ success: true });              // 200 JSON\nassistant.respond({ success: true }, { code: 201 }); // Custom status\nassistant.respond('https://example.com', { code: 302 }); // Redirect\n\n// Errors\nassistant.errorify('Something went wrong', { code: 500, sentry: true });\nassistant.respond(new Error('Bad request'), { code: 400 });\n\n// Logging\nassistant.log('Info message');\nassistant.warn('Warning message');\nassistant.error('Error message');\nassistant.debug('Debug message');\n\n// Environment\nassistant.isDevelopment(); // true in emulator\nassistant.isProduction();  // true in production\nassistant.isTesting();     // true when running tests\n\n// File uploads\nconst { fields, files } = await assistant.parseMultipartFormData();\n```\n\n### User\n\nCreates user objects with default properties:\n\n```javascript\nconst userProps = Manager.User(existingData, { defaults: true }).properties;\n\n// User structure:\n{\n  auth: { uid, email, temporary },\n  subscription: {\n    product: { id, name },   // product from config ('basic', 'premium', etc.)\n    status: 'active',        // active | suspended | cancelled\n    expires: { timestamp, timestampUNIX },\n    trial: { claimed, expires: {...} },\n    cancellation: { pending, date: {...} },\n    limits: {},\n    payment: { processor, resourceId, frequency, startDate, updatedBy }\n  },\n  roles: { admin, betaTester, developer },\n  affiliate: { code, referrals, referrer },\n  metadata: { created, updated },\n  activity: { geolocation, client },\n  api: { clientId, privateKey },\n  usage: { requests: { monthly, daily, total, last } },\n  personal: { birthday, gender, location, name, company, telephone },\n  oauth2: {}\n}\n\n// Methods\nuserProps.merge(otherUser);    // Merge with another user object\n```\n\n### Analytics\n\nSend events to Google Analytics 4:\n\n```javascript\nconst analytics = Manager.Analytics({\n  assistant: assistant,\n  uuid: user.auth.uid,\n});\n\nanalytics.event('purchase', {\n  item_id: 'product-123',\n  value: 29.99,\n  currency: 'USD',\n});\n```\n\n**Auto-tracked User Properties:**\n- `app_version`, `device_category`, `operating_system`, `platform`\n- `authenticated`, `subscription_id`, `subscription_trial_claimed`, `activity_created`\n- `country`, `city`, `language`, `age`, `gender`\n\n### Usage\n\nTrack and limit API usage:\n\n```javascript\nconst usage = await Manager.Usage().init(assistant, {\n  app: 'my-app',                    // App ID for limits\n  key: 'custom-key',                // Optional custom key (default: user UID or IP)\n  whitelistKeys: ['admin-key'],     // Keys that bypass limits\n  unauthenticatedMode: 'firestore', // 'firestore' or 'local'\n  refetch: false,                   // Force refetch app limits\n  log: true,                        // Enable logging\n});\n\n// Check and validate limits\nconst currentUsage = usage.getUsage('requests');  // Get current monthly usage\nconst limit = usage.getLimit('requests');         // Get plan limit (monthly)\nawait usage.validate('requests');                 // Throws if over daily or monthly limit\n\n// Increment usage (increments monthly, daily, and total counters)\nusage.increment('requests', 1);\nusage.set('requests', 0);  // Reset monthly to specific value\n\n// Save to Firestore\nawait usage.update();\n\n// Whitelist keys\nusage.addWhitelistKeys(['another-key']);\n\n// Proxy usage: bill a different user and mirror writes to additional docs\nawait usage.setUser('owner-uid');          // Switch target user (fetches from Firestore)\nusage.addMirror('agents/agent-id');        // Also write usage to this doc on update()\nusage.setMirrors(['agents/a', 'orgs/b']); // Overwrite mirror list\n```\n\n### Middleware\n\nProcess requests through the middleware pipeline:\n\n```javascript\n// In your function definition\nexports.myEndpoint = functions\n  .https.onRequest((req, res) =\u003e Manager.Middleware(req, res).run('myEndpoint', {\n    authenticate: true,\n    setupAnalytics: true,\n    setupUsage: true,\n    setupSettings: true,\n    schema: 'myEndpoint',\n  }));\n```\n\nThe middleware automatically:\n1. Parses multipart form data\n2. Logs request details\n3. Loads route handler (method-specific or index.js)\n4. Authenticates user\n5. Initializes usage tracking\n6. Sets up analytics\n7. Resolves settings from schema\n8. Calls your route handler\n\n### Settings\n\nResolve and validate request settings against a schema:\n\n```javascript\nconst settings = Manager.Settings().resolve(assistant, schema, inputSettings, {\n  dir: '/schemas',\n  schema: 'mySchema',\n  user: user,\n  checkRequired: true,\n});\n\n// Timestamp constants\nconst timestamp = Manager.Settings().constant('timestamp');\n// { types: ['string'], value: undefined, default: '2024-01-01T00:00:00.000Z' }\n\nconst timestampUNIX = Manager.Settings().constant('timestampUNIX');\n// { types: ['number'], value: undefined, default: 1704067200 }\n\nconst timestampFULL = Manager.Settings().constant('timestampFULL');\n// { timestamp: {...}, timestampUNIX: {...} }\n```\n\n### Utilities\n\nBatch operations and helper functions:\n\n```javascript\nconst utilities = Manager.Utilities();\n\n// Batch iterate Firestore collection\nconst results = await utilities.iterateCollection(\n  async ({ docs }, batch, totalCount) =\u003e {\n    for (const doc of docs) {\n      // Process each document\n    }\n    return { processed: docs.length };\n  },\n  {\n    collection: 'users',\n    batchSize: 1000,\n    maxBatches: 10,\n    where: [{ field: 'subscription.product.id', operator: '==', value: 'premium' }],\n    orderBy: { field: 'metadata.created.timestamp', direction: 'desc' },\n    startAfter: 'lastDocId',\n    log: true,\n  }\n);\n\n// Batch iterate Firebase Auth users\nawait utilities.iterateUsers(\n  async ({ users, pageToken }, batch) =\u003e {\n    for (const user of users) {\n      // Process each auth user\n    }\n  },\n  {\n    batchSize: 1000,\n    maxBatches: Infinity,\n    log: true,\n  }\n);\n\n// Get document with owner user\nconst { document, user } = await utilities.getDocumentWithOwnerUser('posts/abc123', {\n  owner: 'owner',\n  resolve: {\n    schema: 'posts',\n    assistant: assistant,\n    checkRequired: false,\n  },\n});\n\n// Generate random ID\nconst id = utilities.randomId({ size: 14 }); // 'A1b2C3d4E5f6G7'\n\n// Cached Firestore read\nconst doc = await utilities.get('users/abc123', {\n  maxAge: 1000 * 60 * 5, // 5 minute cache\n  format: 'data',        // 'raw' or 'data'\n});\n```\n\n### Metadata\n\nAdd timestamps and tags to documents:\n\n```javascript\nconst metadata = Manager.Metadata(document);\n\ndocument.metadata = metadata.set({ tag: 'my-operation' });\n// {\n//   updated: { timestamp: '...', timestampUNIX: ... },\n//   tag: 'my-operation'\n// }\n```\n\n### Local Storage\n\nPersistent JSON storage using lowdb:\n\n```javascript\nconst storage = Manager.storage({\n  name: 'myStorage',     // Storage name (default: 'main')\n  temporary: false,      // Use OS temp directory (default: false)\n  clear: true,           // Clear on dev startup (default: true)\n  log: false,            // Enable logging\n});\n\n// lowdb API\nstorage.set('key', 'value').write();\nconst value = storage.get('key').value();\nstorage.set('nested.path', { data: true }).write();\n```\n\n## Authentication\n\nBEM supports multiple authentication methods (checked in order):\n\n1. **Bearer Token (JWT)**\n   ```\n   Authorization: Bearer \u003cfirebase-id-token\u003e\n   ```\n\n2. **API Key**\n   ```javascript\n   { apiKey: 'user-private-key' }\n   // or\n   { authenticationToken: 'user-private-key' }\n   ```\n\n3. **Backend Manager Key** (Admin access)\n   ```javascript\n   { backendManagerKey: 'your-backend-manager-key' }\n   ```\n\n4. **Session Cookie**\n   ```\n   Cookie: __session=\u003cfirebase-id-token\u003e\n   ```\n\n**Authenticated User Object:**\n\n```javascript\nconst user = await assistant.authenticate();\n\n{\n  authenticated: true,\n  auth: { uid: 'abc123', email: 'user@example.com' },\n  roles: { admin: false, betaTester: false, developer: false },\n  subscription: { product: { id: 'basic', name: 'Basic' }, status: 'active', ... },\n  api: { clientId: '...', privateKey: '...' },\n  // ... other user properties\n}\n```\n\n## CLI Commands\n\nBEM includes a CLI for development and deployment:\n\n```bash\n# Install globally or use npx\nnpm install -g backend-manager\n# or\nnpx backend-manager \u003ccommand\u003e\n```\n\n| Command | Description |\n|---------|-------------|\n| `bem setup` | Run Firebase project setup and validation |\n| `bem serve` | Start local Firebase emulator |\n| `bem deploy` | Deploy functions to Firebase |\n| `bem test [paths...]` | Run integration tests |\n| `bem emulator` | Start Firebase emulator (keep-alive mode) |\n| `bem stripe` | Start Stripe CLI webhook forwarding to local server |\n| `bem version`, `bem v` | Show BEM version |\n| `bem clear` | Clear cache and temp files |\n| `bem install`, `bem i` | Install BEM (local or production) |\n| `bem clean:npm` | Clean and reinstall npm modules |\n| `bem firestore:indexes:get` | Get Firestore indexes |\n| `bem cwd` | Show current working directory |\n| `bem firestore:get \u003cpath\u003e` | Read a Firestore document |\n| `bem firestore:set \u003cpath\u003e '\u003cjson\u003e'` | Write/merge a Firestore document |\n| `bem firestore:query \u003ccollection\u003e` | Query a Firestore collection |\n| `bem firestore:delete \u003cpath\u003e` | Delete a Firestore document |\n| `bem auth:get \u003cuid-or-email\u003e` | Get an Auth user by UID or email |\n| `bem auth:list` | List Auth users |\n| `bem auth:delete \u003cuid-or-email\u003e` | Delete an Auth user |\n| `bem auth:set-claims \u003cuid-or-email\u003e '\u003cjson\u003e'` | Set custom claims on an Auth user |\n| `bem logs:read` | Fetch Cloud Function logs from Google Cloud Logging |\n| `bem logs:tail` | Stream live Cloud Function logs |\n\nAll Firestore and Auth commands support `--emulator` to target the local emulator, `--force` to skip confirmation, and `--raw` for compact JSON output.\n\nLogs commands support `--fn \u003cname\u003e` (function name filter), `--severity \u003clevel\u003e`, `--since \u003cduration\u003e` (read only), `--limit \u003cn\u003e` (read only), and `--raw`. Requires `gcloud` CLI installed and authenticated.\n\n## Environment Variables\n\nSet these in your `functions/.env` file:\n\n| Variable | Description |\n|----------|-------------|\n| `BACKEND_MANAGER_KEY` | Admin authentication key |\n| `STRIPE_SECRET_KEY` | Stripe secret key (enables auto webhook forwarding in `serve`/`emulator`) |\n\n## Response Headers\n\nBEM attaches metadata to responses:\n\n```\nbm-properties: {\"code\":200,\"tag\":\"functionName/executionId\",\"usage\":{...},\"schema\":{...}}\n```\n\n## Testing\n\nBEM includes an integration test framework that runs against the Firebase emulator.\n\n### Running Tests\n\n```bash\n# Option 1: Two terminals (recommended for development)\nnpx mgr emulator  # Terminal 1 - keeps emulator running\nnpx mgr test      # Terminal 2 - runs tests\n\n# Option 2: Single command (auto-starts emulator, shuts down after)\nnpx mgr test\n```\n\n### Extended Mode (real APIs)\n\nSet `TEST_EXTENDED_MODE=true` on the **test command** to opt into real external API calls (SendGrid, Beehiiv, Stripe webhook handlers, marketing libraries). The flag flows automatically to the running emulator via `\u003cprojectRoot\u003e/.temp/test-mode.json` — no need to set it on the emulator too:\n\n```bash\n# Terminal 1 — start once, no flag needed\nnpx mgr emulator\n\n# Terminal 2 — toggle freely between runs\nTEST_EXTENDED_MODE=true npx mgr test ...   # extended mode\nnpx mgr test ...                            # normal mode (next run flips back)\n```\n\nSee [docs/testing.md](docs/testing.md#extended-mode-test_extended_mode) for the full mechanism.\n\n### Filtering Tests\n\n```bash\nnpx mgr test rules/             # Run rules tests (both BEM and project)\nnpx mgr test bem:rules/         # Only BEM's rules tests\nnpx mgr test project:rules/     # Only project's rules tests\nnpx mgr test user/ admin/       # Multiple paths\n```\n\n### Log Files\n\nBEM CLI commands automatically save output to log files in the project's `functions/` directory (alongside firebase-tools' own `*-debug.log` files so everything is grep-able from one place):\n- **`functions/serve.log`** — Output from `npx mgr serve`\n- **`functions/emulator.log`** — Full emulator + Cloud Functions output (`npx mgr emulator`)\n- **`functions/test.log`** — Test runner output (`npx mgr test`, when running against an existing emulator)\n- **`functions/logs.log`** — Cloud Function logs (`npx mgr logs:read` or `npx mgr logs:tail`)\n\nLogs are overwritten on each run and gitignored via `*.log`. Use them to debug failing tests or review function output. Transient internal artifacts (reset sentinels, watch trigger, `test-mode.json`) live separately in `\u003cprojectDir\u003e/.temp/`.\n\n### Test Locations\n\n- **BEM core tests:** `test/`\n- **Project tests:** `functions/test/bem/`\n\nUse `bem:` or `project:` prefix to filter by source.\n\n### Writing Tests\n\n**Suite** - Sequential tests with shared state (stops on first failure):\n\n```javascript\n// test/functions/user/sign-up.js\nmodule.exports = {\n  description: 'User signup flow with affiliate tracking',\n  type: 'suite',\n  tests: [\n    {\n      name: 'verify-referrer-exists',\n      async run({ firestore, assert, state, accounts }) {\n        state.referrerUid = accounts.referrer.uid;\n        const doc = await firestore.get(`users/${state.referrerUid}`);\n        assert.ok(doc, 'Referrer should exist');\n      },\n    },\n    {\n      name: 'call-user-signup-with-affiliate',\n      async run({ http, assert, state }) {\n        const response = await http.as('referred').command('user:sign-up', {\n          attribution: { affiliate: { code: 'TESTREF' } },\n        });\n        assert.isSuccess(response);\n      },\n    },\n  ],\n};\n```\n\n**Group** - Independent tests (continues even if one fails):\n\n```javascript\n// test/functions/admin/firestore-write.js\nmodule.exports = {\n  description: 'Admin Firestore write operation',\n  type: 'group',\n  tests: [\n    {\n      name: 'admin-auth-succeeds',\n      auth: 'admin',\n      async run({ http, assert }) {\n        const response = await http.command('admin:firestore-write', {\n          path: '_test/doc',\n          document: { test: 'value' },\n        });\n        assert.isSuccess(response);\n      },\n    },\n    {\n      name: 'unauthenticated-rejected',\n      auth: 'none',\n      async run({ http, assert }) {\n        const response = await http.command('admin:firestore-write', {\n          path: '_test/doc',\n          document: { test: 'value' },\n        });\n        assert.isError(response, 401);\n      },\n    },\n  ],\n};\n```\n\n**Auth levels:** `none`, `user`/`basic`, `admin`, `premium-active`, `premium-expired`\n\nSee `CLAUDE.md` for complete test API documentation.\n\n## Subscription System\n\nBEM includes a built-in payment/subscription system with Stripe and PayPal integration.\n\n### Subscription Statuses\n\n| Status | Meaning | User can delete account? |\n|--------|---------|--------------------------|\n| `active` | Subscription is current and valid (includes trialing) | No (unless `product.id === 'basic'`) |\n| `suspended` | Payment failed (Stripe: `past_due`, `unpaid`) | No |\n| `cancelled` | Subscription terminated (Stripe: `canceled`, `incomplete`, `incomplete_expired`) | Yes |\n\n### Stripe Status Mapping\n\n| Stripe Status | `subscription.status` | Notes |\n|---|---|---|\n| `active` | `active` | Normal active subscription |\n| `trialing` | `active` | `trial.claimed = true` |\n| `past_due` | `suspended` | Payment failed, retrying |\n| `unpaid` | `suspended` | Payment failed |\n| `canceled` | `cancelled` | Subscription terminated |\n| `incomplete` | `cancelled` | Never completed initial payment |\n| `incomplete_expired` | `cancelled` | Expired before completion |\n| `active` + `cancel_at_period_end` | `active` | `cancellation.pending = true` |\n\n### PayPal Status Mapping\n\n| PayPal Status | `subscription.status` | Notes |\n|---|---|---|\n| `ACTIVE` | `active` | Normal active subscription |\n| `SUSPENDED` | `suspended` | Payment failed or manually suspended |\n| `CANCELLED` | `cancelled` | Subscription terminated |\n| `EXPIRED` | `cancelled` | Billing cycles completed |\n\n### Product Configuration\n\nProducts are defined in `config.payment.products` with flat prices and per-processor IDs:\n\n```javascript\npayment: {\n  products: [\n    { id: 'basic', name: 'Basic', type: 'subscription', limits: { requests: 10 } },\n    {\n      id: 'plus', name: 'Plus', type: 'subscription',\n      limits: { requests: 100 }, trial: { days: 14 },\n      prices: { monthly: 28, annually: 276 },  // also supports 'weekly' and 'daily'\n      stripe: { productId: 'prod_xxx' },\n      paypal: { productId: 'PROD-abc123' },\n    },\n    {\n      id: 'boost', name: 'Boost Pack', type: 'one-time',\n      prices: { once: 9.99 },\n      stripe: { productId: 'prod_yyy' },\n    },\n  ],\n}\n```\n\n### Unified Subscription Object\n\nThe same subscription shape is stored in `users/{uid}.subscription` and `payments-orders/{orderId}.subscription`:\n\n```javascript\nsubscription: {\n  product: {\n    id: 'basic',                   // product ID from config ('basic', 'premium', etc.)\n    name: 'Basic',                 // display name from config\n  },\n  status: 'active',                // 'active' | 'suspended' | 'cancelled'\n  expires: { timestamp, timestampUNIX },\n  trial: {\n    claimed: false,                // has user EVER used a trial\n    expires: { timestamp, timestampUNIX },\n  },\n  cancellation: {\n    pending: false,                // true = cancel at period end\n    date: { timestamp, timestampUNIX },\n  },\n  payment: {\n    processor: null,               // 'stripe' | 'paypal' | etc.\n    resourceId: null,              // provider subscription ID (e.g., 'sub_xxx')\n    frequency: null,               // 'monthly' | 'annually' | 'weekly' | 'daily'\n    startDate: { timestamp, timestampUNIX },\n    updatedBy: {\n      event: { name: null, id: null },\n      date: { timestamp, timestampUNIX },\n    },\n  },\n}\n```\n\n### Access Check Patterns\n\n```javascript\n// Is premium (paid)?\nuser.subscription.status === 'active' \u0026\u0026 user.subscription.product.id !== 'basic'\n\n// Is on trial?\nuser.subscription.trial.claimed \u0026\u0026 user.subscription.status === 'active'\n\n// Has pending cancellation?\nuser.subscription.cancellation.pending === true\n\n// Payment failed?\nuser.subscription.status === 'suspended'\n```\n\n### resolveSubscription(account)\n\nStatic method on the `User` helper that derives calculated subscription fields. Returns only fields that require derivation logic — raw data lives on the account object directly.\n\n```javascript\nconst User = require('backend-manager/src/manager/helpers/user');\n\nconst resolved = User.resolveSubscription(account);\n// Returns: { plan, active, trialing, cancelling }\n```\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `plan` | `string` | Effective plan ID right now (`'basic'` if cancelled/suspended) |\n| `active` | `boolean` | Has paid access (product is not `'basic'` and status is `'active'`) |\n| `trialing` | `boolean` | In active trial (status `'active'` + claimed + unexpired) |\n| `cancelling` | `boolean` | Cancellation pending (status `'active'` + `cancellation.pending`) |\n\nThe same function exists as `auth.resolveSubscription(account)` in [web-manager](https://github.com/itw-creative-works/web-manager) with identical logic and return shape.\n\n## Final Words\n\nIf you are still having difficulty, we would love for you to post a question to [the Backend Manager issues page](https://github.com/itw-creative-works/backend-manager/issues). 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!)\n\n## Projects Using this Library\n\n[Somiibo](https://somiibo.com/): A Social Media Bot with an open-source module library.\n[JekyllUp](https://jekyllup.com/): A website devoted to sharing the best Jekyll themes.\n[Slapform](https://slapform.com/): A backend processor for your HTML forms on static sites.\n[SoundGrail Music App](https://app.soundgrail.com/): A resource for producers, musicians, and DJs.\n[Hammock Report](https://hammockreport.com/): An API for exploring and listing backyard products.\n\nAsk us to have your project listed! :)\n\n## License\n\nISC\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fitw-creative-works%2Fbackend-manager","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fitw-creative-works%2Fbackend-manager","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fitw-creative-works%2Fbackend-manager/lists"}