Settings Module — Vendor surface
Vendor self-service for reading and writing the active vendor's own settings. Settings are organized by scope (admin for staff-facing config like shipping/tax/payouts, store for…
Vendor self-service for reading and writing the active vendor's own settings. Settings are organized by scope (admin for staff-facing config like shipping/tax/payouts, store for the vendor's storefront customization), then by group (e.g. shipping, branding, contact), then by key. The module is registry-driven: every setting is declared as a SettingDefinition (group + key + zod type + flags like forAdmin) and the vendor surface reads/writes against that registry — there are no free-form key-value writes.
Source:
api-modules/settings/src/controllers/vendor-settings.controller.ts.
Conventions
Authentication
All endpoints require a Better-Auth bearer session with an active vendor.
Authorization: Bearer <session-token>The active vendor is resolved via resolveActiveVendorId(session). Sessions missing an active vendor are rejected with 403 Forbidden.
There is no platform RBAC permission on this surface — vendor users are not platform staff, and the active-vendor scoping is the only access gate.
Tenant scoping
Every read and write scopes to the active vendor's id. There is no way to read or write another vendor's settings from this surface — platform staff use /admin/vendors/:vendorId/settings/... (separate controller, platformVendorSetting:* permissions).
Admin-only keys
Some keys in the vendor registry are flagged forAdmin: true — the vendor can read them but cannot write them. The list of such keys per group is returned in readOnlyKeys on every list response so the UI can render those fields disabled. Writes whose payload includes a forAdmin: true key reject the whole call with 403 (no silent partial application).
Response envelope
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* optional */ }
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR (unknown group/key, value fails registry zod, empty body) |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN (no active vendor, or payload includes a forAdmin: true key) |
| 404 | NOT_FOUND |
| 500 | INTERNAL_SERVER_ERROR |
Domain types
VendorScopeSettingsResponse
type VendorScopeSettingsResponse = {
values: Record<string, Record<string, unknown>>; // group → key → value
readOnlyKeys: Record<string, string[]>; // group → forAdmin keys
};VendorGroupSettingsResponse
type VendorGroupSettingsResponse = {
values: Record<string, unknown>; // key → value
readOnlyKeys: string[]; // forAdmin keys in this group
};UpdateVendorSettingsInput
Bulk update payload, nested by group:
{
"shipping": {
"flat_rate_subunit": 4900,
"free_above_subunit": 99900
},
"tax": {
"provider_id": "flat"
}
}Top-level zod is permissive (Record<string, Record<string, unknown>>); the service validates each (group, key, value) triple against the vendor registry. The body must contain at least one group.
Known vendor groups
| Scope | Group | Owner / purpose |
|---|---|---|
admin | shipping | flat_rate_subunit, free_above_subunit, enabled_providers, provider configs (clickpost.*) |
admin | tax | Tax provider id + provider config |
admin | payouts | Bank account info, payout_hold flag (admin-only) |
admin | notifications | Vendor-side notification preferences |
store | branding, contact, etc. | Vendor's storefront customization |
Admin scope
Base path: /vendor/settings/admin. Staff-facing config (the vendor's shipping rules, tax provider choice, notification preferences, …).
GET /vendor/settings/admin — Every admin-scope group
Response 200 — VendorScopeSettingsResponse for the active vendor.
{
"data": {
"values": {
"shipping": {
"flat_rate_subunit": 4900,
"free_above_subunit": 99900,
"enabled_providers": ["clickpost", "self-handled"]
},
"tax": {
"provider_id": "flat"
}
},
"readOnlyKeys": {
"shipping": [],
"payouts": ["bank_account_id", "payout_hold"]
}
},
"message": "Success",
"statusCode": 200
}GET /vendor/settings/admin/:group — One admin-scope group
Path params
| Name | Notes |
|---|---|
group | Group identifier (e.g. shipping, tax) |
Response 200 — VendorGroupSettingsResponse.
{
"data": {
"values": { "flat_rate_subunit": 4900, "free_above_subunit": 99900 },
"readOnlyKeys": []
}
}Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown group in this scope |
PATCH /vendor/settings/admin — Bulk update
Same body shape as the platform-admin endpoints. Validates each (group, key, value) against the registry. Keys flagged forAdmin: true reject with 403 — the whole call rolls back rather than silently partial-applying.
Body — UpdateVendorSettingsInput
{
"shipping": {
"flat_rate_subunit": 4900,
"free_above_subunit": 99900,
"enabled_providers": ["clickpost", "self-handled"]
}
}Response 200 — updated VendorScopeSettingsResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Unknown group/key, value fails registry zod, or empty body |
| 403 | FORBIDDEN | No active vendor, or payload includes a forAdmin: true key (use the platform-admin override surface) |
Store scope
Base path: /vendor/settings/store. Storefront-facing config (the vendor's branding, contact details, etc.).
GET /vendor/settings/store — Every store-scope group
Response 200 — VendorScopeSettingsResponse.
GET /vendor/settings/store/:group — One store-scope group
Response 200 — VendorGroupSettingsResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown group in this scope |
PATCH /vendor/settings/store — Bulk update
Same shape and rules as PATCH /vendor/settings/admin — forAdmin: true keys reject the whole call with 403.
Response 200 — updated VendorScopeSettingsResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Unknown group/key, value fails registry zod, or empty body |
| 403 | FORBIDDEN | No active vendor, or payload includes a forAdmin: true key |
Registry discovery
A read-only introspection surface that exposes the vendor-overridable settings registry. The vendor UI consumes this to render its settings page directly from the registry — adding a new vendor setting (or group) takes no frontend deploy.
Source:
api-modules/settings/src/controllers/vendor-settings-registry.controller.ts.
Authentication
Same as the rest of /vendor/*: a Better-Auth session with an active vendor. Vendor id is resolved server-side via resolveActiveVendorId(session) — never from the client. Sessions missing an active vendor are rejected with 403 Forbidden. No platform RBAC permission applies.
Surfaces returned
The vendor discovery surface returns:
admin— every vendor-overridable admin sub-scope entry for the requested groups (e.g.shipping.flat_rate_subunit,payouts.commission_rate).store— every vendor-overridable store sub-scope entry (currently empty — placeholder for future storefront-facing vendor keys).
Entries flagged forAdmin: true in the registry stay in the payload but carry isAdminOnly: true on the descriptor so the UI renders them as read-only — matching the existing /vendor/settings semantics, which returns the same keys and flags them via readOnlyKeys. Writes to those keys still reject with 403 on the existing PATCH surface.
Cascade hydration
currentValue for every entry is resolved through the vendor cascade:
vendor_setting row for (vendorId, scope, group, key)
↓ (if absent)
platform setting row at (scope, group, key)
↓ (if absent)
registry defaultSo a vendor that has never written its own shipping.flat_rate_subunit sees the platform default; a vendor that has written one sees its own value.
Domain types
VendorRegistryFieldsResponse
type VendorRegistryFieldsResponse = {
admin: Record<string, FieldDescriptor[]>; // group → fields (vendor-overridable admin sub-scope)
store: Record<string, FieldDescriptor[]>; // group → fields (vendor-overridable store sub-scope)
};FieldDescriptor is the same shape as the admin discovery surface — { key, description, default, currentValue, isAdminOnly?, isPublic?, schema }. See admin/settings.md for the field-by-field breakdown.
The response is flatter than the admin one (no vendor.* nesting) because there is no platform-scope surface on the vendor side.
GET /vendor/settings/registry/groups — Group catalog
Returns the flat list of vendor-overridable groups across both sub-scopes. Group keys may collide between admin and store sub-scopes, so each entry carries its scope. settingCount is the total entries in the group (including forAdmin keys) — the UI renders those as read-only via the field-level flag, not by hiding them.
Response 200
{
"data": {
"groups": [
{ "scope": "admin", "key": "shipping", "description": "Per-vendor shipping configuration …", "settingCount": 8 },
{ "scope": "admin", "key": "tax", "description": "Per-vendor tax provider selection …", "settingCount": 2 },
{ "scope": "admin", "key": "returns", "description": "Per-vendor return-window override …", "settingCount": 3 },
{ "scope": "admin", "key": "payouts", "description": "Per-vendor payout overrides …", "settingCount": 3 }
]
},
"message": "Success",
"statusCode": 200
}Errors
| Status | Code | When |
|---|---|---|
| 403 | FORBIDDEN | No active vendor on session |
GET /vendor/settings/registry/fields — Field descriptors
For the requested set of group keys, returns render-ready field descriptors scoped to the active vendor's cascade.
Query
| Name | Notes |
|---|---|
groups | Comma-separated group keys (shipping,payouts). At least one required. |
No vendorId parameter — the active vendor on the session is the only target.
Response 200
{
"data": {
"admin": {
"shipping": [
{
"key": "flat_rate_subunit",
"description": "Per-vendor flat shipping rate (subunits) charged to the customer …",
"default": 0,
"currentValue": 4900,
"schema": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }
},
{
"key": "clickpost.api_key",
"description": "Vendor's ClickPost API key for outbound calls.",
"default": null,
"currentValue": null,
"schema": { "type": "string", "minLength": 1, "maxLength": 500 }
}
],
"payouts": [
{
"key": "commission_rate",
"description": "Per-vendor commission rate override (basis points). …",
"default": null,
"currentValue": null,
"schema": { "anyOf": [{ "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" }] }
},
{
"key": "payout_hold",
"description": "Admin-flipped manually to freeze disbursements …",
"default": false,
"currentValue": false,
"isAdminOnly": true,
"schema": { "type": "boolean" }
}
]
},
"store": {}
},
"message": "Success",
"statusCode": 200
}Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | groups missing or empty |
| 403 | FORBIDDEN | No active vendor on session |
Unknown group keys are silently dropped — the response returns 200 with empty buckets so the UI tolerates tab-name lag.
Related modules
shipping— readsadmin.shipping.*(rate, free threshold, enabled providers). Seeshipping.md.shipping-clickpost— reads its own ClickPost credentials block fromadmin.shipping.clickpost.*(typically via the dedicatedvendor-clickpost-configcontroller — seeshipping-clickpost.md).tax-flat— readsadmin.tax.*. Seetax.md.notifications— reads channel toggles fromadmin.notifications.*. Seenotifications.md.admin-rbac—platformVendorSetting:read/:updategate the override surface (/admin/vendors/:vendorId/settings/...) that platform staff use to read/write any vendor's settings, includingforAdmin: truekeys. The admin-side discovery counterparts of/vendor/settings/registry/*are:GET /admin/settings/registry/*for the global admin page (gated onadminSetting:read;vendor.*is the shape offorAdminentries only, no per-vendor hydration) andGET /admin/vendors/:vendorId/settings/registry/*for the per-vendor admin page (gated onplatformVendorSetting:read; returns the full vendor surface hydrated through the cascade). Seeadmin/settings.md.
Reviews Module — Vendor surface
Vendor-facing HTTP surface for moderating reviews on the vendor's own products — listing, editing content, approving/rejecting, marking/unmarking spam, and soft-deleting. List is…
Shipping ClickPost Module — Vendor surface
Vendor-facing HTTP surface for per-vendor ClickPost integration — credentials, pickup pincode, webhook secret, and the courier allow-list. ClickPost is one shipping provider…