# Creating Routes, API Commands, Events, Cron Jobs

Recipes for building consumer-side routes plus BEM-side API commands, event handlers, and cron jobs. See also [docs/schemas.md](schemas.md) for schema definitions, [docs/auth-hooks.md](auth-hooks.md) for auth lifecycle hooks, and [docs/common-operations.md](common-operations.md) for inside-the-handler patterns.

## New API Command

Create `src/manager/functions/core/actions/api/{category}/{action}.js`:

```javascript
function Module() {}

Module.prototype.main = function () {
  const self = this;
  const Manager = self.Manager;
  const Api = self.Api;
  const assistant = self.assistant;
  const payload = self.payload;

  return new Promise(async function(resolve, reject) {
    // Validate input
    if (!payload.data.payload.requiredField) {
      return reject(assistant.errorify('Missing required field', { code: 400 }));
    }

    // Business logic here
    const result = { success: true };

    // Log and return
    assistant.log('Action completed', result);
    return resolve({ data: result });
  });
};

module.exports = Module;
```

## New Route (Consumer Project)

Routes live at `functions/routes/{path}/{method}.js` — BEM routes requests to the matching method file (`get.js` / `post.js` / `put.js` / `delete.js`), falling back to `index.js` if no method-specific file exists.

A route exports an **async function receiving a context object** (built by the middleware — see `src/manager/helpers/middleware.js`):

```javascript
/**
 * POST /items - Create a new item
 */
module.exports = async ({ Manager, assistant, analytics, usage, user, settings, libraries, utilities }) => {
  if (!user.authenticated) {
    return assistant.respond('Authentication required', { code: 401 });
  }

  const { admin } = libraries;
  const firestore = admin.firestore();

  // Track usage
  await usage.validate('requests');
  usage.increment('requests');
  await usage.update();

  // settings strings are whitespace-trimmed by middleware; HTML is preserved.
  // Call utilities.sanitize() at the HTML-insertion site, or opt the whole
  // route in to middleware HTML strip via { sanitize: true } on .run().
  const id = settings.id; // auto-generated by the schema (see schemas.md)

  await firestore.doc(`items/${id}`).set({ id, owner: user.auth.uid, ...settings });

  return assistant.respond({ id });
};
```

Context object fields: `Manager`, `assistant`, `user` (from `assistant.getUser()`), `usage`, `settings` (schema-resolved), `analytics`, `libraries`, `utilities`.

### CRUD method files

Every resource endpoint follows proper CRUD with **method-specific files** (plural-noun route names — `items`, not `item`):

| Method | File | Purpose | Path |
|--------|------|---------|------|
| GET | `get.js` | List all or fetch one | `/items` or `/items/{id}` |
| POST | `post.js` | Create new | `/items` |
| PUT | `put.js` | Update existing | `/items/{id}` |
| DELETE | `delete.js` | Delete existing | `/items/{id}` |

**GET routes exist for external API consumers.** The dashboard/frontend reads data directly from Firestore (faster, cheaper, no cold starts). POST/PUT/DELETE still go through Cloud Functions for server-side validation, usage tracking, and analytics.

**PUT and DELETE must verify ownership** before mutating:

```javascript
const existing = docSnap.data();

if (existing.owner !== user.auth.uid) {
  return assistant.respond('Not authorized', { code: 403 });
}
```

Immutable fields (`id`, `owner`, `stats`, `metadata.created`) should NOT be editable via PUT. ID generation (POST) and ID-from-path extraction (GET/PUT/DELETE) happen in the **schema**, not the route — see [schemas.md](schemas.md).

### Key route patterns

- Short-circuit returns for auth/validation checks
- `settings` contains the parsed + validated request data (from schemas)
- `assistant.respond()` for ALL responses (success and error)
- `admin.firestore().doc('collection/id')` shorthand for Firestore access ([firestore.md](firestore.md))
- Timestamps under `metadata.{created,updated}` ([firestore.md](firestore.md#document-metadata))

### Functions entry point (`functions/index.js`)

```javascript
const Manager = (new (require('backend-manager'))).init(exports, {
  setupFunctionsIdentity: false,
});
const { functions } = Manager.libraries;

/**
 * @route /items
 *
 * @method GET    /items            - List all items
 * @method GET    /items/:itemId    - Get single item
 * @method POST   /items            - Create a new item
 * @method PUT    /items/:itemId    - Update an item
 * @method DELETE /items/:itemId    - Delete an item
 */
exports.items = functions
.runWith({ memory: '256MB', timeoutSeconds: 120 })
.https.onRequest((req, res) => Manager.Middleware(req, res).run('items'));
```

The schema defaults to the route name (`.run('items')` loads `functions/schemas/items/`); pass `{ schema: 'custom' }` only when the schema path differs.

For operations that don't fit CRUD (e.g. `/items/:itemId/export`), add an **action sub-path** handled within the same Cloud Function (parsed from the request path) or as a separate function if resource needs differ significantly.

### firebase.json routing

Make routes public with rewrites. Use the bracket syntax so sub-paths like `/items/{id}` route correctly:

```json
{
  "hosting": {
    "rewrites": [
      { "source": "{/items,/items/**}", "function": "items" }
    ]
  }
}
```

**CRITICAL**: Without the `/**` wildcard, requests to `/items/{id}` won't reach the function.

**CRITICAL: Rewrite order matters — first match wins.** When multiple functions share a path prefix, the most specific routes MUST come first and the catch-all last, otherwise it swallows all sub-routes:

```json
{
  "hosting": {
    "rewrites": [
      { "source": "{/agents/*/chat,/agents/*/chat/**}", "function": "agentsChat" },
      { "source": "/agents/*/conversations/**", "function": "agentsConversations" },
      { "source": "{/agents,/agents/**}", "function": "agents" }
    ]
  }
}
```

Order: most specific → least specific → catch-all.

## New Event Handler

Create `src/manager/functions/core/events/{type}/{event}.js`:

```javascript
function Module() {}

Module.prototype.init = function (Manager, payload) {
  const self = this;
  self.Manager = Manager;
  self.assistant = Manager.Assistant();
  self.libraries = Manager.libraries;
  self.user = payload.user;
  self.context = payload.context;
  return self;
};

Module.prototype.main = function () {
  const self = this;
  const Manager = self.Manager;
  const assistant = self.assistant;

  return new Promise(async function(resolve, reject) {
    const { admin } = self.libraries;

    assistant.log('Event triggered', self.user);

    // Event logic here

    return resolve(self);
  });
};

module.exports = Module;
```

## New Cron Job (Consumer Project)

Create `hooks/cron/daily/{job}.js`:

```javascript
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 daily job...');

    // Job logic here

    return resolve();
  });
};

module.exports = Job;
```
