KnowledgeVault AI module-spec M-02
internal prototype · canonical JSON + Dreamborn Forge HTML
internal generated
module-spec · supabase_json

KnowledgeVault AI module-spec M-02

M-02 creates the products catalog and product_pre_seeds tables, exposes GET /api/products/queue for authenticated experts, and delivers two mobile screens: the Product Queue Screen (S-02-a) where experts browse and filter SKUs by specialty, and the Product Pre-Session Screen (S-02-b) where they review product details and earnings before starting a capture session. Coverage percentage is stubbed at 0.0 in all queue responses until M-04 adds product_id tracking to capture_sessions. product_pre_see

Planning Surface

Use this to decide what happens next.

Status

approved

Phase

M-02

open questions

No items captured.

Agent Handoff
Start Here

M-02 creates the products catalog and product_pre_seeds tables, exposes GET /api/products/queue for authenticated experts, and delivers two mobile screens: the Product Queue Screen (S-02-a) where experts browse and filter SKUs by specialty, and the Product Pre-Session Screen (S-02-b) where they review product details and earnings before starting a capture session. Coverage percentage is stubbed at 0.0 in all queue responses until M-04 adds product_id tracking to capture_sessions. product_pre_see

Completion Evidence

No explicit evidence field yet. Require tests, screenshots, linked PRs, or reviewed outputs before marking complete.

Open Questions

No section body captured.

Structured Payload

Machine-readable source fields

phase
id

M-02

summary

M-02 creates the products catalog and product_pre_seeds tables, exposes GET /api/products/queue for authenticated experts, and delivers two mobile screens: the Product Queue Screen (S-02-a) where experts browse and filter SKUs by specialty, and the Product Pre-Session Screen (S-02-b) where they review product details and earnings before starting a capture session. Coverage percentage is stubbed at 0.0 in all queue responses until M-04 adds product_id tracking to capture_sessions. product_pre_seeds schema is created here but data is populated by M-03.

module id

M-02

api routes
authpatherrorsmethodrequestresponse
Expert JWT required — Supabase Auth JWT; users.role must equal 'expert'/api/products/queue- Missing or invalid JWT → 401 {"error":"Unauthorized","code":"unauthorized","status":401} - users.role != 'expert' → 403 {"error":"Forbidden — expert role required","code":"forbidden","status":403} - expert_profiles row absent → 200 {"products":[]} treated as empty specializations - Supabase query error → 500 {"error":"Could not load products — try again","code":"database_error","status":500}GETNo body, no query params. JWT via Authorization: Bearer header or @supabase/ssr cookie.200 { "products": [ { "id": "uuid", "sku": "string", "name": "string", "manufacturer": "string", "image_url": "string|null", "has_surge_badge": boolean, "coverage_pct": 0.0, "earnings_potential": { "min": float, "max": float } } ] }. min=5.00,max=10.00 when has_surge_badge=true; min=3.00,max=8.00 when false.
data model
notes

products.category values must exactly match strings stored in expert_profiles.specializations[] — no normalisation layer in M-02. The skus table (M-01) is a separate general catalog and is not joined to products. coverage_pct = count(capture_sessions WHERE product_id=products.id AND status='completed') / products.target_session_count — always 0.0 in M-02 because capture_sessions.product_id FK is added in M-04. earnings_potential computed at response time: base {min:3.00,max:8.00}; has_surge_badge adds {+2.00,+2.00}.

tables
rlsnameindexespurposekey columns
Enable RLS. Policy 'products_select_authenticated': FOR SELECT USING (auth.role()='authenticated'). No INSERT/UPDATE/DELETE policy for authenticated users — service-key bypasses RLS.products- idx_products_category ON products(category) - idx_products_surge ON products(has_surge_badge) WHERE has_surge_badge=true - idx_products_fully_captured ON products(is_fully_captured) WHERE is_fully_captured=falseCapture-queue-specific product catalog with surge incentive and coverage-tracking fields.- id uuid PRIMARY KEY DEFAULT gen_random_uuid() - sku text NOT NULL UNIQUE - name text NOT NULL - manufacturer text NOT NULL - category text — must match strings in expert_profiles.specializations[] exactly - description text - spec_sheet_url text - image_url text - has_surge_badge boolean NOT NULL DEFAULT false - is_fully_captured boolean NOT NULL DEFAULT false — updated by M-04 trigger - target_session_count integer NOT NULL DEFAULT 10 CHECK (target_session_count > 0) - created_at timestamptz NOT NULL DEFAULT now()
Enable RLS. Policy 'pre_seeds_select_expert': FOR SELECT USING (auth.role()='authenticated' AND EXISTS (SELECT 1 FROM public.users WHERE supabase_auth_id=auth.uid() AND role='expert')). No INSERT/UPDATE/DELETE policy for authenticated users.product_pre_seeds- idx_product_pre_seeds_product_id ON product_pre_seeds(product_id)AI-generated pre-seed data per product — schema created here, populated by M-03, consumed by M-04.- id uuid PRIMARY KEY DEFAULT gen_random_uuid() - product_id uuid NOT NULL UNIQUE REFERENCES products(id) ON DELETE CASCADE - question_set jsonb NOT NULL DEFAULT '[]' — array of {id:uuid, text:string, chunk_type:string} - context_summary jsonb NOT NULL DEFAULT '{}' — {summary:string, key_specs:string[], common_failure_modes:string[]} - key_vectors jsonb NOT NULL DEFAULT '[]' — array of 1536-dim float arrays - model_version text — e.g. 'gpt-4o-2024-08-06' - generated_at timestamptz NOT NULL DEFAULT now()
module name

Product Catalog & Queue

deliverables
idnametypeinputsoutputsdescription
D-02-01002_products_catalog.sqlmigration- 001_kv_phase1_foundation.sql already applied (users, expert_profiles tables exist)- products table in public schema - product_pre_seeds table in public schema - idx_products_category index - idx_products_surge partial index - idx_products_fully_captured partial index - RLS policies on both tablesCreates the products table (capture-queue catalog) and product_pre_seeds table (AI session seed data, populated by M-03). Adds partial indexes on category, has_surge_badge, and is_fully_captured. RLS: products SELECT open to authenticated users; INSERT/UPDATE/DELETE service-key only. product_pre_seeds SELECT restricted to authenticated users with users.role='expert'; INSERT/UPDATE/DELETE service-key only.
D-02-02GET /api/products/queue — apps/kv-app/app/api/products/queue/route.tsapi_route- Authorization: Bearer <supabase_jwt> header or @supabase/ssr cookie- 200 { products: [{id, sku, name, manufacturer, image_url, has_surge_badge, coverage_pct:0.0, earnings_potential:{min,max}}] } - 401 on missing/invalid JWT - 403 on non-expert role - 500 on database errorReturns the expert's personalized product queue. Authenticates via Supabase JWT via @supabase/ssr cookie client. Resolves users.role to verify 'expert'. Looks up expert_profiles.specializations. Filters products WHERE category = ANY(specializations) AND is_fully_captured = false. Orders has_surge_badge DESC. Computes earnings_potential: base {min:3.00, max:8.00}, adds {+2.00,+2.00} when has_surge_badge=true. coverage_pct always 0.0 in M-02. Returns empty array for empty specializations or all-captured match set.
D-02-03CoverageBadge + ProductCard — apps/kv-app/components/products/component- CoverageBadge: {coverage_pct: number} - ProductCard: {product: {id,sku,name,manufacturer,image_url,has_surge_badge,coverage_pct,earnings_potential:{min,max}}, onClick: ()=>void, loading?: boolean}- apps/kv-app/components/products/CoverageBadge.tsx - apps/kv-app/components/products/ProductCard.tsx - apps/kv-app/components/products/index.tsTwo reusable React components. CoverageBadge: colored pill with aria-label, thresholds red=0-30% (bg-red-500) / yellow=31-70% (bg-yellow-400) / green=71-99% (bg-green-500). ProductCard: tappable div with product image (img src=image_url, fallback via onError), name, manufacturer, CoverageBadge, conditional 'Surge' chip when has_surge_badge=true, text 'Earn up to $X.XX'. Skeleton state (loading=true) renders animate-pulse divs matching card dimensions. Both exported from components/products/index.ts.
D-02-04Product Queue Screen S-02-a — apps/kv-app/app/(expert)/products/page.tsxcomponent- Next.js cookies() for JWT - /api/products/queue response- apps/kv-app/app/(expert)/products/page.tsx - apps/kv-app/app/(expert)/products/ProductQueueClient.tsxNext.js 14 server component at /products. SSR-fetches /api/products/queue with user JWT from @supabase/ssr cookies. Passes product array to ProductQueueClient.tsx (client component) for specialty filter interactivity. Renders: sticky h1 'Products to capture' (role=heading level=1), specialty filter tab bar (chips: 'All' + unique categories from products[]), ProductCard grid. Filter is client-side array filter. Empty state 1 (specializations=[]): headline 'Set your specialties', body 'Tell us your trade areas to see products you can capture.', CTA 'Update profile' href='/profile'. Empty state 2
D-02-05Product Pre-Session Screen S-02-b — apps/kv-app/app/(expert)/products/[id]/page.tsxcomponent- Route param: id (product uuid string) - Supabase JWT via @supabase/ssr cookies (auth gate) - Supabase SELECT on products table- apps/kv-app/app/(expert)/products/[id]/page.tsx - apps/kv-app/app/(expert)/products/[id]/PreSessionClient.tsxNext.js 14 server component at /products/[id]. SSR-fetches product from Supabase: SELECT id,sku,name,manufacturer,category,description,image_url,has_surge_badge,target_session_count FROM products WHERE id=$1. Returns notFound() if product not found. Renders: full-width product image (loading='lazy', onError shows placeholder), product info block (SKU / Category / Manufacturer labels), earnings badge 'Earn $<min>–$<max>', conditional surge badge 'Surge bonus active — extra $2.00' (when has_surge_badge=true), first-capture badge 'First capture bonus — $5.00' (always shown in M-02 since coverage_
integrations
callspurposeservice
- supabase.auth.getUser() — validates JWT, returns auth user with uid - supabase.from('users').select('id,role').eq('supabase_auth_id', uid).single() — resolve role and internal user id - supabase.from('expert_profiles').select('specializations').eq('user_id', user.id).single() — fetch specialty filter list - supabase.from('products').select('id,sku,name,manufacturer,image_url,has_surge_badge,target_session_count').filter('category','cs','{'+specializations.join(',')+'}').eq('is_fully_captured',false).order('has_surge_badge',{ascending:false}) — fetch filtered queueValidate expert JWT and resolve auth.uid() to internal user record for role check and expert_profiles.specializations lookup.Supabase Auth
prerequisites
  • users table exists with columns: id uuid pk, supabase_auth_id uuid unique, email text, role text CHECK IN ('expert','curator','consumer','tenant_admin')
  • expert_profiles table exists with columns: id uuid pk, user_id uuid fk users.id, specializations text[] not null default '{}'
  • Supabase Auth JWT auth flow exists: POST /api/auth/signup, POST /api/auth/login, GET /api/auth/me all functional
  • Next.js 14 App Router project initialised at apps/kv-app/ with @supabase/ssr and @supabase/supabase-js installed
  • lib/supabase/server.ts createClient() helper exists and validated in M-01
  • 001_kv_phase1_foundation.sql migration applied without error to target Supabase project
open questions

No items captured.

schema version

1.0