Supercommerce API Docs
Admin API

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 @RequirePermissions is AND-only — combining adminSetting and storeSetting on 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 own vendorSetting resource.


Conventions

Authentication

All endpoints require a Better-Auth admin session and a role granting the matching permission.

Endpoint groupPermission
GET /admin/settings/admin/**adminSetting: read
PATCH /admin/settings/adminadminSetting: update
GET /admin/settings/store/**storeSetting: read
PATCH /admin/settings/storestoreSetting: update
GET /admin/settings/registry/**adminSetting: read
GET /admin/vendors/:vendorId/settings/**platformVendorSetting: read
PATCH /admin/vendors/:vendorId/settings/admin|storeplatformVendorSetting: 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

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR (unknown group / key, value fails registry validator)
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND
500INTERNAL_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 200ScopeSettingsResponse.


GET /admin/settings/admin/:group — Single admin group

Required permission: adminSetting: read.

Path params

NameNotes
groupGroup identifier (e.g. reviews, payment)

Response 200GroupSettingsResponse (flat { key: value }).

Errors

StatusCodeWhen
400VALIDATION_ERRORUnknown 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 200ScopeSettingsResponse (full admin scope after the patch).

Errors

StatusCodeWhen
400VALIDATION_ERROREmpty 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 scope
  • GET /admin/settings/store/:group — single group
  • PATCH /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

NameNotes
vendorIdTarget vendor id

Response 200VendorScopeSettingsResponse.


GET /admin/vendors/:vendorId/settings/admin/:group — Single vendor admin group

Required permission: platformVendorSetting: read.

Response 200VendorGroupSettingsResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORUnknown 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 200VendorScopeSettingsResponse after the patch.

Errors

StatusCodeWhen
400VALIDATION_ERROREmpty 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 scope
  • GET /admin/vendors/:vendorId/settings/store/:group — single group
  • PATCH /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:

EndpointPermissionPurpose
GET /admin/settings/registry/*adminSetting: readGlobal 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: readPer-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, with currentValue from the platform settings table.
  • store — every platform store-scope setting in the requested groups, with currentValue from 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 flagged forAdmin: true (admin-controlled per-vendor knobs like payouts.payout_hold, payouts.notes). currentValue is the registry default — never per-vendor, even when a vendorId would be available in the URL. This keeps adminSetting:read from leaking per-vendor data.
    • Per-vendor admin (/admin/vendors/:vendorId/settings/registry/fields): the full vendor surface (both forAdmin and vendor-writable entries) hydrated through the override → platform → default cascade for the target vendor. Matches PlatformVendorSettingsController, the existing values endpoint that lets admin write any vendor key with the forAdmin write guard bypassed. forAdmin entries carry isAdminOnly: true on the descriptor so the UI flags them visually rather than dropping them.

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[]>;   // groupfields
  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

NameNotes
groupsComma-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

StatusCodeWhen
400VALIDATION_ERRORgroups missing or empty (route param validation)
403FORBIDDENCaller 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

NameNotes
vendorIdTarget vendor id

Response 200VendorRegistryGroupsResponse

{
  "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

StatusCodeWhen
403FORBIDDENCaller 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

NameNotes
vendorIdTarget vendor id

Query

NameNotes
groupsComma-separated group keys. At least one required. Same matching semantics as the global admin endpoint.

Response 200RegistryFieldsResponse (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

StatusCodeWhen
400VALIDATION_ERRORgroups missing or empty
403FORBIDDENCaller lacks platformVendorSetting: read

Unknown group keys are silently dropped — same tolerance as the global endpoint.


  • admin-rbac — gates every endpoint via adminSetting:* / storeSetting:* / platformVendorSetting:*. See admin-rbac.md. The registry-discovery surface inherits the same split: /admin/settings/registry/* is adminSetting:read; /admin/vendors/:vendorId/settings/registry/* is platformVendorSetting:read.
  • vendor — vendor-self read/write of the same per-vendor settings keys; the same DTO carries the readOnlyKeys[] list informing the vendor UI what they can't edit. The vendor-side registry discovery surface is at GET /vendor/settings/registry/* (see vendor/settings.md).
  • order — reads admin.payment.pending_timeout_hours for the stale-pending sweep. See order.md.
  • reviews — reads admin.reviews.* keys for moderation rules.

On this page