Dynamic Link Module — Admin
HTTP surface for managing dynamic link groups (CMS-style tile collections — e.g. homepage banner slots) and the dynamic links inside each group. Groups are addressable by slug and…
HTTP surface for managing dynamic link groups (CMS-style tile collections — e.g. homepage banner slots) and the dynamic links inside each group. Groups are addressable by slug and read by the public storefront; this admin surface is the only writer.
Source:
api-modules/dynamic-link/src/controllers/admin-dynamic-link-group.controller.ts,api-modules/dynamic-link/src/controllers/admin-dynamic-link.controller.ts.A group is a named collection of links rendered as an ordered list (carousel, tile grid, feature row). A link is one renderable item with optional image / url / text and an
orderfor sorting within its parent group.
Conventions
Authentication
All endpoints require a Better-Auth admin session and a role granting the matching permission.
| Endpoint group | Permission |
|---|---|
GET /admin/dynamic-link-groups, GET /admin/dynamic-link-groups/:id | dynamicLinkGroup: read |
POST /admin/dynamic-link-groups, POST /admin/dynamic-link-groups/:id/duplicate | dynamicLinkGroup: create |
PUT /admin/dynamic-link-groups/:id | dynamicLinkGroup: update |
DELETE /admin/dynamic-link-groups/:id | dynamicLinkGroup: delete |
GET /admin/dynamic-link-groups/:groupId/links | dynamicLink: read |
POST /admin/dynamic-link-groups/:groupId/links, POST /admin/dynamic-link-groups/:groupId/links/:linkId/duplicate | dynamicLink: create |
PUT /admin/dynamic-link-groups/:groupId/links/:linkId, PATCH /admin/dynamic-link-groups/:groupId/links/reorder | dynamicLink: update |
DELETE /admin/dynamic-link-groups/:groupId/links/:linkId | dynamicLink: delete |
Response envelope
Successful responses are wrapped by ResponseInterceptor:
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* optional, e.g. pagination */ }
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 409 | UNIQUE_VIOLATION (slug collision) |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Lifecycle
Groups and links are hard-deleted (no soft-delete). Deleting a group cascades to its links via the database foreign key.
Domain types
DynamicLinkGroupResponse
type DynamicLinkGroupResponse = {
id: string;
title: string; // 1..255
slug: string; // lowercase alnum + hyphens, 1..255
metadata: Record<string, unknown> | null;
createdAt: string; // ISO
updatedAt: string;
};
type DynamicLinkGroupWithLinksResponse = DynamicLinkGroupResponse & {
links: DynamicLinkResponse[]; // sorted by order ASC
};DynamicLinkResponse
A link must have at least one of image, url, or text — the rest can be null.
type DynamicLinkResponse = {
id: string;
groupId: string;
image: string | null; // max 2048 chars
url: string | null; // max 2048 chars
text: string | null; // max 1024 chars
order: number; // ASC sort within group, >= 0
metadata: Record<string, unknown> | null;
createdAt: string;
updatedAt: string;
};Dynamic link groups
Base path: /admin/dynamic-link-groups.
GET /admin/dynamic-link-groups — List groups
Required permission: dynamicLinkGroup: read. Standard QueryDto (page / limit / search / filters[]).
Response 200 — paginated envelope of DynamicLinkGroupResponse[].
GET /admin/dynamic-link-groups/:id — Get a group with its links
Required permission: dynamicLinkGroup: read. Returns the group with nested links[] sorted by order ASC.
Response 200 — DynamicLinkGroupWithLinksResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
POST /admin/dynamic-link-groups — Create a group
Required permission: dynamicLinkGroup: create.
Body
{
"title": "Homepage Hero Tiles",
"slug": "homepage-hero-tiles",
"metadata": { "platform": "WEB" }
}| Field | Type | Constraints |
|---|---|---|
title | string | 1..255 |
slug | string | 1..255, lowercase alnum + hyphens |
metadata | Record<string, unknown> | null? | Free-form |
Response 201 — DynamicLinkGroupResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod |
| 409 | UNIQUE_VIOLATION | slug already in use |
PUT /admin/dynamic-link-groups/:id — Update a group
Required permission: dynamicLinkGroup: update. Partial body.
Response 200 — DynamicLinkGroupResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
| 409 | UNIQUE_VIOLATION | slug taken by another group |
DELETE /admin/dynamic-link-groups/:id — Delete a group
Required permission: dynamicLinkGroup: delete. Hard delete; cascades to child links via DB FK.
Response 204 No Content.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
POST /admin/dynamic-link-groups/:id/duplicate — Duplicate a group
Required permission: dynamicLinkGroup: create. Clones the source group with a fresh title + slug. Source metadata is copied as-is; every child link is cloned with its order, image, url, text, and metadata.
Body
{ "title": "Homepage Hero Tiles (v2)", "slug": "homepage-hero-tiles-v2" }Response 201 — DynamicLinkGroupWithLinksResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod |
| 404 | NOT_FOUND | Source group not found |
| 409 | UNIQUE_VIOLATION | New slug taken |
Dynamic links
Base path: /admin/dynamic-link-groups/:groupId/links.
GET .../links — List links in a group
Required permission: dynamicLink: read. Returns the full ordered list (no pagination — link counts per group are small by design).
Response 200 — DynamicLinkResponse[] sorted by order ASC.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Group not found |
POST .../links — Create a link
Required permission: dynamicLink: create. At least one of image, url, text must be present (whitespace-only inputs are normalized to null before the check).
Body
{
"image": "tiles/promo-summer.png",
"url": "/sale/summer",
"text": "Summer Sale",
"order": 0,
"metadata": { "campaign": "Q2" }
}| Field | Type | Constraints |
|---|---|---|
image | string | null? | max 2048; trimmed; empty → null |
url | string | null? | max 2048 |
text | string | null? | max 1024 |
order | int | >= 0. Default 0 |
metadata | Record<string, unknown> | null? | Free-form |
Response 201 — DynamicLinkResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | All three of image/url/text are empty, or body otherwise invalid |
| 404 | NOT_FOUND | Group not found |
PUT .../links/:linkId — Update a link
Required permission: dynamicLink: update. Partial; the "at least one renderable" rule re-runs against the merged state.
Response 200 — DynamicLinkResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod |
| 404 | NOT_FOUND | Link not in this group |
DELETE .../links/:linkId — Delete a link
Required permission: dynamicLink: delete. Hard delete.
Response 204 No Content.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Link not in this group |
POST .../links/:linkId/duplicate — Duplicate a link
Required permission: dynamicLink: create. Clones the link within the same group with order = max(order) + 1.
Response 201 — cloned DynamicLinkResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Link not in this group |
PATCH .../links/reorder — Bulk reorder
Required permission: dynamicLink: update. Reapplies order for every supplied id. The literal route segment reorder is declared before :linkId so it cannot collide with a UUID.
Body
{
"items": [
{ "linkId": "01J9aaaa...", "order": 0 },
{ "linkId": "01J9bbbb...", "order": 1 },
{ "linkId": "01J9cccc...", "order": 2 }
]
}| Field | Type | Constraints |
|---|---|---|
items[].linkId | string | Required |
items[].order | int | >= 0 |
items | array | Min 1 entry |
Response 200 — reordered DynamicLinkResponse[] in their new order.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Empty items, missing linkId, etc. |
| 404 | NOT_FOUND | Group not found (links not in group are silently ignored at the service layer) |
Related modules
admin-rbac— gates every endpoint viadynamicLinkGroup:*/dynamicLink:*. Seeadmin-rbac.md.store/dynamic-link— unauthenticated storefront read of a group by slug.
Discount Module — Admin
HTTP surface for managing platform-wide discount coupons: percentage or fixed-amount discounts, with rich targeting (variants / categories / brands / tags / ingredients / vendors…
Frequently Bought Together Module — Admin
HTTP surface for operating the Frequently-Bought-Together (FBT) recommendation pipeline — an offline batch that mines product co-purchase pairs from confirmed orders and stores a…