Developer Docs

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

  1. Platform admins manage curated tags via SectionTagResolver (create, update, delete)
  2. Malet Owners assign tags during Malet creation or via the updateMaletSectionTags mutation
  3. Both paths pass through SectionTagService.validateAndRegister() โ€” valid slugs are auto-registered, malformed slugs are rejected
  4. Custom tags are created with category: "User" and description: "User-contributed tag"
  5. 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
  • isActive defaults to true

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:

  1. Tag Sync Events โ€” the malet_tags_updated TCP event indexes sectionTags changes to Meilisearch in real-time
  2. Search Tag Facets โ€” Visitors can filter Lobby results by tags (e.g., "Show me all black-owned Malets that offer delivery")
  3. Tags appear as selectable filters in the SearchFilters sidebar alongside existing facets like price range and item type