---
title: RequestTemplate
tags: [data-model, mongoose, postgres]
aliases: [Template, Request Template, IRequestTemplate]
---

# RequestTemplate

> **Last updated:** 2026-06-01 — Postgres schema and backfill surface documented.

A reusable template authored by a seller. When a buyer visits the template's `shareableLink`, the front-end pre-fills a new [[PurchaseRequest]] with the template's category, urgency, specs, seller-selected delivery mode, payment rail allowlist, and an optional default seller `proposal`. The schema mirrors `PurchaseRequest` for fast cloning, plus template-specific bookkeeping (`isActive`, `usageCount`, `maxUsage`, `expiresAt`).

> [!note] Source
> `backend/src/models/RequestTemplate.ts:83` — Mongoose schema definition
> `backend/src/models/RequestTemplate.ts:335` — model export
> `backend/src/db/schema/requestTemplate.ts:35` — Drizzle table definition
> `backend/src/db/backfill/backfill-requestTemplates.ts:1` — Mongo → Postgres backfill

## Schema

| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `sellerId` | ObjectId → [[User]] | yes | — | — | yes (compound) | Template author. |
| `title` | String | yes | — | trim, maxlength 200 | — | Headline. |
| `description` | String | yes | — | trim, maxlength 2000 | — | Description. |
| `categoryId` | ObjectId → [[Category]] | yes | — | — | yes (compound) | Category. |
| `productType` | String | no | `physical_product` | enum: `physical_product` / `digital_product` / `service` / `consultation` | yes (compound) | Fulfilment type. |
| `productLink` | String | no | — | URL regex | — | Reference URL. |
| `size` | String | no | — | trim, maxlength 100 | — | Size. |
| `color` | String | no | — | trim, maxlength 100 | — | Color. |
| `brand` | String | no | — | trim, maxlength 100 | — | Brand. |
| `quantity` | Number | no | `1` | min 1 | — | Default unit count. |
| `budget.min` | Number | no | — | min 0 | — | Lower bound. |
| `budget.max` | Number | no | — | min 0 | — | Upper bound. |
| `budget.currency` | String | no | `USDT` | enum: `USD` / `EUR` / `IRR` / `USDT` / `USDC` | — | Currency. Shared with [[PurchaseRequest]] so a template can be converted without enum drift. |
| `urgency` | String | no | `medium` | enum: `low` / `medium` / `high` / `urgent` | — | Urgency. |
| `tags[]` | String[] | no | — | trim | — | Tags. |
| `specifications[].key` | String | yes | — | trim | — | Spec key. |
| `specifications[].value` | String | yes | — | trim | — | Spec value. |
| `specifications[].label` | String | no | — | trim | — | Human label. |
| `deliveryInfo.deliveryType` | String | no | `physical` | enum: `physical` / `online` | — | Seller-selected delivery channel. Buyers cannot override this at checkout. |
| `deliveryInfo.notes` | String | no | — | — | — | Seller notes about delivery. |
| `deliveryInfo.email` | String | no | — | email regex when non-empty | — | Legacy/optional field. Template checkout now asks the buyer for a receiving email when `deliveryType === "online"`. |
| `paymentConfig.useShopDefault` | Boolean | no | `true` | — | — | When `false`, the template's own chain/token allowlist overrides [[ShopSettings]]. New template UI defaults this to `false` so sellers choose rails explicitly. |
| `paymentConfig.allowedChains[]` | Number[] | no | `[1, 56]` | must contain at least one positive chain id | — | Chain ids accepted for this template, e.g. `1` Ethereum, `56` BSC. Empty arrays are rejected. |
| `paymentConfig.allowedTokens[]` | String[] | no | `["USDC", "USDT"]` | must contain at least one non-empty token symbol | — | Settlement tokens accepted for this template. Empty arrays are rejected. |
| `serviceInfo.duration` | Number | no | — | min 0.5 | — | Hours. |
| `serviceInfo.sessionType` | String | no | — | enum: `online` / `in_person` / `hybrid` | — | Session type. |
| `serviceInfo.location` | String | no | — | trim, maxlength 200 | — | Location. |
| `serviceInfo.requirements[]` | String[] | no | — | trim | — | Pre-requisites. |
| `proposal.title` | String | no | — | trim, maxlength 200 | — | Default offer title. |
| `proposal.price` | Number | no | — | min 0.01 | — | Default offer price. |
| `proposal.deliveryTime` | Number | no | — | min 1, max 365 | — | Default ETA in days. |
| `proposal.description` | String | no | — | trim, maxlength 1000 | — | Default offer description. |
| `attachments[]` | String[] | no | — | — | — | File URLs. |
| `images[]` | String[] | no | — | trim | — | Image URLs. |
| `metadata.source` | String | no | `manual` | enum: `manual` / `template` / `api` | — | Origin. |
| `metadata.templateId` | String | no | — | trim | — | Originating template id. |
| `metadata.version` | String | no | — | trim | — | Schema version. |
| `isActive` | Boolean | no | `true` | — | yes (single + compound) | Active flag. |
| `shareableLink` | String | yes | — | trim | unique (+ compound) | Public link slug. |
| `usageCount` | Number | no | `0` | min 0 | — | Number of times used. |
| `maxUsage` | Number | no | `null` | min 1 | — | Optional cap. |
| `expiresAt` | Date | no | `null` | — | yes | Optional expiry. |
| `createdAt` | Date | auto | — | — | yes (desc) | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |

## Virtuals

None defined.

## Indexes

Defined at `backend/src/models/RequestTemplate.ts:283-293`:

- `{ categoryId: 1 }`
- `{ productType: 1 }`
- `{ isActive: 1 }`
- `{ createdAt: -1 }`
- `{ expiresAt: 1 }`
- `{ sellerId: 1, isActive: 1 }`
- `{ shareableLink: 1, isActive: 1 }`
- `{ productType: 1, isActive: 1 }`
- `{ categoryId: 1, productType: 1 }`

`shareableLink` and `sellerId` already get indexes from `unique: true` / field-level conventions (see source comment at line 282).

Postgres migration `0010_request_templates.sql` creates `request_templates` with:

- `request_templates_legacy_object_id_uq`: idempotent Mongo bridge for backfill.
- `request_templates_shareable_link_uq`: public slug uniqueness.
- FK columns `seller_id → users.id` and `category_id → categories.id`.
- Matching single/compound indexes for seller, category, product type, active state, expiry, and public slug lookups.
- JSONB `specifications` and scalar/array columns for delivery, service, proposal, payment rails, images, and attachments.

Runtime service wiring is not cut over yet; `RequestTemplateService` still uses Mongoose directly.

## Pre/Post Hooks

None declared.

## Instance Methods

None defined.

## Static Methods

None defined.

## Relationships

- **References**: [[User]] (`sellerId`), [[Category]] (`categoryId`).
- **Referenced by**: [[PurchaseRequest]] (`metadata.templateId` as string), [[Review]] (`subjectId` when `subjectType === 'template'`).

## Checkout Semantics

- The seller chooses `deliveryInfo.deliveryType` on the template. The buyer checkout step only collects the required fulfillment details: a physical address for `physical`, a receiving email for `online`, and both when a cart mixes physical and online templates.
- `batch-convert` copies the seller's delivery mode into each generated [[PurchaseRequest]] and overlays the buyer-supplied billing/email details.
- Payment checkout resolves allowed rails through `paymentConfig`: template override first, then [[ShopSettings]], then the global supported default. A template with an explicit empty chain or token list is invalid.

## State Transitions

```mermaid
stateDiagram-v2
    [*] --> active : created
    active --> inactive : seller toggles off
    inactive --> active : seller toggles on
    active --> expired : expiresAt passed
    active --> capped : usageCount == maxUsage
    expired --> [*]
    capped --> [*]
```

> [!note] Soft state
> Only `isActive` is persisted directly. `expired` and `capped` are derived at query time using `expiresAt` and `usageCount`.

## Common Queries

```ts
// Seller's active templates
RequestTemplate.find({ sellerId, isActive: true }).sort({ createdAt: -1 });

// Public template by slug
RequestTemplate.findOne({ shareableLink: slug, isActive: true });

// Bump usage atomically
RequestTemplate.findOneAndUpdate(
  { _id, isActive: true, $or: [{ maxUsage: null }, { $expr: { $lt: ['$usageCount', '$maxUsage'] } }] },
  { $inc: { usageCount: 1 } },
  { new: true }
);

// Cleanup expired
RequestTemplate.find({ expiresAt: { $lt: new Date() }, isActive: true });
```

Related: [[PurchaseRequest]], [[User]], [[Category]], [[Review]].
