---
feature: Platform settings and feature flags
slug: settings
surface: admin
consumer: vue
version: 0.1.0
status: in-progress
brd_version: "Morvia Project Guide"
brd_ids: [ADM-051, PLT-001]
business_rules: [BR-FIN-03]
decisions: [DEC-02, DEC-06, DEC-22, DEC-25, TD-14, TD-15, TD-17]
schema_tables: [app_configs, feature_flags]
screens: []
related_docs: []
last_updated: 2026-11-12
---

# Platform settings and feature flags — Vue dashboard integration

> Base URL: `{HOST}/api/v1/admin`. Shared conventions: [API_CONVENTIONS.md](../API_CONVENTIONS.md).

## Quick reference

| | |
|---|---|
| Version | 0.1.0 (2026-11-12); dashboard action: integrate existing controls with §13 limitations. |
| Status | In progress: base endpoints exist; ADM-051 and PLT-001 are incomplete. |
| Auth | Active admin bearer token; route names below are RBAC permission keys; local/testing bypass RBAC. |

| Method | Path | Route / permission | Purpose |
|---|---|---|---|
| GET | `/settings` | `admin.settings.index` | Grouped settings |
| PUT | `/settings` | `admin.settings.update` | Update existing values |
| GET | `/feature-flags` | `admin.feature-flags.index` | List flags |
| POST | `/feature-flags` | `admin.feature-flags.create` | Create flag |
| PUT | `/feature-flags/{id}` | `admin.feature-flags.update` | Partial edit |
| DELETE | `/feature-flags/{id}` | `admin.feature-flags.delete` | Delete flag |
| POST | `/feature-flags/{id}/toggle` | `admin.feature-flags.toggle` | Invert enabled state |

**Statuses:** flag `is_enabled` true/false; settings have no status. **Errors:** 401, 403, 404, 405, 422, 429, 500; these paths do not define feature-specific `error_code`s. **Open decisions:** DEC-02 OTP values; DEC-06 payment gateway; DEC-25 loyalty. See §13.

## 1. Business summary

The dashboard can read grouped configuration values, update existing keys, and manage server-side feature flags. These base endpoints do not yet implement the complete platform settings required by the BRD. Financial values are provisional; changing a setting does not establish that every business flow consumes it.

## 2. Pages and permissions

| Component | Purpose | Endpoint / permission |
|---|---|---|
| Settings page | Show groups and edit values | GET `admin.settings.index`; PUT `admin.settings.update` |
| Feature flags table | List flags | GET `admin.feature-flags.index` |
| Flag form | Create / edit | POST `admin.feature-flags.create`; PUT `admin.feature-flags.update` |
| Flag actions | Delete / toggle | DELETE `admin.feature-flags.delete`; POST `admin.feature-flags.toggle` |

Gate each control by its exact permission; the sidebar needs the relevant index permission. Send `Authorization: Bearer {token}`, `Accept: application/json`, `Content-Type: application/json` on JSON requests, and optionally `Accept-Language: ar` or `en` for messages. All paths in this guide are relative to the base URL. These routes use `auth:api`, `active`, and `rbac.auto`; permission enforcement is bypassed in local/testing and can depend on route-to-permission resolution elsewhere.

## 3. List / table

**Settings:** GET `/settings` returns `data` as an object keyed by group, each an array sorted by group and key; an empty collection can serialize as `[]`. No pagination, filtering, search, or sorting parameters are implemented. Each raw record contains `uuid` (public UUID, not `id`), `key`, `type`, nullable string `value`, `group`, nullable JSON `metadata`, and nullable ISO timestamp strings `created_at`, `updated_at`. TD-14 hides numeric IDs; settings identify updates by key.

Seeded keys below all belong to `general`; values are provisional defaults from `AppConfigSeeder`, not guaranteed current values. The same FormRequest validation applies to every key: it does not impose numeric ranges, boolean, locale, currency, or key-specific rules.

| Key | Seed type | Seed value | Meaning |
|---|---|---|---|
| `app_name` | string | `Morvia` | Platform name |
| `default_locale` | string | `ar` | Default locale |
| `default_currency` | string | `SAR` | Currency |
| `trainer_commission_rate`, `center_commission_rate`, `hall_commission_rate` | number | `0.00` each | Commission rates |
| `points_conversion_points` | number | `100` | Points conversion quantity |
| `points_conversion_value` | number | `10.00` | Conversion value |
| `points_conversion_enabled` | boolean | `0` | Conversion switch |
| `threshold_minimum_deadline_days`, `threshold_date_deadline_days` | number | `7` each | Threshold deadlines in days |
| `otp_expiry_minutes` | number | `5` | OTP expiry minutes |
| `otp_resend_interval_seconds` | number | `60` | Resend interval seconds |
| `otp_max_attempts` | number | `5` | Maximum attempts |
| `otp_max_resends` | number | `3` | Maximum resends |

Real response, isolated SQLite test with seeded settings, HTTP 200 (array trimmed to one complete record):

```json
{"status":"success","message":"Settings retrieved","data":{"general":[{"uuid":"a3354ac6-388f-4466-a7a2-1c5f993fed4a","key":"app_name","type":"string","value":"Morvia","group":"general","metadata":null,"created_at":"2026-11-12T14:13:50.000000Z","updated_at":"2026-11-12T14:13:50.000000Z"}]}}
```

**Flags:** GET `/feature-flags`: default `page=1`, `per_page=15` (also accepts `limit`; `per_page` wins), clamped 1–100. `paginate=false` or `paginate=0` returns a plain `data` array capped at 500. Paginated `data` has `feature_flags`, `links` (`first`, `last`, `prev`, `next`) and `meta` (`current_page`, `from`, `last_page`, `links`, `path`, `per_page`, `to`, `total`); meta links contain `url`, `label`, `page`, `active`. Default order is newest `created_at`. `search` and `filters[...]` have no effect because this model defines no searchable fields. `sort[field]=asc|desc`, or `sort=field&direction=asc|desc`, reaches generic SQL sorting without a flag-specific allowlist; use known columns such as `key`, `is_enabled`, `created_at`, or `updated_at`, and avoid unsupported fields (which can cause 500).

Real HTTP 200 paginated response from the same isolated test:

```json
{"status":"success","message":"Data retrieved successfully","data":{"feature_flags":[{"id":"a3354ac6-4c67-486d-b270-7cc20bb2fc08","key":"sample_flag","is_enabled":true,"targeting_rules":null,"description":"Sample","created_at":"2026-11-12 14:13:51"}],"links":{"first":"http://localhost/api/v1/admin/feature-flags?page=1","last":"http://localhost/api/v1/admin/feature-flags?page=1","prev":null,"next":null},"meta":{"current_page":1,"from":1,"last_page":1,"links":[{"url":null,"label":"&laquo; Previous","page":null,"active":false},{"url":"http://localhost/api/v1/admin/feature-flags?page=1","label":"1","page":1,"active":true},{"url":null,"label":"Next &raquo;","page":null,"active":false}],"path":"http://localhost/api/v1/admin/feature-flags","per_page":15,"to":1,"total":1}}}
```

## 4. Details view

No settings or flag show route exists. Edit a flag from its list record or from the returned mutation detail. Flag fields: `id` UUID string, `key` string, `is_enabled` boolean, `targeting_rules` array/object or null, `description` string or null, `created_at` string or null; mutation details also have `updated_at` string or null. Flag dates use `YYYY-MM-DD HH:mm:ss`; settings timestamps use ISO format. Do not infer timezone from flag date strings.

## 5. Forms (create / edit)

**Settings PUT `/settings`:**

| Field | Validation | Notes |
|---|---|---|
| `settings` | required, array | Submit at least one existing key. |
| `settings.*.key` | required, string, max 100 | Unique database key; no allowlist validation. |
| `settings.*.value` | nullable, string | Send explicitly, including `null`; no max, range or type-specific validation. Omission passes validation but service accesses the missing key. |

Example request (illustrative input): `{"settings":[{"key":"app_name","value":"Example"}]}`. Updates loop through keys without a transaction; a later failure can leave earlier values changed. Group, type and metadata cannot be edited here. Creating an unknown key fails with a database error because required `group` is not supplied. Re-fetch after saving or any failure.

Real HTTP 200 response:

```json
{"status":"success","message":"Settings updated successfully","data":null}
```

**Flags POST `/feature-flags` / PUT `/feature-flags/{id}`:** `{id}` is the flag UUID. PUT is partial when the UUID resolves; no PATCH route.

| Field | Create | Update | FormRequest validation |
|---|---|---|---|
| `key` | required | sometimes | string, max 100, unique in `feature_flags.key`, excluding the resolved flag |
| `is_enabled` | optional | optional | sometimes, boolean; database default false |
| `targeting_rules` | optional | optional | nullable, array; no nested validation |
| `description` | optional | optional | nullable, string; no request max |

Laravel boolean validation accepts `true`, `false`, `1`, `0`, `"1"`, `"0"`; send JSON booleans. No default targeting is required. Request example: `{"key":"sample_flag","is_enabled":true,"description":"Sample"}`.

Real create HTTP 201:

```json
{"status":"success","message":"Data saved successfully","data":{"id":"a3354ac6-4c67-486d-b270-7cc20bb2fc08","key":"sample_flag","is_enabled":true,"targeting_rules":null,"description":"Sample","created_at":"2026-11-12 14:13:51","updated_at":"2026-11-12 14:13:51"}}
```

Real update HTTP 200 for `{"description":"Updated"}`:

```json
{"status":"success","message":"Data saved successfully","data":{"id":"a3354ac6-4c67-486d-b270-7cc20bb2fc08","key":"sample_flag","is_enabled":true,"targeting_rules":null,"description":"Updated","created_at":"2026-11-12 14:13:51","updated_at":"2026-11-12 14:13:51"}}
```

The evaluator understands targeting rule objects `country` (`values` country codes, based on `X-Country` then user country), `version` (`min_version`, based on `X-App-Version`, default `0.0.0`), `segment` (`values`), and `percentage` (`value`, `seed`, deterministic per user). All must pass; missing context fails country/segment/percentage; unknown rule types pass. A disabled flag is always off. These nested shapes and ranges are not validated by the request; malformed rules can produce unexpected behavior.

## 6. Actions (non-CRUD)

### 6.1 Toggle and delete

POST `/feature-flags/{id}/toggle`, permission `admin.feature-flags.toggle`, takes no body and flips the current boolean; avoid blind retries because it is not idempotent. Real HTTP 200:

```json
{"status":"success","message":"Feature flag toggled","data":{"id":"a3354ac6-4c67-486d-b270-7cc20bb2fc08","key":"sample_flag","is_enabled":false,"targeting_rules":null,"description":"Updated","created_at":"2026-11-12 14:13:51","updated_at":"2026-11-12 14:13:51"}}
```

DELETE `/feature-flags/{id}`, permission `admin.feature-flags.delete`, takes no body and deletes the record. Real HTTP 200:

```json
{"status":"success","message":"Data removed successfully","data":null}
```

All flag mutations invalidate the flag cache; settings updates bump bootstrap cache version. Refresh the list after mutation.

### 6.2 Errors for all endpoints

| HTTP | `error_code` | When | Dashboard behavior |
|---|---|---|---|
| 401 | — | Missing/invalid authentication | Reauthenticate. |
| 403 | — | Inactive account/role or denied permission | Show message; hide unauthorized controls; denied RBAC may include `X-Missing-Permission`. |
| 404 | — | Missing flag UUID or route | Refresh list; show message. An update with a missing UUID and no key can instead fail validation first (422). |
| 405 | — | Unsupported method | Correct integration. |
| 422 | — | Request validation, including duplicate flag key | Show first `message`; no field error map. |
| 429 | — | Request throttled | Retry later. |
| 500 | — | Server/database error, including new settings without `group` | Show generic failure; re-fetch settings because bulk updates are not atomic. |

Real validation response, HTTP 422: `{"status":"fail","message":"The Key field is required.","data":null}`. Real missing flag deletion, HTTP 404: `{"status":"fail","message":"Resource not found","data":null}`. Messages may vary with locale and debug configuration; never display diagnostic SQL to users.

## 7. Statuses and enums

| Field | Value | Meaning / actions |
|---|---|---|
| `is_enabled` | `true` | Globally eligible, subject to targeting; toggle disables. |
| `is_enabled` | `false` | Disabled regardless of targeting; toggle enables. |
| Setting `type` | `string`, `number`, `boolean` | Seed labels only; all returned `value`s are strings or null. Other database type labels can exist. |

There are no status enums or `can_*` fields. Seeded flags: `enable_notifications` and `enable_reviews` enabled, `maintenance_mode` disabled; these defaults do not prove consuming flows enforce them.

## Data model

| Table | Access | Notes |
|---|---|---|
| `app_configs` | Read/write | Unique key, UUID; group and metadata returned; existing value updates only. |
| `feature_flags` | Read/write/delete | UUID exposed as `id`; unique key and nullable JSON targeting rules. |

## 8. TypeScript types

```ts
type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
type Envelope<T> = { status: string; message: string; data: T };
type Setting = { uuid: string; key: string; type: string; value: string | null; group: string; metadata: Json; created_at: string | null; updated_at: string | null };
type SettingsData = Record<string, Setting[]> | [];
type SettingsUpdate = { settings: { key: string; value: string | null }[] };
type FeatureFlag = { id: string; key: string; is_enabled: boolean; targeting_rules: Json[] | Record<string, Json> | null; description: string | null; created_at: string | null };
type FeatureFlagDetails = FeatureFlag & { updated_at: string | null };
type FlagCreate = { key: string; is_enabled?: boolean; targeting_rules?: Json[] | Record<string, Json> | null; description?: string | null };
type FlagUpdate = Partial<FlagCreate>;
type PageLink = { url: string | null; label: string; page: number | null; active: boolean };
type FlagPage = { feature_flags: FeatureFlag[]; links: { first: string | null; last: string | null; prev: string | null; next: string | null }; meta: { current_page: number; from: number | null; last_page: number; links: PageLink[]; path: string; per_page: number; to: number | null; total: number } };
```

## 9. Business rules enforced by the backend

| Rule | Backend behavior | Dashboard effect |
|---|---|---|
| Base validation | Generic string settings and unique flag keys; no financial bounds. | Validate carefully and surface server errors. |
| Flag targeting | Enabled flag must also pass all recognized targeting rules. Unknown types pass. | Enabled does not mean enabled for every user. |
| BR-FIN-03 / TD-17 | Checkout confirmation reads trainer/center commission and records the rate per earning. | Earlier recorded checkout rates are snapshots; do not assume all financial workflows provide this guarantee. |

## 10. Notifications triggered

N/A: these services do not explicitly send notifications. Generic CRUD pipeline hooks and auditing are separate infrastructure, not a documented push event contract.

## 11. Integration checklist

- [ ] Gate each control with its route permission; test access in an environment enforcing RBAC.
- [ ] Load settings dynamically by group and key, retain string values, and send only existing keys with explicit values.
- [ ] Do not treat provisional defaults or unvalidated financial values as approved business policy.
- [ ] Use UUID flag IDs, pagination shape, partial PUT, and explicit refresh after actions.
- [ ] Handle validation, missing records, server errors and non-idempotent toggle retries.
- [ ] Test Arabic/English and RTL; locale setting alone does not establish complete localization.
- [ ] Coordinate unsupported settings and gateway integration with backend; do not expose invented controls.

## 12. Acceptance scenarios

| ID | Scenario | Expected / current evidence |
|---|---|---|
| ADM-051 (no specific AC ID identified) | Read and update a seeded setting | Grouped record is returned and update responds 200; re-fetch to verify. |
| Base flags | Create, list, edit, toggle, delete a flag | UUID-based responses and 201/200 as captured; delete then yields 404. |
| Base validation | Submit missing key | 422 with first message and null data. |
| BR-FIN-03 | Change commission after prior transaction | Prior earning keeps stored rate in checkout; full financial lifecycle remains to be verified. |
| PLT-001 | Switch platform language and use both languages | BRD requires Arabic and English; end-to-end coverage and application of this setting are not established. |

## 13. Open decisions and known limitations

| ID | Impact / current behavior |
|---|---|
| ADM-051 | BRD also expects contact details, subscriptions/business model, cashback earning rules, points levels, and notification-type switches. No corresponding seeded settings or specialized endpoints were found here; general flags do not establish per-notification control. |
| ADM-051 / BR-FIN-03 / DEC-22 | Trainer, center and hall commission keys exist; checkout consumes trainer/center rates and snapshots them. Global hall rate is decided, but consumption and snapshot in hall bookings are not established by this guide. Rates have no request bounds. |
| ADM-032 | Two deadline keys exist, but no minimum enrollment threshold setting is seeded and these deadline keys have no application references found in the inspected code. |
| DEC-02 | OTP expiry/resend values are provisional seeds; no application references to these keys were found. Engineering values and actual enforcement remain to be verified. |
| DEC-25 | Points conversion keys exist; cashback and points posting remain open/incomplete under TD-17. Seeded values are not proof of a working loyalty policy. |
| DEC-06 / TD-15 / S3 | Gateway vendor/methods remain open and live integration deferred; payment uses a configured fake driver. There is no gateway credential/configuration setting or admin gateway screen in these endpoints. |
| PLT-001 | `default_locale` exists but there is no locale-specific validation or proof it controls all clients and translations. |
| Base / TD-14 | Settings list uses `uuid`, not an `id` property; flag resources use UUID `id`. New settings cannot be created successfully through this API; bulk updates can partially persist. |
| Base | Seed labels `number` and `boolean` do not match the model typed accessor's `int`/`float`/`bool` cases; the settings API always returns raw values. Bootstrap reads only `ui`, `limits`, and `urls` groups, not seeded `general`. |
| Verification | Examples were captured from a passing isolated SQLite PHPUnit run with middleware disabled; they establish observed payloads, not authentication, permission enforcement, production database behavior, or full BRD acceptance. The test made one smoke assertion; it did not assert every response. |

---

## Version history

| Version | Date | Type | Summary | Dashboard action required |
|---|---|---|---|---|
| 0.1.0 | 2026-11-12 | Initial | Document existing base settings and flags, with BRD gaps. | Integrate existing endpoints subject to §13. |

## Changelog

### 0.1.0 — 2026-11-12

- **Added:** Existing settings and feature-flag endpoint contracts, captured responses and limitations.
- **Changed:** —
- **Fixed:** —
- **Removed / Deprecated:** —
- **Breaking:** No
- **Endpoints affected:** `GET /settings`, `PUT /settings`, `GET /feature-flags`, `POST /feature-flags`, `PUT /feature-flags/{id}`, `DELETE /feature-flags/{id}`, `POST /feature-flags/{id}/toggle`.
- **Dashboard action required:** Integrate the existing base endpoints and observe §13 gaps.
- **BRD / decisions:** ADM-051, PLT-001, BR-FIN-03, DEC-02, DEC-06, DEC-22, DEC-25, TD-14, TD-15, TD-17.
