--- 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]].