@@ -202,6 +202,153 @@ paths:
202202 " 404 " :
203203 $ref : " #/components/responses/NotFound"
204204
205+ /api/v1/apps/{app_id}/feature-config :
206+ get :
207+ operationId : getAppFeatureConfig
208+ summary : Get the feature config layers of an app
209+ description : >
210+ Returns three layers of the app's feature config, all fully populated
211+ with server defaults so the client never has to reason about what is
212+ "unset": the effective **plan** config (defaults ← plan, i.e. what the
213+ app would get with no app-specific override — display this as the
214+ read-only "Plan" column/panel), the app-specific override YAML
215+ document as stored (verbatim, sparse — only the fields this app
216+ overrides), and the effective config for the app as it actually runs
217+ today (defaults ← plan ← app override). The server merges every
218+ section field-by-field (see `pkg/lib/config/feature_*.go` — every
219+ section's `Merge` is field-level, so setting one field never resets an
220+ unrelated sibling field back to a default); clients MUST NOT
221+ reimplement this merge — always use `effective_plan_feature_config`
222+ and `effective_app_feature_config` from this response for display, and the
223+ preview endpoint for live feedback while editing.
224+ parameters :
225+ - name : app_id
226+ in : path
227+ required : true
228+ schema :
229+ type : string
230+ responses :
231+ " 200 " :
232+ description : The feature config layers of the app
233+ content :
234+ application/json :
235+ schema :
236+ $ref : " #/components/schemas/AppFeatureConfigResponse"
237+ " 400 " :
238+ $ref : " #/components/responses/BadRequest"
239+ " 403 " :
240+ $ref : " #/components/responses/Forbidden"
241+ " 404 " :
242+ $ref : " #/components/responses/NotFound"
243+
244+ put :
245+ operationId : updateAppFeatureConfig
246+ summary : Replace the app-specific feature config override
247+ description : >
248+ Replaces the entire app-specific feature config override with the
249+ given YAML document. This is a full replacement, not a patch: any
250+ field previously overridden but omitted from the new document reverts
251+ to inheriting from the plan. The document is stored verbatim,
252+ preserving comments and key order. Pass an empty string (or a
253+ whitespace-only string) to clear all app-specific overrides. The
254+ document is validated against the feature config JSON schema before
255+ being stored; on failure, responds 400 with reason `ValidationFailed`
256+ and `info.causes` populated per
257+ [ValidationErrorCause](#/components/schemas/ValidationErrorCause) —
258+ each cause's `location` is an RFC 6901 JSON pointer into the submitted
259+ document (e.g. `/identity/oauth/providers/apple/disabled`), letting the
260+ client map every failure directly to the corresponding row in a table
261+ view without any custom error-parsing logic. The plan config itself is
262+ never modified by this endpoint. A site admin audit log entry
263+ (`site_admin.app.feature_config.updated`) is recorded.
264+ parameters :
265+ - name : app_id
266+ in : path
267+ required : true
268+ schema :
269+ type : string
270+ requestBody :
271+ required : true
272+ content :
273+ application/json :
274+ schema :
275+ $ref : " #/components/schemas/UpdateAppFeatureConfigRequest"
276+ responses :
277+ " 200 " :
278+ description : >
279+ The feature config layers after the update, including the newly
280+ computed effective config.
281+ content :
282+ application/json :
283+ schema :
284+ $ref : " #/components/schemas/AppFeatureConfigResponse"
285+ " 400 " :
286+ description : >
287+ Invalid request. When `reason` is `ValidationFailed`,
288+ `info.causes` is an array of ValidationErrorCause objects, one per
289+ failing field — see the `ValidationErrorCause` schema and the
290+ description above for the exact shape and an example.
291+ content :
292+ application/json :
293+ schema :
294+ $ref : " #/components/schemas/ErrorEnvelope"
295+ " 403 " :
296+ $ref : " #/components/responses/Forbidden"
297+ " 404 " :
298+ $ref : " #/components/responses/NotFound"
299+
300+ /api/v1/apps/{app_id}/feature-config/preview :
301+ post :
302+ operationId : previewAppFeatureConfig
303+ summary : Preview the effective feature config for a candidate override
304+ description : >
305+ Computes the effective feature config that would result from the given
306+ app-specific override, without persisting anything. Merging uses the
307+ app's current plan and the exact same server-side merge as the GET and
308+ PUT endpoints. This is the mechanism that keeps merge logic out of the
309+ client entirely: call this endpoint (debounced) on every edit to
310+ refresh a live "Effective" view — never compute it from
311+ `effective_plan_feature_config` and the edited YAML locally. The
312+ candidate override is validated against the feature config JSON
313+ schema exactly as PUT does; validation failures use the same 400
314+ `ValidationFailed` / `info.causes` shape described on PUT, so the same
315+ client-side error-highlighting code handles both endpoints.
316+ parameters :
317+ - name : app_id
318+ in : path
319+ required : true
320+ schema :
321+ type : string
322+ requestBody :
323+ required : true
324+ content :
325+ application/json :
326+ schema :
327+ $ref : " #/components/schemas/PreviewAppFeatureConfigRequest"
328+ responses :
329+ " 200 " :
330+ description : >
331+ The feature config layers as they would be if the candidate
332+ override were saved. Nothing is persisted.
333+ content :
334+ application/json :
335+ schema :
336+ $ref : " #/components/schemas/AppFeatureConfigResponse"
337+ " 400 " :
338+ description : >
339+ Invalid request. When `reason` is `ValidationFailed`,
340+ `info.causes` is an array of ValidationErrorCause objects, one per
341+ failing field — see the `ValidationErrorCause` schema and the
342+ description on PUT for the exact shape and an example.
343+ content :
344+ application/json :
345+ schema :
346+ $ref : " #/components/schemas/ErrorEnvelope"
347+ " 403 " :
348+ $ref : " #/components/responses/Forbidden"
349+ " 404 " :
350+ $ref : " #/components/responses/NotFound"
351+
205352 /api/v1/apps/{app_id}/usage/messaging :
206353 get :
207354 operationId : getAppMessagingUsage
@@ -507,7 +654,13 @@ components:
507654 description : Opaque ID for correlating errors in logs
508655 info :
509656 type : object
510- description : Additional structured details about the error
657+ description : >
658+ Additional structured details about the error. Shape depends on
659+ `reason`. Notably, for `reason: ValidationFailed` (JSON-schema
660+ validation failures — this is the server-wide convention, not
661+ specific to any one endpoint), `info.causes` is an array of
662+ [ValidationErrorCause](#/components/schemas/ValidationErrorCause)
663+ objects, one per failing field.
511664 additionalProperties : true
512665
513666 MessagingUsage :
@@ -804,3 +957,137 @@ components:
804957 Raw audit log data — the full event JSON stored in the
805958 _audit_log.data column, including context and payload.
806959 additionalProperties : true
960+
961+ ValidationErrorCause :
962+ description : >
963+ One JSON-schema validation failure. Matches the server's
964+ `validation.Error` struct (`pkg/util/validation/error.go`) — this is
965+ the server-wide shape for every `ValidationFailed` error in this API,
966+ not something specific to feature config. Emitted as one entry per
967+ failing leaf value; a client can group/sort by `location` to render
968+ multiple errors against the same document.
969+ type : object
970+ required :
971+ - location
972+ - kind
973+ properties :
974+ location :
975+ type : string
976+ description : >
977+ RFC 6901 JSON pointer to the failing value in the submitted
978+ document, e.g. `/identity/oauth/providers/apple/disabled` or
979+ `/oauth/client/maximum`. Empty string means the failure applies to
980+ the document root. Clients use this to look up the corresponding
981+ field in a declarative field registry and highlight that row.
982+ kind :
983+ type : string
984+ description : >
985+ The JSON schema keyword that failed, e.g. `type`, `required`,
986+ `enum`, `minimum`, `additionalProperties`. Use `details` and/or a
987+ generic per-kind message for display; do not assume every kind
988+ listed here is exhaustive — new keywords can appear as the schema
989+ evolves.
990+ details :
991+ type : object
992+ description : >
993+ Keyword-specific detail payload, e.g. `{"actual": "string",
994+ "expected": "boolean"}` for a `type` failure, or `{"missing":
995+ ["client_id"]}` for a `required` failure. Shape varies by `kind`;
996+ treat as opaque beyond displaying it or using known keys
997+ defensively.
998+ additionalProperties : true
999+
1000+ FeatureConfig :
1001+ x-go-type : config.FeatureConfig
1002+ x-go-type-import :
1003+ name : config
1004+ path : github.com/authgear/authgear-server/pkg/lib/config
1005+ description : >
1006+ An Authgear feature config object (the JSON form of
1007+ authgear.features.yaml). The authoritative schema is the server's
1008+ feature config JSON schema (FeatureConfigSchema in
1009+ pkg/lib/config); it is not duplicated here. Top-level sections
1010+ include identity, authentication, authenticator, custom_domain, ui,
1011+ oauth, hook, audit_log, google_tag_manager, rate_limits, messaging,
1012+ usage, collaborator, admin_api, test_mode, and fraud_protection. Every
1013+ section merges field-by-field across layers (code defaults ← cluster
1014+ ← plan ← app override) — an app-specific override only ever affects
1015+ the exact fields it sets; every other field simply inherits from the
1016+ plan.
1017+ type : object
1018+ additionalProperties : true
1019+
1020+ AppFeatureConfigResponse :
1021+ type : object
1022+ required :
1023+ - plan_name
1024+ - effective_plan_feature_config
1025+ - app_feature_config_yaml
1026+ - effective_app_feature_config
1027+ properties :
1028+ plan_name :
1029+ type : string
1030+ description : The plan the app is currently on.
1031+ effective_plan_feature_config :
1032+ allOf :
1033+ - $ref : " #/components/schemas/FeatureConfig"
1034+ description : >
1035+ The **effective** feature config of the plan alone — server
1036+ defaults merged with the plan's config, fully populated. This is
1037+ what the app would get if it had no app-specific override at all;
1038+ display this as the read-only "Plan" column/panel. It is
1039+ deliberately not the raw/sparse plan document: since merge is
1040+ field-level everywhere, the effective value is always well-defined
1041+ and there is no ambiguity between "the plan set this" and "this is
1042+ a code default" for the client to worry about — every value shown
1043+ here is simply what the app would actually run with under this
1044+ plan.
1045+ app_feature_config_yaml :
1046+ type : string
1047+ description : >
1048+ The stored app-specific override YAML document, verbatim —
1049+ including comments and key order. Empty string when the app has
1050+ no override. Sparse: only fields explicitly overridden for this
1051+ app are present. Clients parse this to render structured views
1052+ (e.g. a table); any field absent from this document simply
1053+ inherits from `effective_plan_feature_config`.
1054+ effective_app_feature_config :
1055+ allOf :
1056+ - $ref : " #/components/schemas/FeatureConfig"
1057+ description : >
1058+ The effective feature config computed by the server by merging
1059+ defaults ← plan ← app override, fully populated. This is what the
1060+ app actually runs with; use it for the "Effective" display. Always
1061+ re-fetch this (via GET after save, or via the preview endpoint
1062+ while editing) rather than deriving it from
1063+ `effective_plan_feature_config` and `app_feature_config_yaml`
1064+ locally.
1065+
1066+ UpdateAppFeatureConfigRequest :
1067+ type : object
1068+ required :
1069+ - app_feature_config_yaml
1070+ properties :
1071+ app_feature_config_yaml :
1072+ type : string
1073+ description : >
1074+ The new app-specific feature config override as a YAML document.
1075+ Replaces the existing override entirely and is stored verbatim,
1076+ preserving comments and key order. Pass an empty string (or a
1077+ whitespace-only string) to clear all overrides. Validated against
1078+ the feature config JSON schema; see the `ValidationErrorCause`
1079+ schema for the shape of validation failures. Since YAML is a
1080+ superset of JSON, automation may pass a JSON document here.
1081+
1082+ PreviewAppFeatureConfigRequest :
1083+ type : object
1084+ required :
1085+ - app_feature_config_yaml
1086+ properties :
1087+ app_feature_config_yaml :
1088+ type : string
1089+ description : >
1090+ The candidate app-specific feature config override as a YAML
1091+ document. Not persisted. Validated against the feature config
1092+ JSON schema exactly as PUT does; see the `ValidationErrorCause`
1093+ schema for the shape of validation failures.
0 commit comments