Settings Module — Admin
HTTP surface for the platform-wide settings store (admin + store scopes) and the platform-admin override surface for vendor-scoped settings (admin + store sub-scopes per vendor).…
HTTP surface for the platform-wide settings store (admin + store scopes) and the platform-admin override surface for vendor-scoped settings (admin + store sub-scopes per vendor). Reads are grouped (per group or full-scope); writes are bulk patches that the service validates against the in-code settings registry.
Source:
api-modules/settings/src/controllers/admin-settings.controller.ts,api-modules/settings/src/controllers/platform-vendor-settings.controller.ts.Routes are intentionally split per scope (rather than a single
/:scope) because@RequirePermissionsis AND-only — combiningadminSettingandstoreSettingon one decorator would require the caller to hold both, which is the opposite of what we want. Two fixed paths keep guard semantics clean. The vendor override surface uses a distinct permission resource (platformVendorSetting) so it can be granted to platform staff without also granting it to vendor users via their ownvendorSettingresource.
Conventions
Authentication
All endpoints require a Better-Auth admin session and a role granting the matching permission.
| Endpoint group | Permission |
|---|---|
GET /admin/settings/admin/** | adminSetting: read |
PATCH /admin/settings/admin | adminSetting: update |
GET /admin/settings/store/** | storeSetting: read |
PATCH /admin/settings/store | storeSetting: update |
GET /admin/settings/registry/** | adminSetting: read |
GET /admin/vendors/:vendorId/settings/** | platformVendorSetting: read |
PATCH /admin/vendors/:vendorId/settings/admin|store | platformVendorSetting: update |
GET /admin/vendors/:vendorId/settings/registry/** | platformVendorSetting: read |
Response envelope
Successful responses are wrapped by ResponseInterceptor:
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* optional, e.g. pagination */ }
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR (unknown group / key, value fails registry validator) |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Settings registry
The in-code settings registry is the source of truth for which (scope, group, key) tuples exist and what each value validates as. Both the controller and the OpenAPI DTOs are intentionally permissive (Record<string, unknown>) because duplicating the registry's types here would drift on every new setting; the service rejects unknown groups / keys and invalid values with 400 VALIDATION_ERROR.
forAdmin flag
Some vendor settings keys are flagged forAdmin: true in the registry. The vendor-self surface refuses to write those keys; only the platform-admin override below can. The vendor read surface returns them inside a readOnlyKeys[] list so the vendor UI can render those fields disabled.
Domain types
Platform ScopeSettingsResponse
type ScopeSettingsResponse = Record<string /* group */, Record<string /* key */, unknown /* value */>>;Platform GroupSettingsResponse
type GroupSettingsResponse = Record<string /* key */, unknown /* value */>;Vendor VendorScopeSettingsResponse
type VendorScopeSettingsResponse = {
values: Record<string /* group */, Record<string /* key */, unknown>>;
readOnlyKeys: Record<string /* group */, string[]>;
};Vendor VendorGroupSettingsResponse
type VendorGroupSettingsResponse = {
values: Record<string /* key */, unknown>;
readOnlyKeys: string[];
};UpdateSettingsInput (bulk update body)
type UpdateSettingsInput = Record<string /* group */, Record<string /* key */, unknown>>;At least one group must be present.
Platform settings — admin scope
Base path: /admin/settings/admin. Permissions on adminSetting:*.
GET /admin/settings/admin — Full admin scope
Required permission: adminSetting: read. Returns every admin-scope setting, grouped.
Response 200 — ScopeSettingsResponse.
GET /admin/settings/admin/:group — Single admin group
Required permission: adminSetting: read.
Path params
| Name | Notes |
|---|---|
group | Group identifier (e.g. reviews, payment) |
Response 200 — GroupSettingsResponse (flat { key: value }).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Unknown group |
PATCH /admin/settings/admin — Bulk-update admin settings
Required permission: adminSetting: update.
Body
{
"reviews": {
"allow_vendor_approve": true,
"max_images_per_review": 10
},
"payment": {
"pending_timeout_hours": 24
}
}The service validates each (group, key) against the registry and rejects unknown groups/keys + invalid values with 400.
Response 200 — ScopeSettingsResponse (full admin scope after the patch).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Empty body, unknown group / key, value fails registry validator |
Platform settings — store scope
Base path: /admin/settings/store. Permissions on storeSetting:*.
The store scope shape mirrors the admin scope exactly — replace adminSetting with storeSetting and the path segment admin with store. Endpoints:
GET /admin/settings/store— full store scopeGET /admin/settings/store/:group— single groupPATCH /admin/settings/store— bulk update
Required permissions: storeSetting: read / storeSetting: update.
Platform-admin vendor settings override
Base path: /admin/vendors/:vendorId/settings. Permissions on platformVendorSetting:*. Target vendor is taken from the URL, not the active session. Writes bypass the forAdmin guard — platform staff can set any registered key including the admin-only ones.
GET /admin/vendors/:vendorId/settings/admin — Full vendor admin scope
Required permission: platformVendorSetting: read.
Path params
| Name | Notes |
|---|---|
vendorId | Target vendor id |
Response 200 — VendorScopeSettingsResponse.
GET /admin/vendors/:vendorId/settings/admin/:group — Single vendor admin group
Required permission: platformVendorSetting: read.
Response 200 — VendorGroupSettingsResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Unknown group |
PATCH /admin/vendors/:vendorId/settings/admin — Bulk-update vendor admin settings
Required permission: platformVendorSetting: update. Bypasses the forAdmin write guard.
Body — same shape as UpdateSettingsInput.
Response 200 — VendorScopeSettingsResponse after the patch.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Empty body, unknown group / key, value fails validator |
Vendor store scope (override)
Mirrors the vendor admin scope. Endpoints:
GET /admin/vendors/:vendorId/settings/store— full vendor store scopeGET /admin/vendors/:vendorId/settings/store/:group— single groupPATCH /admin/vendors/:vendorId/settings/store— bulk update
Required permissions: platformVendorSetting: read / platformVendorSetting: update. Same forAdmin bypass.
Registry discovery
A read-only introspection surface that exposes the settings registry itself: which groups exist, what fields each group contains, what type each field is, and what its current value is. The admin UI consumes this to render the settings page directly from the registry — adding a new setting (or group) takes no frontend deploy.
There are two admin-side discovery endpoints, split on the same permission boundary the values surface already uses:
| Endpoint | Permission | Purpose |
|---|---|---|
GET /admin/settings/registry/* | adminSetting: read | Global admin settings page. Platform admin + store fields with current values; shape of forAdmin: true vendor entries (no per-vendor hydration). |
GET /admin/vendors/:vendorId/settings/registry/* | platformVendorSetting: read | Per-vendor admin settings page. Same shape, but vendor.* is the full vendor surface hydrated through the cascade for the target vendor. |
Sources:
api-modules/settings/src/controllers/admin-settings-registry.controller.ts,api-modules/settings/src/controllers/platform-vendor-settings-registry.controller.ts.
Surfaces returned
Both endpoints return the same RegistryFieldsResponse shape:
admin— every platform admin-scope setting in the requested groups, withcurrentValuefrom the platform settings table.store— every platform store-scope setting in the requested groups, withcurrentValuefrom the platform settings table.vendor.admin/vendor.store— vendor-overridable entries in the requested groups. Filtering and hydration differ per endpoint:- Global admin (
/admin/settings/registry/fields): only entries flaggedforAdmin: true(admin-controlled per-vendor knobs likepayouts.payout_hold,payouts.notes).currentValueis the registry default — never per-vendor, even when avendorIdwould be available in the URL. This keepsadminSetting:readfrom leaking per-vendor data. - Per-vendor admin (
/admin/vendors/:vendorId/settings/registry/fields): the full vendor surface (bothforAdminand vendor-writable entries) hydrated through the override → platform → default cascade for the target vendor. MatchesPlatformVendorSettingsController, the existing values endpoint that lets admin write any vendor key with theforAdminwrite guard bypassed.forAdminentries carryisAdminOnly: trueon the descriptor so the UI flags them visually rather than dropping them.
- Global admin (
Groups with no entries in a given surface are omitted from the response (not returned as empty arrays).
Domain types
SettingsGroupSummary
type SettingsGroupSummary = {
scope: "admin" | "store";
key: string; // group identifier
description: string; // copy for the admin tab/section heading
settingCount: number; // entries in this (scope, group)
};FieldDescriptor
type FieldDescriptor = {
key: string; // setting key within the group
description: string;
default: unknown; // registry default (null when unset)
currentValue: unknown; // hydrated value; falls back to default
isPublic?: true; // store-scope keys exposed via /store/settings
isAdminOnly?: true; // vendor keys flagged forAdmin in the registry
schema: JsonSchema; // JSON Schema (draft 2020-12) for the value
};schema is produced by z.toJSONSchema(def.schema, { unrepresentable: "any" }) — the UI picks form controls off schema.type / schema.enum / schema.format and renders constraints (minimum, maxLength, etc.) from the same payload. Optional / nullable schemas surface as { anyOf: [...] }. Unrepresentable schemas degrade to {} so a single odd definition never crashes discovery.
RegistryFieldsResponse
type RegistryFieldsResponse = {
admin: Record<string, FieldDescriptor[]>; // group → fields
store: Record<string, FieldDescriptor[]>;
vendor: {
// Global admin endpoint: forAdmin-only entries, default currentValue.
// Per-vendor admin endpoint: full vendor surface, cascade-hydrated.
admin: Record<string, FieldDescriptor[]>;
store: Record<string, FieldDescriptor[]>;
};
};GET /admin/settings/registry/groups — Group catalog
Required permission: adminSetting: read. Returns the flat list of platform groups (admin + store scope) for the admin UI's left rail / tab strip. Vendor-overridable groups are intentionally absent — they describe a parallel surface and are reached via the fields endpoint (filtered to forAdmin) or /vendor/settings/registry/groups.
Response 200
{
"data": {
"groups": [
{ "scope": "admin", "key": "reviews", "description": "Review moderation policy …", "settingCount": 9 },
{ "scope": "admin", "key": "payment", "description": "Checkout payment configuration …", "settingCount": 3 },
{ "scope": "store", "key": "branding", "description": "Customer-facing brand identity …", "settingCount": 3 }
]
},
"message": "Success",
"statusCode": 200
}GET /admin/settings/registry/fields — Global admin field descriptors
Required permission: adminSetting: read. For the requested set of group keys, returns render-ready field descriptors split by surface, with currentValue hydrated from the persisted platform settings table (defaults filled in for keys that have never been set). vendor.* carries only forAdmin: true entries, and their currentValue is the registry default — there is no vendorId parameter on this endpoint; per-vendor hydration lives on the sibling endpoint below.
Query
| Name | Notes |
|---|---|
groups | Comma-separated group keys (reviews,payment,branding). At least one required. Match is by key alone — supplying shipping returns every entry across scopes whose key matches. |
Response 200
{
"data": {
"admin": {
"reviews": [
{
"key": "auto_approve",
"description": "When true, new reviews skip the pending state and publish immediately.",
"default": false,
"currentValue": false,
"schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "boolean" }
},
{
"key": "max_images_per_review",
"description": "Hard cap on review_images rows per review. Enforced at submit time.",
"default": 5,
"currentValue": 5,
"schema": { "type": "integer", "minimum": 0, "maximum": 20 }
}
]
},
"store": {
"branding": [
{
"key": "logo_url",
"description": "Absolute URL of the storefront logo image.",
"default": null,
"currentValue": null,
"isPublic": true,
"schema": { "anyOf": [{ "type": "string", "format": "uri" }, { "type": "null" }] }
}
]
},
"vendor": {
"admin": {
"payouts": [
{
"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 (route param validation) |
| 403 | FORBIDDEN | Caller lacks adminSetting: read |
Unknown group keys are silently dropped (response returns 200 with empty buckets) — this lets the UI request the union of currently-rendered tabs without hard-failing when a tab name lags behind a registry rename.
GET /admin/vendors/:vendorId/settings/registry/groups — Per-vendor group catalog
Required permission: platformVendorSetting: read. Returns the flat list of vendor-overridable groups for the per-vendor admin settings page's tab strip. The catalog itself is global — vendorId is in the URL for permission gating + symmetry with the sibling values surface at /admin/vendors/:vendorId/settings/*.
Path params
| Name | Notes |
|---|---|
vendorId | Target vendor id |
Response 200 — VendorRegistryGroupsResponse
{
"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 | Caller lacks platformVendorSetting: read |
GET /admin/vendors/:vendorId/settings/registry/fields — Per-vendor field descriptors
Required permission: platformVendorSetting: read. Same RegistryFieldsResponse shape as the global admin endpoint, but vendor.* is the full vendor surface — both forAdmin and vendor-writable entries — hydrated through the cascade for the target vendor. Used by the per-vendor admin settings page to render an editable form against the vendor's actual values.
Path params
| Name | Notes |
|---|---|
vendorId | Target vendor id |
Query
| Name | Notes |
|---|---|
groups | Comma-separated group keys. At least one required. Same matching semantics as the global admin endpoint. |
Response 200 — RegistryFieldsResponse (same TS type as the global endpoint)
{
"data": {
"admin": { /* … platform admin scope, current values from the platform table */ },
"store": { /* … platform store scope, current values from the platform table */ },
"vendor": {
"admin": {
"payouts": [
{
"key": "commission_rate",
"description": "Per-vendor commission rate override (basis points). …",
"default": null,
"currentValue": 1500,
"schema": { "anyOf": [{ "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" }] }
},
{
"key": "payout_hold",
"description": "Admin-flipped manually to freeze disbursements …",
"default": false,
"currentValue": true,
"isAdminOnly": true,
"schema": { "type": "boolean" }
},
{
"key": "notes",
"description": "Free-form admin notes about the vendor's payout situation. …",
"default": "",
"currentValue": "escalated; check with finance next week",
"isAdminOnly": true,
"schema": { "type": "string" }
}
]
},
"store": {}
}
},
"message": "Success",
"statusCode": 200
}Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | groups missing or empty |
| 403 | FORBIDDEN | Caller lacks platformVendorSetting: read |
Unknown group keys are silently dropped — same tolerance as the global endpoint.
Related modules
admin-rbac— gates every endpoint viaadminSetting:*/storeSetting:*/platformVendorSetting:*. Seeadmin-rbac.md. The registry-discovery surface inherits the same split:/admin/settings/registry/*isadminSetting:read;/admin/vendors/:vendorId/settings/registry/*isplatformVendorSetting:read.vendor— vendor-self read/write of the same per-vendor settings keys; the same DTO carries thereadOnlyKeys[]list informing the vendor UI what they can't edit. The vendor-side registry discovery surface is atGET /vendor/settings/registry/*(seevendor/settings.md).order— readsadmin.payment.pending_timeout_hoursfor the stale-pending sweep. Seeorder.md.reviews— readsadmin.reviews.*keys for moderation rules.
Search Module — Admin
HTTP surface for the platform-admin operational control of the product search index. Currently a single endpoint: kick off a (chained) bulk reindex of the search index, optionally…
Shipping Module — Admin
HTTP surface for the platform-admin override of any vendor's shipping config (enabled providers list + customer-charge flat-rate + free-shipping threshold). Vendor-self read/write…