Skip to content

Categories / Taxonomy Audit

Run date: 2026-04-17. Source of truth: current main at commit d1a5411.

Findings

  • No categories table exists. Product classification is driven entirely by products.type, a VARCHAR column with a CHECK constraint pinned to three values: digital, membership, online_class (constraint defined in migration 010_add_product_type_fields.sql).
  • Backend treats type as a type-of-product, not a category. The service layer switches on ProductTypeDigital / ProductTypeMembership / ProductTypeOnlineClass for validation and per-type field handling (backend/internal/services/product.go and friends). This is a "fulfillment kind" dimension, not a subject/topic taxonomy.
  • Frontend has a hard-coded category chip filter that is already stale. frontend/src/pages/main/products/product-list.tsx and frontend/src/pages/dashboard/selling/my-products.tsx both ship a chip list with ['all','digital','membership','course']. The course chip matches no products because backend uses online_class, not course. The filter is therefore broken for anything other than all.
  • There is no subject/topic taxonomy at all. Nothing maps to Whop categories like "Trading", "Ecom", "Fitness", "Creator Tools". The /discover and marketplace pages rely on search only.
  • Category names are not yet in the DB, so SEO slugs (/categories/trading) don't exist. Whop-style category landing pages would need either path (A) extended type values or (B) a proper categories table.

Recommendation

Path B: a real categories table, but scoped tightly for v1 and introduced in Phase 2 (alongside the storefront handle work) rather than Phase 1.

Why: - Keeping products.type as a fulfillment dimension (digital / membership / online_class) is correct. It drives per-type UX. It is not what buyers browse by. - Whop-parity discovery needs subject-based browsing ("Trading", "Ecom", "AI & Automation", …). Stuffing those into products.type conflates two orthogonal dimensions and will have to be undone later. - A categories table with slug, name_id, name_en, optional icon, parent_id, sort_order lets us ship proper SEO-indexed category landing pages in Phase 2 for near-zero incremental cost. - Backfill: seed a starter taxonomy from the user (7–12 top-level categories) in the migration itself; add a product_categories join table so a product can be tagged with 1–3.

Scope for v1 introduction: - Single level (no nested subcategories); parent_id stays nullable for a v1.5 hierarchy later. - Seed list owned by admin; no UX for creators to invent new categories. - Update the broken frontend chip row to hit /categories endpoint, drop the hard-coded course chip entirely.

Fix that should ship regardless of path

Remove the broken 'course' chip from product-list.tsx and my-products.tsx now. It is already misleading on the live site. Small cleanup, zero risk. This is fair game to include in Phase 1 as part of the catalog polish.

Heads-up on migration numbering

The migrations plan in the master prompt assumes the next free number is 010. On the live main, migrations 010, 011, and 012 are already shipped (added during the batch-3 sprint for product type fields, community link, and product image remote_url respectively). The first new migration for Phase 1 will be 013; the whole Appendix C index should be re-numbered by +3.