Skip to content

Reserved Paths

Reserved paths are per-consumer-platform route reservations that prevent content authors from aliasing a vanity URL onto a path that the consuming frontend routes to app functionality. Without reservations, a content author could create a vanity URL at /cart or /sign-in, and the resulting manifest would redirect shoppers off real application routes — a functional regression.

Reserved paths are stored in the cms_reserved_paths table and managed via this API + the EngageHQ platform-admin UI. Each entry binds a path to a platform_key (matching the same kebab-case convention used by Platform Elements), or to the sentinel '*' for entries that apply to every platform (infrastructure routes like /api and /admin).

When reserved paths apply

OperationEnforcement
POST / PUT /api/v1/content/items with a vanity_urls payloadThe union of every platform's active reserved paths is applied — most restrictive. A content author cannot create a path that ANY platform has reserved.
GET /api/v1/public/vanity-urls?platform_key=... (manifest)Only the requesting platform's reserved entries plus universal (*) entries are applied. Grandfathered entries (authored before a path was reserved) silently drop out of the manifest for that platform.

Authentication

All endpoints below require an authenticated admin JWT and are gated to platform super admins (is_super_admin=true in the JWT claims). Regular org admins — even those with full content-management permissions — cannot mutate reserved paths. Reserved paths are platform policy, not org content.

Endpoints

GET /api/v1/content/reserved-paths

List reserved paths, optionally filtered by platform_key or is_active. Results are ordered by platform_key, then path.

Query parameters

ParameterTypeRequiredDescription
platform_keystringnoFilter to a single platform. Use * to get universal entries only.
is_activebooleannoFilter to active or inactive entries.
searchstringnoCase-insensitive substring match against path and description.
per_pageintegernoPagination page size (max 100, default 50).

Response

json
{
  "success": true,
  "data": {
    "data": [
      {
        "id": "rpa_...",
        "platform_key": "shophero-ecom-v2",
        "path": "/cart",
        "match_type": "prefix",
        "description": "E-commerce cart / cartless list page.",
        "is_active": true,
        "created_by": "system",
        "updated_by": null,
        "created_at": "2026-04-16T12:00:00+00:00",
        "updated_at": "2026-04-16T12:00:00+00:00"
      }
    ],
    "meta": {
      "current_page": 1,
      "per_page": 50,
      "total": 33,
      "last_page": 1
    }
  }
}

POST /api/v1/content/reserved-paths

Register a new reserved path.

Request body

FieldTypeRequiredDescription
platform_keystringyesKebab-case identifier (e.g. shophero-ecom-v2) or * for a universal entry. Must match /^(\*|[a-z0-9]([a-z0-9-]*[a-z0-9])?)$/.
pathstringyesMust match /^\/[a-z0-9][a-z0-9\-\/.]*$/. Case-sensitive unique per platform_key.
match_typeenumnoprefix (default) or exact.
descriptionstringnoHuman-readable note — which feature owns this route?
is_activebooleannoDefaults to true.

Match types

  • prefix (default): blocks the exact path AND anything starting with path + '/'. Registering /cart blocks /cart and /cart/checkout.
  • exact: blocks only the literal path. Registering /exact-only blocks /exact-only but not /exact-only/sub.

Success response

Returns the created entry (same shape as the GET response's data.data[] items).

Error responses

StatusCondition
403Caller is not a platform super admin.
422Invalid platform_key / path format, or duplicate (platform_key, path).

GET /api/v1/content/reserved-paths/{reservedPath}

Fetch a single entry by hashkey.

PUT /api/v1/content/reserved-paths/{reservedPath}

Update mutable fields. platform_key and path are immutable — delete and recreate to change them. This keeps audit trails coherent and prevents silent behavior changes for entries other code references by (platform_key, path).

Request body (all optional)

FieldTypeDescription
match_typeenumprefix or exact.
descriptionstring | nullUpdate the admin note.
is_activebooleanToggle enforcement without deleting the row.

Error responses

StatusCondition
403Caller is not a platform super admin.
404No reserved path with the given hashkey.
422Attempted to change immutable platform_key or path. Response body includes errors.immutable_fields listing which fields were rejected.

DELETE /api/v1/content/reserved-paths/{reservedPath}

Hard-delete the entry. Existing vanity URLs that previously collided with this path will reappear in future manifest builds (the manifest re-checks reservations at build time).

Prefer toggling is_active: false via PUT if you want to temporarily disable enforcement while keeping the audit record.

Data model

ColumnTypeDescription
idbigintInternal primary key. Not exposed in the API — use hashKey (prefix rpa_).
platform_keyvarchar(50)Kebab-case consumer identifier, or * for universal.
pathvarchar(255)Reserved path.
match_typeenumprefix | exact.
descriptiontext nullAdmin notes.
is_activeboolSoft toggle for enforcement.
created_byvarchar nullHashkey of the user who created the entry (or 'system' for seeded rows).
updated_byvarchar nullHashkey of the user who last updated the entry.
created_at / updated_attimestampStandard Laravel timestamps.

Unique constraint: (platform_key, path).

Seeded baseline

On initial deploy of this feature, the ReservedPathSeeder populates the table with ShopHero-owned platforms. Re-run it on dev/staging after schema refreshes or after adding new routes to a platform:

php artisan db:seed --class=ReservedPathSeeder

The seeder is idempotent (updateOrCreate keyed on (platform_key, path)) and only touches ShopHero-owned platform_key values. Entries registered by platform admins for other platforms are not affected.

Changelog

Version history
  • 2026-04-16 — Initial release. Introduced as a refactor of the hardcoded VanityUrlService::RESERVED_PATH_PREFIXES constant. Entries for shophero-ecom-v2 and universal infrastructure paths seeded at schema-introduction time via a one-shot data migration; the ReservedPathSeeder keeps the list current going forward.

EngageHQ Public Content Delivery API