Catalog-backed merch tables across three schema files:
packages/db/src/schema/merchTables.ts (including campaign satellite tables and audit log)packages/db/src/schema/merchDesignTables.tspackages/db/src/schema/merchCatalogTables.tspackages/db/src/schema/merchBenchmarkTables.ts| Table | Purpose |
|---|---|
merch_campaign | Core campaign identity and lifecycle (normalized — satellite tables hold branding, copy, etc.) |
merch_campaign_branding | Campaign branding (logos, colors, partner info) |
merch_campaign_studio | Studio/selfie capture config (headlines, captions, carousel slides) |
merch_campaign_legal | Legal text (terms, privacy, cookie content) |
merch_campaign_config | Shipping, payment, and misc settings |
merch_campaign_copy | Fan-facing copy (headlines, thank-you messages, email templates) |
merch_audit_log | Audit trail for campaign changes (changeSetId for grouping) |
catalog_product | Global product catalog rows (sku, type, base pricing, sizing, personalization requirements) |
catalog_product_image | Product images managed by catalog product |
catalog_renderer_config | Product renderer placement/background configuration |
shop_product | Campaign-specific attachment of a catalog product, with display/price/free overrides |
merch_product | Deprecated legacy product table retained only for migration compatibility |
merch_asset | S3 media assets (logo, hero, ai_template, overlay, backside_print) |
merch_session | Fan session (photo ref, cart ref, shipping, payment state) |
merch_selfie | Selfie image + face detection + demographics per session |
merch_item | Unified table representing cart items and order items (cart-side rows have order_id IS NULL, order-side rows have it set) |
merch_session_analytics | Session analytics sidecar (steps, milestones, generated assets) |
merch_session_generation | Per-(session, design, productType) generation cache (AI art key, candidates, cost); product type is derived from the selected catalog product |
merch_order | Completed orders (customer, shipping, payment, fulfillment) |
merch_design | Named design per campaign — supports a 3-level hierarchy (design → product variation → demographic variation) |
merch_design_config | Per-design AI config blob (prompt, assets, qualityTiers). productType = NULL = Level 1 default |
merch_ai_model | AI model registry (endpoints, costs, parameters) |
merch_share | Shareable result links |
merch_tracking_link | Campaign tracking links (UTM-style) |
merch_tracking_event | Individual tracking visit events |
merch_benchmark_session | Admin benchmark run metadata (prompts, assets, counters, cost) |
merch_benchmark_generation | One row per benchmark generation (selfie × prompt × model × params) |
merch_campaign_visit | IP-deduplicated campaign visits |
merch_seed_test | AI seed consistency tests |
merch_seed_test_review | Seed test review judgments |
19 enums in packages/db/src/schema/merchEnums.ts:
| Enum | Values |
|---|---|
merch_consent_type | implied, checkbox |
merch_activation_status | DRAFT, LIVE, ENDED |
merch_shutdown_mode | NONE, SOFT_CLOSE, EMERGENCY_CLOSE, ENDED |
merch_user_role | customer, tester, preview |
merch_order_mode | LIVE, PREVIEW |
merch_preview_type | DEMO, TEST |
merch_order_status | pending, processing, completed, failed, cancelled, new, received, working_on, ready, shipped, delivered |
merch_product_type | Deprecated legacy enum for old merch_product rows |
merch_asset_type | logo, hero, mockup, ai_template, backside_print, studio_carousel, overlay |
merch_payment_status | pending, paid, succeeded, failed, refunded |
merch_fulfillment_status | pending, processing, shipped, delivered, cancelled |
merch_face_error | no_face, multiple_faces, face_occluded, no_model |
merch_lighting_error | too_dark |
merch_ai_model_status | active, inactive, deprecated |
merch_speed_category | fast, medium, slow |
merch_progress_indicator_type | linear, spinner |
merch_source_type | selfie, upload |
merch_gender | female, male, not-distinctive |
merch_age_group | child, teen, 20s, 30s, 40s, elder |
The campaign table now holds core identity and lifecycle fields. Detail fields have been extracted into satellite tables:
Satellite tables (all FK to merch_campaign.id):
| Table | Fields |
|---|---|
merch_campaign_branding | partnerLogoAssetId, heroImageAssetId, primaryColor, secondaryColor |
merch_campaign_studio | studioHeadline, studioSelfieButtonLabel, studioUploadButtonLabel, studioBackLabel, studioSlide1-3ImageAssetId, studioSlide1-3Caption, studioVerificationHeadline/Subtext |
merch_campaign_legal | legalFooterConsentType, terms/privacy/cookie title+content+updatedAt |
merch_campaign_config | allowedCountries, currency, shippingFlatRate, shippingIntlRate, paymentMethods, mobileProductGrid, plaqueConfig, fanLocationText |
merch_campaign_copy | desktopTitle, cameraPermissionText, progressIndicatorType, thankYouMessage, successHeadline, emailSubjectLine, shareTextTemplate, supportEmail |
Note: Generation-related fields (
aiConfig,itemGenerationConfigs,upscaleConfig) have been removed frommerch_campaign_config. All AI generation settings now live inmerch_design_config.configJson.
Note: Selfie image data (selfieKey, sourceType, faceDetected, faceError, lightingError, gender, ageGroup, peopleCount) and cart items (cartItems JSON) have been extracted into dedicated
merch_selfieandmerch_item(originallymerch_cart_item) tables. Legacy columns remain onmerch_sessionfor backwards compatibility.campaignIdis nullable to support multishop journey sessions where the session is created before campaign selection.
Extracted selfie record, created on every upload/capture. The session's activeSelfieId points to the selfie currently used for generation.
selfie | upload)verifyImageWithGemini): gender, ageGroup, peopleCountUnified table representing cart items and order items (cart-side rows have order_id IS NULL, order-side rows have it set). Replaces the old merch_session.cartItems JSON array and the deprecated merch_cart_item / merch_order_item tables.
Named design belonging to a campaign. Supports a 3-level hierarchy via the self-referencing parentDesignId FK:
| Level | parentDesignId | gender/ageGroup | Purpose |
|---|---|---|---|
| Level 1 — Design | NULL | NULL | Top-level design with default generation settings for all products |
| Level 2 — Product Variation | points to Level 1 | NULL | Overrides template image, prompt, and AI model for a specific catalog product |
| Level 3 — Demographic Variation | points to Level 2 | set | Overrides template image, prompt, and AI model for a specific gender/age within a product variation |
merch_gender), ageGroup (merch_age_group)Resolution logic: During generation, resolveDesignConfig3Level walks the hierarchy:
productType value (the productType column on Level 1 configs is ignored; whatever config exists is treated as the universal default)Child configs only need to specify overridden fields — empty fields inherit from the parent level.
Legacy compatibility: Direct demographic sub-designs (Level 2 with gender/ageGroup, no intermediate product variation) are still supported as a fallback path.
AI generation config blob for a design at any level.
NULL = default config applying to all product types)Level 1 designs have a single config row containing the full generation settings (template image, prompt, AI model, quality tiers, fan location text, overlay, back print). The productType value on this row is ignored at runtime — whatever config exists is treated as the universal default for all products. Legacy data may have a specific productType value (e.g. "tshirt") rather than NULL; this is harmless since the resolver ignores it.
Level 2/3 designs have a config row containing only override fields (template image, prompt, AI model). During resolution these are merged on top of the parent's config.
Unique index: (design_id, product_type) — enforced via manual upsert because PostgreSQL treats NULL as distinct in unique indexes.
Caches AI generation output keyed by (sessionId, designId, productType). Enables deduplication and avoids re-generation for the same selfie+design+product combination.
productType is retained in the cache key for fallback-chain compatibility; runtime product/design association uses the selected catalog product ID.
merch_campaign_config.currency)Validated against GarmentDesignConfigSchema or PlaqueDesignConfigSchema (Zod) in packages/merch/srv/src/design-config-schemas.ts.
Garment example (tshirt / hoodie — Level 1 full config):
{
"templateImageUrl": "https://s3.../merch/template.png",
"templateImageAssetId": "asset-abc123",
"prompt": "A vivid portrait of {fanName} as a superhero...",
"modelEndpoint": "fal-ai/gpt-image-1.5/edit",
"qualityTiers": ["low", "medium", "high"],
"fanLocationText": "person on the left",
"overlayImageUrl": "https://s3.../overlay.png",
"backPrintImageUrl": "https://s3.../backprint.png"
}
Level 2/3 override example (only overridden fields):
{
"templateImageUrl": "https://s3.../hoodie-specific-template.png",
"prompt": "Same style but optimized for hoodie layout..."
}
Note:
sourceImageUrl/sourceImageAssetIdare deprecated aliases fortemplateImageUrl/templateImageAssetId. The runtime readstemplateImageUrl ?? sourceImageUrlfor backward compatibility.
Plaque example:
{
"prompt": "Create a platinum vinyl record artwork...",
"qualityTiers": ["low", "low"]
}
qualityTiers is an optional array of "low" | "medium" | "high" values. Each entry produces one generation attempt at that quality level. Examples:
["low", "medium", "high"] → 3 attempts (one at each tier) — fans choose from all candidates["low", "low"] → 2 low-quality attemptsDEFAULT_QUALITY_TIERS = ["low"] (single low-quality generation)[
{ "artKey": "merch/ai-candidates/sess-1-best-1234.png", "url": "https://...", "score": 0.92 },
{ "artKey": "merch/ai-candidates/sess-2-alt-1234.png", "url": "https://...", "score": 0.85 },
{ "artKey": "merch/ai-candidates/sess-3-alt-1234.png", "url": "https://...", "score": 0.71 }
]
Sorted by score (character consistency likeness, 0–1). The fan selects their preferred candidate via POST /api/merch/ai/select.
{
"email": "fan@example.com",
"firstName": "John",
"lastName": "Doe",
"addressLine1": "123 Main St",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
{
"vinyl": { "x": 200, "y": 200, "size": 280 },
"prompt": "Create a platinum vinyl record artwork.",
"fanName": "MARK THOMPSON",
"standardPricing": { "price": 299, "shipping": 35 },
"deluxePricing": { "price": 539, "shipping": 35 },
"standardImages": { "basePlaqueUrl": "...", "recordFrameUrl": "...", "artistRefUrl": "..." },
"deluxeImages": { "basePlaqueUrl": "...", "recordFrameUrl": "...", "artistRefUrl": "..." }
}
13 access files in packages/db/src/access/merch/:
| File | Functions |
|---|---|
merch-campaign.ts | getMerchCampaignBySlug, getMerchCampaignById, listActiveMerchCampaigns, getMerchCampaignLifecycle |
merch-catalog.ts | create/list/update catalog products, product images, renderer config |
shop-product.ts | attach/list/update/reorder catalog products for a campaign |
merch-asset.ts | getMerchAssetById, getMerchAssetsByIds, getMerchAssetByKey, createMerchAsset |
merch-session.ts | createMerchSession, getMerchSessionById, updateMerchSession |
merch-selfie.ts | createMerchSelfie, getMerchSelfieById, listMerchSelfiesBySession, getActiveSelfie, updateMerchSelfie |
merch-item.ts | addItemToCart, replaceCartItems, listInCartItems, removeItem, clearCartItems, updateItemQuantity, updateItemImages, attachCartItemSnapshotToOrder, listMerchOrderItemsByOrder |
merch-session-analytics.ts | createMerchSessionAnalytics, getMerchAnalyticsBySessionId, updateMerchAnalyticsStep, updateMerchAnalyticsMilestone, appendMerchGeneratedAsset |
merch-order.ts | createMerchOrder, getMerchOrderById, getMerchOrderBySession, updateMerchOrderStatus |
merch-share.ts | createMerchShare, getMerchShareById |
merch-tracking.ts | getMerchTrackingLinkByCampaignSlug, incrementMerchTrackingVisit, recordMerchTrackingEvent |
merch-campaign-visit.ts | recordMerchCampaignVisit |
merch-ai-model.ts | getActiveMerchAIModels, getMerchAIModelByEndpoint |
Design access in merch-design.ts (also in access/merch/):
| Function | Description |
|---|---|
listDesignsByCampaign | Top-level designs only (parentDesignId IS NULL) |
listDesignsByCatalogProductId | Top-level designs for a specific catalog product (parentDesignId IS NULL) |
listAllDesignsByCampaign | All designs including sub-designs |
getDesignById | Single design by ID |
createDesign | Insert a new design row |
updateDesign | Update design fields |
listSubDesigns(parentDesignId) | Sub-designs for a parent design |
resolveDesignConfig3Level(designId, productType, demographics, fallbackChain, catalogProductId) | Primary resolver — walks the 3-level hierarchy. Level 1 loads the first config from the top-level design (ignores productType on the row — universal default). Level 2/3 merge overrides on top. Returns { effectiveDesignId, configJson } |
resolveDesignForDemographics(parentDesignId, demographics) | Legacy: find best-matching demographic sub-design |
resolveDesignConfigWithFallback(effectiveDesignId, parentDesignId, productType, fallbackChain) | Legacy: resolve config with parent fallback |
resolveDesignConfigRow(designId, productType, fallbackChain) | Resolve config for a design with product-type fallback chain |
upsertDesignConfig | Create-or-update a design config row |
All exported from packages/db/src/index.ts.
merch_campaign(slug, talentName), merch_tracking_link(campaignId, slug), merch_campaign_visit(campaignId, ipAddress), merch_ai_model(endpointId), merch_session_analytics(sessionId, campaignId)catalog_product(sku) uniqueshop_product(campaign_id, catalog_product_id) uniquemerch_design_config(design_id, product_type) — enforced via manual upsert (NULL product_type = default config)merch_design(parent_design_id, gender, age_group) NULLS NOT DISTINCT WHERE parent_design_id IS NOT NULL (prevents duplicate variations)merch_design(parent_design_id)merch_session_generation(session_id, design_id, product_type) uniquemerch_selfie(session_id), merch_selfie(selfie_key)merch_cart_item(session_id), merch_cart_item(selfie_id), merch_cart_item(product_id)merch_campaign(deletedAt) — queries filter deletedAt IS NULLmerch_campaign_visit uses onConflictDoNothing() for upsertSET visitCount = visitCount + 1| Migration | Description |
|---|---|
0044_add_design_tables.sql | Initial merch_design, merch_design_config, merch_session_generation tables |
0045_stable_product_ids.sql | Legacy unique index on merch_product(campaign_id, type) |
0046_merch_session_demographics.sql | merch_gender / merch_age_group enums; gender, age_group, people_count on merch_session |
0047_extract_selfie_cart_item.sql | merch_selfie table, merch_cart_item table, active_selfie_id on session, selfie_id on generation/order_item |
0048_design_demographics.sql | parent_design_id, gender, age_group on merch_design + sub-design unique index |
0049_merch_campaign_normalization.sql | Normalizes campaign into satellite tables (branding, studio, legal, config, copy) |
0050_merch_audit_log.sql | Audit log table for campaign changes |
0051_remove_generation_from_campaign.sql | Removes deprecated ai_config, itemGenerationConfigs, upscaleConfig from merch_campaign_config |
0052_design_default_config.sql | Creates productType = NULL default configs for top-level designs (Level 1 hierarchy support) |
Data migrations run automatically on deploy via packages/db/src/migrate.ts → runDataMigrations(). The registry is in packages/db/src/data-migrations/registry.ts.
| Data Migration | Description |
|---|---|
0001-migrate-designs | Back-fills merch_design rows from legacy campaign JSON |
0002-extract-selfies-and-cart-items | Migrates selfie/cart data from session columns into merch_selfie / merch_cart_item rows |
| Content | S3 Key Pattern |
|---|---|
| Selfies | merch/selfie/{filename} |
| AI art candidates | merch/ai-candidates/{sessionId}-{attempt}-{timestamp}.png |
| Final AI art | merch/ai-generated/{sessionId}-{timestamp}.png |
| Background masks | merch/bg-masks/mask-{sessionId}-{timestamp}.png |
| Catalog renders | merch/renders/render-{sessionId}-{catalogProductId}-{preview|clean}-{timestamp}.webp |
| Mockups / plaque illustrations | Legacy paths retained for older endpoints |
On This Page
Tables OverviewEnumsKey Table Detailsmerch_campaign (normalized)merch_sessionmerch_selfiemerch_itemmerch_designmerch_design_configmerch_session_generationmerch_orderJSON Column ShapesconfigJson (merch_design_config)aiArtCandidates (merch_session / merch_session_generation)shippingInfo (merch_session)plaqueConfig (merch_campaign)Access LayerIndexesMigrationsS3 Key Patterns