Developer Docs

Vertical Seeding Infrastructure

The vertical seeding system creates realistic, production-representative test data across all 11 Ngwenya Verticals with strict multi-tenant isolation. Each Malet is owned by a specific user or Organization, and every seeded entity is scoped to exactly one Malet.

NOTE

This page covers the vertical-specific seeding (11 malets, products, services). For the full 22-archetype platform seed including auth users, social graph, commerce, and enterprise features, see Seed Infrastructure.

NOTE

This infrastructure lives in scripts/db/verticals/ in the ngwenya-federation repository. All 11 verticals seed in ~1 second on an M-series Mac.


Architecture

Two-Plane Approach

The seed system mirrors the platform's own architectural split:

Plane Method Used For
Control Plane GraphQL mutations via Gateway Malets, Products, Services โ€” entities with business-logic validators (slug generation, @Max() constraints, etc.)
Feature Plane Direct MongoDB inserts via Mongoose Events, Exhibitions, Galleries, Membership Plans โ€” experiences subgraph entities without public create mutations

This split is necessary because the experiences subgraph exposes read-only queries for most entities. The seed scripts bypass the resolver layer to populate data that would normally be created by internal admin or backend workflows.

Shared Utilities

All 11 vertical scripts import from a common shared/ directory to eliminate duplication:

scripts/db/verticals/
โ”œโ”€โ”€ shared/
โ”‚   โ”œโ”€โ”€ config.ts          # GATEWAY_URL, MONGODB_URI, POSTGRES_URI, TOKEN
โ”‚   โ”œโ”€โ”€ gql-client.ts      # GraphQL client with user impersonation
โ”‚   โ”œโ”€โ”€ mongo.ts           # Lazy Mongoose connection factory
โ”‚   โ”œโ”€โ”€ ids.ts             # oid(), daysAgo(), daysFromNow(), img()
โ”‚   โ””โ”€โ”€ users.ts           # 22 users, 2 orgs, 11 malet definitions
โ”œโ”€โ”€ seed-full.ts           # Master orchestrator (all phases)
โ”œโ”€โ”€ seed-all.ts            # Verticals-only orchestrator (3 phases)
โ”œโ”€โ”€ seed-users.ts          # Auth users (Postgres)
โ”œโ”€โ”€ seed-profiles.ts       # User profiles (MongoDB)
โ”œโ”€โ”€ seed-social.ts         # Follows, wishlists, likes
โ”œโ”€โ”€ seed-commerce.ts       # Orders, carts, reviews
โ”œโ”€โ”€ seed-community.ts      # Discussions, comments, posts
โ”œโ”€โ”€ seed-enterprise.ts     # SAML, SCIM, roles, SIEM
โ”œโ”€โ”€ clean.ts               # Drops all seed collections
โ””โ”€โ”€ seed-{vertical}.ts     # One per vertical (11 total)

Key Design: `ensureMalet()`

The shared gql-client.ts exposes an ensureMalet() function that handles idempotent Malet creation:

const { id, handle } = await ensureMalet({
  handle: 'fashion-demo',
  name: "Amara's Boutique",
  tagline: 'Curated African fashion',
  description: '...',
  verticalType: 'fashion',
  ownerType: 'USER',
  ownerId: USER_SOLO_FASHION.id,
  sectionTags: ['luxury', 'handmade'],
}, USER_SOLO_FASHION.id);

Internally, ensureMalet():

  1. Queries by handle โ€” if found, returns existing Malet (idempotent)
  2. Creates via createOneMalet mutation โ€” only passes GQL-valid fields
  3. Patches ownerType/ownerId directly via MongoDB (not on MaletInput DTO)

User Simulation

The seed system simulates real-world multi-tenancy with 22 deterministic users, 2 Organizations, and both solo and team-based ownership patterns. See Seed Infrastructure for the full 22-archetype matrix.

Solo Malet Owners (Starter Tier)

Individual Malet Owners who own exactly one Malet:

Handle Name Malet Vertical
amara-styles Amara Ndlovu fashion-demo Fashion
zuri-writes Zuri Okafor author-demo Author
lens-maestro Kagiso Molefe photo-demo Photographer
lerato-wellness Lerato Khumalo serenity-spa-demo Wellness
lindiwe-advisory Lindiwe Dlamini advisory-demo Professional

Organization-Owned Malets

Two Organizations with team structures and role-based access:

AfriArts Collective โ€” A creative agency operating 4 Malets:

Handle Name Role Responsibility
thabo-mgmt Thabo Mthembu OWNER Overall management
naledi-ops Naledi Kgosi MEMBER Kitchen operations
sipho-trails Sipho Dlamini MEMBER Tour guide
zanele-events Zanele Maseko ADMIN Team management
bongani-viewer Bongani Nkosi VIEWER Read-only analytics

Teams: "Events & Culture" (Thabo, Zanele), "Food & Tours" (Naledi, Sipho, Bongani)

Circuit Board Inc. โ€” Enterprise-tier consumer electronics with SAML/SCIM:

Handle Name Role
kwame-tech Kwame Asante OWNER
ayanda-dev Ayanda Moyo MEMBER
fatima-editor Fatima Hassan MEMBER (AUTHOR_EDITOR)

Vertical Breakdown

Malet Registry

Handle Name Vertical Owner
fashion-demo Amara's Boutique fashion Amara Ndlovu (USER)
author-demo Zuri's Tech Journal author Zuri Okafor (USER)
photo-demo Lens & Light Studio photographer Kagiso Molefe (USER)
serenity-spa-demo Serenity Wellness Spa wellness Lerato Khumalo (USER)
advisory-demo Apex Financial Advisory professional Lindiwe Dlamini (USER)
venue-demo The Grand Venue entertainment AfriArts Collective (ORG)
gallery-demo The Zeitz Collection culture AfriArts Collective (ORG)
tour-demo Ubuntu Safari Co. tour AfriArts Collective (ORG)
restaurant-demo Umami Kitchen restaurant AfriArts Collective (ORG)
tech-store-demo Circuit Board Tech tech Circuit Board Inc. (ORG)
pixel-vault-demo Pixel Vault Games arcade Circuit Board Inc. (ORG)

Seeded Entities Per Vertical

Vertical MongoDB Collections Key Data
Fashion products, lookbooks, productcollections 4 products (size charts, variants), 1 lookbook (3 outfits), 3 collections
Entertainment events, creditwallets, loyaltyconfigs, visitorloyalties 3 events (tiered pricing), credit wallet, 4-tier loyalty + leaderboard
Professional clientengagements, vaultdocuments 4 engagements (lifecycle notes), 7 categorized documents
Wellness wellnessprofiles, membershipplans, membershipsubscriptions 1 profile, 4 plans (Bronzeโ†’Platinum), 6 subscriptions
Culture artistprofiles, exhibitions 4 South African artists, 4 exhibitions (11 artworks)
Tech products, productspecs, warrantyregistrations, warrantyclaims 3 products (8 variants), 3 spec sheets, 3 warranties, 3 claims
Arcade gameprofiles, digitalvaults 4 games (system reqs), 6 key vaults (63 keys)
Tour services 3 multi-day itineraries (5โ€“7 days, detailed stops)
Restaurant kitchenorders 9 kitchen orders (SA cuisine, various statuses)
Photographer galleries, proofingsessions 4 galleries (110 photos), 3 proofing sessions
Author subscriptions, blogs 9 subscriptions, 7 blog posts (free + gated)

Execution

Full Seed

# Seed all 11 verticals (3 phases, ~1 second)
make seed-verticals

# Full platform seed (all 22 archetypes, ~5 seconds)
make seed-full

# Or run the orchestrator directly
npx ts-node scripts/db/verticals/seed-all.ts

The orchestrator runs in dependency order:

Phase 1: Organizations  โ†’ AfriArts Collective, Circuit Board Inc.
Phase 2: Solo Malets    โ†’ fashion, author, photographer, wellness, professional
Phase 3: Org Malets     โ†’ entertainment, culture, tour, restaurant, tech, arcade

Individual Verticals

make seed-fashion           # Amara's Boutique
make seed-entertainment     # The Grand Venue
make seed-wellness          # Serenity Wellness Spa
make seed-orgs              # Organizations only
# ... etc (one target per vertical)

Clean Slate

# Drop 38 seed-related MongoDB collections
make seed-clean

# Or via the standalone script (no container CLI needed)
npx ts-node scripts/db/verticals/clean.ts

# Wipe + re-seed
make seed-clean && make seed-verticals

Environment Variables

Variable Default Description
GATEWAY_URL http://localhost:30000/graphql Hive Gateway endpoint
MONGODB_URI mongodb://localhost:27017/ngwenya MongoDB connection string
TOKEN Dev session token Auth header for GQL mutations

Frontend Catalog Integration

The catalog page ([malet]/catalog/+page.ts) uses a parallel fetch + graceful fallback strategy to display vertical-specific entities alongside core Products, Services, and Articles.

Data Loading Pattern

// Core data โ€” always fetched
const [products, services, blogs] = await Promise.all([
  client.request(GET_MALET_PRODUCTS, { maletId }),
  client.request(GET_MALET_SERVICES, { maletId }),
  client.request(GET_MALET_BLOGS, { maletId }),
]);

// Vertical-specific โ€” wrapped in safeFetch()
const [events, exhibitions, plans] = await Promise.all([
  safeFetch(client.request(GET_UPCOMING_EVENTS, { maletId }), { upcomingEvents: [] }),
  safeFetch(client.request(GET_ACTIVE_EXHIBITIONS, { maletId }), { activeExhibitions: [] }),
  safeFetch(client.request(GET_MEMBERSHIP_PLANS, { maletId }), { membershipPlans: [] }),
]);

`safeFetch()` Pattern

The safeFetch() wrapper ensures the catalog page never crashes when a subgraph query doesn't match the current Malet's vertical. For example, a Fashion Malet has no Events โ€” the upcomingEvents query returns [] via graceful fallback rather than throwing.

async function safeFetch<T>(promise: Promise<T>, fallback: T): Promise<T> {
  try { return await promise; } catch { return fallback; }
}

Dynamic Sections

The catalog template conditionally renders sections based on what data is available:

Section Query Renders When
Products GET_MALET_PRODUCTS products.length > 0
Services GET_MALET_SERVICES services.length > 0
Events GET_UPCOMING_EVENTS events.length > 0
Exhibitions GET_ACTIVE_EXHIBITIONS exhibitions.length > 0
Membership Plans GET_MEMBERSHIP_PLANS membershipPlans.length > 0
Articles GET_MALET_BLOGS blogs.length > 0

Idempotency

All scripts are safe to re-run:

  • Malet creation: ensureMalet() checks for existing handle first
  • Product creation: ensureProduct() checks for existing slug+maletId before creating (idempotent)
  • MongoDB inserts: wrapped in try/catch that skips E11000 duplicate key errors
  • Deterministic IDs: User UUIDs are hardcoded in shared/users.ts
  • Meilisearch reindex: seed-full.ts Phase 10 automatically reindexes (non-fatal if Meilisearch is unavailable)

For a completely clean state, run make seed-clean before seeding.


Common Pitfalls

Issue Cause Fix
Field "ownerType" not defined by MaletInput ownerType/ownerId not on GQL DTO ensureMalet() patches via MongoDB after creation
Tour services fail with "Bad Request" Duration exceeds @Max(1440) validator Multi-day tours use direct MongoDB insert
SizeChart entries fail Float validation Measurements must be numeric (82) not strings ("80-84cm") Use midpoint values in cm
Membership plans show no price Backend entity uses monthlyPrice, not pricePerMonth Match field names exactly