Tag Registry โ Developer Guide
Overview
The Tag Registry is a centralized system for managing the sectionTags that Malet Owners can assign to their Malets. The registry uses a suggest-and-extend model: curated platform tags serve as suggestions, but Malet Owners can also create their own custom tags during Malet creation or tag updates. Custom tags are auto-registered into the registry when they pass slug format validation.
This ensures tags are always properly formatted and discoverable, while giving Malet Owners the flexibility to describe their business in their own terms.
| Concept | Description |
|---|---|
| SectionTag | A canonical tag record in the registry (e.g., slug: "organic", displayName: "Organic", category: "Lifestyle") |
| Registry | The SectionTag collection in the malets subgraph โ the single source of truth for all tags |
| Open Validation | Tags that pass slug format validation are auto-registered in the registry โ unknown-but-valid tags are accepted, only malformed slugs are rejected |
| Auto-Seeding | 19 curated defaults are seeded on first boot if the collection is empty |
| User Tags | Custom tags added by Malet Owners are auto-registered with category: "User" |
Architecture
graph TD
A["SectionTagResolver"] --> B["SectionTagService"]
C["MaletCRUDResolver"] --> B
D["MaletQueryResolver"] --> B
B --> E["SectionTag Collection (MongoDB)"]
B --> F["Malet Collection (MongoDB)"]
G["Auto-Seed on Boot"] --> B
style E fill:#3b82f6,color:#fff
style F fill:#3b82f6,color:#fff
style G fill:#f59e0b,color:#fff
Data Flow
- Platform admins manage curated tags via
SectionTagResolver(create, update, delete) - Malet Owners assign tags during Malet creation or via the
updateMaletSectionTagsmutation - Both paths pass through
SectionTagService.validateAndRegister()โ valid slugs are auto-registered, malformed slugs are rejected - Custom tags are created with
category: "User"anddescription: "User-contributed tag" - Visitors discover Malets by their tags in the Lobby (future: filterable facets in search)
Default Tag Set
The registry ships with 19 curated defaults across 4 categories, seeded automatically on first boot:
| Category | Tags |
|---|---|
| Lifestyle | organic ยท luxury ยท handmade ยท eco-friendly ยท artisan ยท vintage ยท local ยท minimalist |
| Commerce | delivery ยท pickup ยท wholesale ยท subscription ยท made-to-order |
| Identity | black-owned ยท women-owned ยท veteran-owned |
| Experience | by-appointment ยท walk-in ยท virtual |
| User | (auto-populated by Malet Owner custom tags) |
NOTE
Seeding only runs when the SectionTag collection is empty. Adding new defaults requires updating the DEFAULT_TAGS array in SectionTagService and clearing the collection, or using the createSectionTag mutation.
Entity Schema
SectionTag
| Field | Type | Description |
|---|---|---|
id |
ID! |
Auto-generated unique identifier |
slug |
String! |
URL-friendly unique identifier (e.g., eco-friendly) |
displayName |
String! |
Human-readable label (e.g., "Eco-Friendly") |
description |
String |
Brief explanation of the tag's meaning |
category |
String |
Grouping category (Lifestyle, Commerce, Identity, Experience) |
isActive |
Boolean! |
Whether the tag can be assigned to Malets (default: true) |
createdAt |
DateTime! |
Creation timestamp |
updatedAt |
DateTime! |
Last update timestamp |
Indexes:
{ slug: 1 }โ Unique index for fast lookups and duplicate prevention
Slug Format:
- Must match
^[a-z0-9]+(?:-[a-z0-9]+)*$ - Lowercase alphanumeric characters with hyphens only
- Examples:
organic,eco-friendly,made-to-order
GraphQL API
Public Queries
These queries are available to all users โ no authentication required. They power tag discovery in the Lobby and Malet setup flows.
List All Tags
query {
sectionTags(filter: { category: "Lifestyle", isActive: true, search: "eco" }) {
id
slug
displayName
description
category
isActive
}
}
| Filter Field | Type | Description |
|---|---|---|
category |
String |
Exact match on category name |
isActive |
Boolean |
Filter by active/inactive status |
search |
String |
Case-insensitive regex search on slug and displayName |
Results are sorted by category (ascending), then slug (ascending).
Get Single Tag
query {
sectionTag(slug: "organic") {
id
slug
displayName
description
category
isActive
}
}
Returns null for unknown slugs (no error thrown).
Admin Mutations
All mutations require the MANAGE_TAGS permission โ only platform administrators have this by default. See Custom RBAC for delegating permissions.
Create Tag
mutation {
createSectionTag(
input: {
slug: "fair-trade"
displayName: "Fair Trade"
description: "Certified fair trade sourcing"
category: "Lifestyle"
}
) {
id
slug
displayName
category
isActive
}
}
Validation:
- Slug must match the format
^[a-z0-9]+(?:-[a-z0-9]+)*$ - Slug must be unique โ duplicate slugs return a
ConflictException isActivedefaults totrue
Update Tag
mutation UpdateTag($id: ID!, $input: UpdateSectionTagInput!) {
updateSectionTag(id: $id, input: $input) {
id
displayName
description
isActive
}
}
Variables:
{
"id": "tag-abc123",
"input": {
"displayName": "Updated Display Name",
"description": "New description",
"isActive": false
}
}
Partial updates are supported โ only include the fields you want to change. slug cannot be changed after creation.
Delete Tag
mutation {
deleteSectionTag(id: "tag-abc123")
}
Returns true on success.
WARNING
A tag cannot be deleted if any Malet is currently using it. You must first remove the tag from all Malets via updateMaletSectionTags, then delete it. The error message includes the count of Malets using the tag.
Malet Integration
Assigning Tags on Creation
When creating a Malet, include sectionTags in the input. All tags are validated against the registry:
mutation CreateMalet($input: CreateOneMaletInput!) {
createOneMalet(input: $input) {
id
name
sectionTags
}
}
Variables:
{
"input": {
"malet": {
"name": "Luminara Crafts",
"sectionTags": ["handmade", "eco-friendly", "artisan"]
}
}
}
If any tag has a malformed slug format (not lowercase alphanumeric with hyphens), the mutation rejects those specific tags:
BadRequestException: Invalid tag format: MY TAG, 123$%.
Tags must be lowercase alphanumeric with hyphens (e.g., "eco-friendly").
Tags that pass format validation but don't exist in the registry are auto-registered as user-contributed tags. There is no need to pre-create tags before assigning them.
Updating Tags
Malet Owners can update their tags via a dedicated mutation (owner-only):
mutation UpdateTags($id: ID!, $tags: [String!]!) {
updateMaletSectionTags(id: $id, tags: $tags) {
id
sectionTags
}
}
This is a whole-array replace โ pass the complete list of desired tags. To clear all tags, pass an empty array [].
Validation rules:
- The caller must be the Malet Owner
- All tags must pass slug format validation (
^[a-z0-9]+(?:-[a-z0-9]+)*$, max 40 chars) - Unknown-but-valid tags are auto-registered
- Empty arrays are allowed (clears all tags)
Permission Model
| Action | Required Permission | Who Has It |
|---|---|---|
| List/query tags | None (public) | Everyone |
| Create/update/delete tags | MANAGE_TAGS |
Platform Admins |
| Assign tags to own Malet | Malet ownership | Malet Owner |
The MANAGE_TAGS permission is part of the platform's action-based permission system. It can be granted to Organization members via custom roles if needed.
Module Structure
apps/malets/src/section-tag/
โโโ section-tag.entity.ts # SectionTag Typegoose entity
โโโ section-tag.service.ts # CRUD + seed + validate
โโโ section-tag.service.spec.ts # 16 unit tests
โโโ section-tag.resolver.ts # GraphQL queries + mutations
โโโ section-tag.resolver.spec.ts # 5 unit tests
โโโ section-tag.module.ts # NestJS module
โโโ dto/
โโโ section-tag.dto.ts # Create/Update/Filter DTOs
Error Handling
| Scenario | Error Type | Message |
|---|---|---|
| Invalid slug format | BadRequestException |
"Slug must be lowercase alphanumeric with hyphens" |
| Duplicate slug | ConflictException |
"Tag with slug "fair-trade" already exists" |
| Tag not found (update/delete) | NotFoundException |
"Section tag not found: tag-abc123" |
| Delete tag in use | ConflictException |
"Cannot delete tag "organic" โ it is used by 3 Malet(s)" |
| Malformed tags on Malet create | BadRequestException |
"Invalid tag format: MY TAG. Tags must be lowercase alphanumeric with hyphens." |
| Non-owner updating Malet tags | UnauthorizedException |
"Only the Malet Owner can update section tags" |
Testing
Unit Tests
# Run all malets unit tests (123 tests across 8 suites)
npm run test -- apps/malets --no-coverage
Key coverage areas:
- SectionTagService: CRUD lifecycle, slug validation, duplicate detection, seed logic, delete-in-use protection,
validateAndRegister()auto-creation, race-condition safety - SectionTagResolver: Query delegation, mutation authorization, error propagation
E2E Tests
# Run section-tag E2E tests (16 tests)
npx jest --config apps/malets/test/jest-e2e.json --testPathPattern="section-tag" --detectOpenHandles
Covers: Full CRUD lifecycle, Malet creation with valid/invalid tags, updateMaletSectionTags with validation, tag clearing, and delete-in-use protection.
Search Facets Integration
The Tag Registry is fully integrated with the Search Engine as filterable facets. When sectionTags change:
- Tag Sync Events โ the
malet_tags_updatedTCP event indexessectionTagschanges to Meilisearch in real-time - Search Tag Facets โ Visitors can filter Lobby results by tags (e.g., "Show me all
black-ownedMalets that offerdelivery") - Tags appear as selectable filters in the
SearchFilterssidebar alongside existing facets like price range and item type
Related
- Organization & Malet Management โ Frontend management UI where Malet Owners configure their Malets
- Search Engine Administration โ Admin tools for search index management โ tag facets and real-time sync
- Universal Search Index โ Blog posts, Malets, and guides as searchable entities โ uses
sectionTagsfrom the registry - Custom RBAC โ The
MANAGE_TAGSpermission can be delegated to Organization members via custom roles - Universal Search Guide โ Frontend search integration that surfaces tag-based filtering
- Entertainment & Experiences โ Entertainment Malets use section tags for Lobby discoverability