Guided discovery

Flavor Wizard

Tag-driven product matching for a liquor and smoke shop

Next.js 16React 19TypeScriptSupabaseTailwind 4

A guided discovery surface on the Beaver Trap storefront. No vector search, no LLM, just a carefully shaped tag system over Supabase Postgres and a small API route that filters, ranks, and returns matches the owner can audit and edit.

Technical narrative

This is where the archive goes deeper than the homepage: architecture choices, operating constraints, and the decisions that make the product maintainable over time.

The problem

The shop carries hundreds of products across dozens of categories. A flat catalog crushes casual customers who know what they want to feel but not what to type into a search bar. The feature needed to meet them where they are, using favorite flavors, an occasion, and a rough price ceiling, without asking them to learn liquor taxonomy first.

Four modes, one flow

The wizard runs the same pipeline with four entry points, chosen by how specific the customer already is.

  • Flavor mode: pick flavor tags (smoky, vanilla, spicy…) scoped to a department; match against product_tags.
  • Direct mode: skip tags; show everything for a chosen subtype (e.g. all Bourbons).
  • Type-Direct: broader still; all products for a type (e.g. all Whiskey).
  • Preference lanes: cross-category occasions (e.g. “Dessert spirits”) that ignore strict hierarchy.

Mobile read

Start with intent

The flow asks what the shopper is trying to buy before filtering by product taxonomy.

Narrow in steps

Taste, occasion, and budget reduce the catalog without dumping users into a giant grid.

End with confidence

Recommendations explain why they match, which helps the handoff feel trustworthy.

Departmentspirits / wine / beer / rtdTypewhiskey, gin, tequila…Flavor modeselect flavor tags → matchDirect modeskip tags, show subtypeType-Directall products for typePreference lanesoccasions, cross-categoryPOST /api/wizardroute.tsRanked productsprice · stock · image
Scroll the detail view if you want the full map. Department and type narrow the candidate set first. The mode split decides how tags, subtype, or occasion lanes filter the rest.

The tag system

Every matchable attribute lives in product_tags as (product_id, tag) rows. Department-scoped flavor vocabularies live in flavor-tags.ts so the whiskey lane never shows you gin tags. Tags are inferred from product metadata and subtype rules, then overridden per brand where the rule is wrong (Highland Park → smoky, regardless of what the default Scotch rule says).

bt-liquor/src/lib/flavor-tags.tsdepartment-scoped vocabularies and rule maps

bt-liquor/src/lib/flavor-wizard-helpers.tsinference + brand overrides + scoring

The request

Mobile read

Capture happens once

The public form creates a structured request record instead of loose inbox text.

Scoring is inspectable

AI and fallback scoring both produce reasoning that can be reviewed before action.

Routing leaves a trail

Email, Slack, and review events attach back to the same lead for debugging.

Wizard UIReact state · MotionPOST /api/wizardroute handlerSupabase queryproducts ⋈ variants ⋈ tagsRanked JSONprice · stock · image
Scroll the detail view if you want the full map. Client form → POST /api/wizard → Supabase join on products/variants/tags → ranked JSON.
ts
// POST /api/wizard
type WizardRequest = {
  department: "spirits" | "wine" | "beer" | "rtd";
  typeKey: string;               // e.g. "whiskey"
  subtypeKey?: string;           // e.g. "bourbon"
  budgetTier?: "value" | "mid" | "premium";
  flavorTags: string[];          // e.g. ["smoky", "oaky"]
  styleTags: string[];
  mode: "flavor" | "direct" | "type-direct" | "preference";
};

type WizardResult = {
  product_id: string;
  name: string;
  brand: string;
  price_cents: number;
  on_hand: number;
  image_url: string | null;
  matched_tags: string[];
};
The payload shape. Every field is a real dimension the owner can filter the catalog by.

bt-liquor/src/app/api/wizard/route.tsrequest handler: validation, query, rank, respond

Why not vector search or an LLM?

Admin control

The owner manages tags and tag-to-product associations from the same admin catalog he uses to edit pricing and images. No separate “AI configuration” surface; the recommendation layer is just another part of the product record.