Settings Module
HTTP surface for platform settings (admin and store scopes managed by platform staff) and per-vendor settings (admin scope for vendor back-office config, store scope for…
HTTP surface for platform settings (admin and store scopes managed by platform staff) and per-vendor settings (admin scope for vendor back-office config, store scope for storefront branding the vendor controls). Storefront has an anonymous read of the subset of platform store-scope settings flagged public: true.
Source:
api-modules/settings(registered viaSettingsModule.forRoot()inapps/api/src/app.module.ts).The module is registry-driven: every setting is declared as a
SettingDefinition(group + key + zod type + flags likepublic,forAdmin). Other modules consume settings viaSettingsService.getGroup(scope, group)andVendorSettingsService.getGroup(vendorId, scope, group)— never by reading the underlying tables directly.
Conventions
Authentication
| Endpoint group | Auth | Permission |
|---|---|---|
GET /store/settings/** | none | — (only registry keys flagged public: true surface) |
GET/PATCH /admin/settings/admin* | required | adminSetting: read / update |
GET/PATCH /admin/settings/store* | required | storeSetting: read / update |
GET/PATCH /vendor/settings/admin*, /vendor/settings/store* | required (vendor session) | active vendor in session |
GET/PATCH /admin/vendors/:vendorId/settings/admin*, /store* | required | platformVendorSetting: read / update |
The vendor self-service surface refuses to write any key flagged forAdmin: true in the registry (responds 403 FORBIDDEN at the service layer). The platform-admin override surface (/admin/vendors/:vendorId/settings/...) bypasses that guard, so platform staff can set admin-only keys on a target vendor's behalf.
Response envelope
Successful responses are wrapped by ResponseInterceptor:
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": null
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR (unknown group/key, value fails the registry's zod) |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN (missing permission, vendor wrote a forAdmin key) |
| 404 | NOT_FOUND (storefront — unregistered or non-public key) |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Architecture
┌─────────────────────────────────────────┐
│ SettingDefinition │
│ scope ∈ {admin, store} │
│ group, key, zod, defaultValue │
│ public?: bool (storefront read-out) │
│ forAdmin?: bool (vendor write-guard) │
└────────────────┬────────────────────────┘
│
┌────────────────────────┼───────────────────────────┐
▼ ▼ ▼
Platform registry Vendor registry Storefront `public:true` filter
(api-modules/ (api-modules/ reads only flagged keys
settings/registry/ settings/registry/ no auth required
admin/*) vendor/*)Two scopes:
| Scope | Owned by | Purpose |
|---|---|---|
admin | platform staff / vendor back-office | Operational config: payment provider credentials, review moderation rules, shipping defaults, notification preferences. Never read by the storefront |
store | platform staff / vendor branding | Storefront-facing: logo, contact email, social links, terms-and-conditions URL. Subset is public — see below |
Two surfaces per actor:
| Actor | Reads | Writes |
|---|---|---|
| Anonymous storefront | public: true keys in platform store scope only | — |
| Platform staff | Both scopes of the platform registry; any vendor's settings (via override surface) | Both scopes; any vendor's keys including forAdmin |
| Vendor user | The active vendor's two scopes | Both scopes except forAdmin: true keys (403) |
Audit: every write goes through setMany which records a per-key audit row (actor_id, scope, group, key, before → after).
Domain types
ScopeSettingsResponse (platform admin)
type ScopeSettingsResponse = Record<string, Record<string, unknown>>;
// { groupName: { keyName: value }, ... }GroupSettingsResponse (platform admin, single group)
type GroupSettingsResponse = Record<string, unknown>;
// { keyName: value, ... }SettingValueResponse (storefront, single key)
type SettingValueResponse = { value: unknown };VendorScopeSettingsResponse / VendorGroupSettingsResponse
Diverges from the platform shape by exposing a readOnlyKeys array — the keys in this scope/group flagged forAdmin: true. The vendor UI uses it to disable write controls on those fields.
type VendorScopeSettingsResponse = {
values: Record<string, Record<string, unknown>>; // group → key → value
readOnlyKeys: Record<string, string[]>; // group → forAdmin keys
};
type VendorGroupSettingsResponse = {
values: Record<string, unknown>;
readOnlyKeys: string[];
};UpdateSettingsInput / UpdateVendorSettingsInput
Bulk update payload — nested by group:
{
"reviews": {
"allow_vendor_approve": true,
"max_images_per_review": 10
},
"payment.razorpay": {
"key_id": "rzp_live_..."
}
}Top-level zod is permissive (Record<string, Record<string, unknown>>); the service validates each (group, key, value) triple against the registry. Unknown groups/keys → 400; value-shape mismatch → 400 with the zod issue.
Storefront
Base path: /store/settings. Anonymous. Returns only keys flagged public: true in the platform store registry. Unknown groups/keys throw 404 rather than returning empty — that way an attacker can't probe for the existence (or absence) of admin-only configuration.
GET /store/settings — Full public scope
Response 200 — ScopeSettingsResponse containing only public keys.
{
"data": {
"branding": {
"logo_url": "https://cdn.example/logo.svg",
"store_name": "Acme"
},
"contact": {
"email": "hello@acme.example",
"phone": "+91-80-1234-5678"
}
},
"message": "Success",
"statusCode": 200
}GET /store/settings/:group
Response 200 — GroupSettingsResponse. 404 when the group has no public keys.
GET /store/settings/:group/:key
Response 200 — SettingValueResponse. 404 when the key isn't registered as public.
Platform — admin scope
Base path: /admin/settings/admin. Required permission: adminSetting:read / :update.
| Method + path | Notes |
|---|---|
GET /admin/settings/admin | Every admin-scope group keyed { group: { key: value } } |
GET /admin/settings/admin/:group | One group, flat { key: value } |
PATCH /admin/settings/admin | Bulk update. Body { group: { key: value } }. Unknown group/key → 400; value mismatch → 400 |
The split per-scope routing (separate admin* and store* paths) exists because @RequirePermissions is AND-only — combining both resources on one decorator would require both permissions on the caller.
Platform — store scope
Base path: /admin/settings/store. Required permission: storeSetting:read / :update. Same shape as the admin-scope endpoints.
Vendor self-service
Base path: /vendor/settings. Auth: Better-Auth session with an active vendor. No platform RBAC permission — vendor users are not platform staff.
GET /vendor/settings/admin / GET /vendor/settings/store
Returns VendorScopeSettingsResponse for the active vendor:
{
"data": {
"values": {
"shipping": {
"flat_rate_subunit": 4900,
"free_above_subunit": 99900,
"enabled_providers": ["clickpost", "self-handled"]
}
},
"readOnlyKeys": {
"shipping": ["admin_only_carrier_token"]
}
},
"message": "Success",
"statusCode": 200
}GET /vendor/settings/admin/:group / GET /vendor/settings/store/:group
Returns VendorGroupSettingsResponse for one group.
PATCH /vendor/settings/admin / PATCH /vendor/settings/store
Bulk update. Same body shape as the platform-admin endpoints. forAdmin: true keys reject with 403 even when the rest of the payload is valid — the service rejects the whole call rather than silently partial-applying. Use the platform-admin override surface (below) to write those.
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 |
Platform-admin override — any vendor
Base path: /admin/vendors/:vendorId/settings. Required permission: platformVendorSetting:read / :update. Lets platform staff read and write any vendor's settings, including keys flagged forAdmin: true.
Distinct permission resource from the regular admin settings so it can be granted to platform support staff without also granting it via the adminSetting/storeSetting resources (and vice versa).
| Method + path | Notes |
|---|---|
GET /admin/vendors/:vendorId/settings/admin | Same shape as the vendor self-service endpoint, scoped to target vendor |
GET /admin/vendors/:vendorId/settings/admin/:group | Single group |
PATCH /admin/vendors/:vendorId/settings/admin | Bulk update. Bypasses the forAdmin write-guard |
GET /admin/vendors/:vendorId/settings/store | Store scope |
GET /admin/vendors/:vendorId/settings/store/:group | |
PATCH /admin/vendors/:vendorId/settings/store | Bulk update. Bypasses the forAdmin write-guard |
Registry (programmatic API)
Settings are declared with defineSetting() / defineVendorSetting(). New consumer modules don't add HTTP endpoints — they extend the registry array and call SettingsService / VendorSettingsService.
// api-modules/settings/src/registry/admin/reviews.ts
export const adminReviewsSettings = [
defineSetting({
scope: "admin",
group: "reviews",
key: "allow_vendor_approve",
zod: z.boolean(),
defaultValue: false,
// public: false (default) — not exposed on /store/settings
// forAdmin: false (default) — vendor self-service can read but ... wait, this is platform-scope
}),
];
// api-modules/settings/src/registry/vendor/admin/shipping.ts
export const adminShippingVendorSettings = [
defineVendorSetting({
scope: "admin",
group: "shipping",
key: "flat_rate_subunit",
zod: z.number().int().min(0),
defaultValue: 0,
}),
defineVendorSetting({
scope: "admin",
group: "shipping",
key: "admin_only_carrier_token",
zod: z.string(),
defaultValue: "",
forAdmin: true, // vendor reads but cannot write — surfaces in readOnlyKeys
}),
];Known platform groups
| Scope | Group | Owner / purpose |
|---|---|---|
admin | reviews | Review moderation flags consumed by @sc/reviews |
admin | payment | Enabled payment provider list per platform |
admin | payment.razorpay | Razorpay credentials (key_id, key_secret, webhook_secret) |
admin | shipping | Platform-wide shipping defaults / allow-lists |
admin | notifications | Channel toggles, default templates |
store | (registered by storefront-facing features) | Branding, contact details — typically public: true |
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 | notifications | Vendor-side notification preferences |
store | branding, contact, etc. | Vendor's storefront customization |
Related modules
admin-rbac— provides theplatformVendorSetting,adminSetting,storeSettingpermissions. Seeadmin-rbac.md.shipping— readsvendor.admin.shipping.*; provides a typed UI over the underlying settings. Seeshipping.md.payment-razorpay— readsadmin.payment.razorpay.*credentials from this module. Seepayment-razorpay.md.reviews— readsadmin.reviews.allow_vendor_*flags to gate vendor-side moderation. Seereviews.md.notifications— reads channel toggles fromadmin.notifications.*. Seenotifications.md.
Search Module
Storefront-facing faceted product search and autocomplete, plus an admin-side bulk reindex trigger. Backed by Typesense via a provider-neutral SEARCH_PORT; the adapter is…
Shipping Module
Two-layer shipping model. Customer-charge layer: every vendor has a flat per-order shipping rate (with optional free-above threshold) that the cart applies as a single line at…