Data model

Supabase project PROTEIN_BARS_DB (bsyktcmaolhwzynkxjeb, us-east-2). Shared with proteinbarnerd.com.

Tables this project reads

TableRowsUsed by
FULL_REPORT_INGREDIENTS514Backend tools, /full-report, /search
whats_in_the_bar_reports767lookup_product, /bars, /brands
product_ingredient_snapshotBar ingredient breakdown
listicle_transformed_stage_2Copied route code
listicle_segmentsCopied route code
REVIEWSCopied route code

FULL_REPORT_INGREDIENTS — the asset

82 columns per ingredient, 100% populated across every high-value field. This is the product; everything else is delivery.

GroupColumns
Identityingredient_name, url_target, ingredient_category, subcategory
Regulatoryus_fda_gras_or_food_additive, eu_status, codex_ins, jecfa_adi, efsa_noael
Toxicologygenotoxicity, carcinogenicity, reproductive_developmental_toxicity, infant_specific_guidance
Processingprocessing_level, process_steps, chemicals_used, method
Assessmentoverall_health_score, bottom_line, summary, p_potential_concerns
Alternativesalternatives_array + alternative_1..3_* (derivation, why healthier)
Othercitations, allergens, diet, glycemic_index, sweetness_vs_sucrose

url_target is the slug and the join key between the database and both the web routes and the MCP tools.

Row Level Security

RLS is enabled on all 23 tables. Only these grants exist for anon:

Tableanon can
whats_in_the_bar_reportsSELECT
FULL_REPORT_INGREDIENTSSELECT
listicle_transformed_stage_2SELECT
product_ingredient_snapshot / _previewSELECT
listicle_segments, REVIEWSSELECT
EMAIL_LISTINSERT only — no read-back
everything elsenothing — service role only

No INSERT/UPDATE/DELETE policy exists on any content table, so the anon key cannot write. Verified: an UPDATE against all 767 products as anon modifies 0 rows.

Public read is deliberate, and blocks the paid tier

FULL_REPORT_INGREDIENTS allows anonymous read because that is what renders the public ingredient pages. If the data becomes a paid product, that grant has to go and access must move behind a server route holding the service key that checks a subscription first — otherwise the paid dataset is downloadable with a key designed to be public.

Six client components in the copied route code import the Supabase client, so that change is not just a policy edit.

Query behaviour worth knowing

lookupProduct() is deliberately fuzzy. Matching a whole phrase against each column fails on real input — flavors are stored "Cookies & Cream" while people type "cookies and cream". So it:

1. try the whole phrase           → "quest cookies and cream"
2. else split into words, drop stopwords (and, bar, the, protein)
3. normalize "&" ↔ "and" on both sides
4. fetch up to 50 candidates, then rank:
     brand-name word hit  = 10 points
     other word hit       =  1 point

The wide candidate fetch matters: with a plain LIMIT 5 the right brand falls outside the result set, outvoted by common words like "cream". Brand hits are weighted 10× because naming a brand means wanting that brand.

Known gaps

  • Coverage is scoped to protein bars — aspartame is absent, as are many mainstream additives.
  • SHORT_REPORT_INGREDIENTS is referenced by copied frontend code but does not exist.
  • product_ingredient_preview exists with 0 rows and only 4 columns — a free/paid split that was planned, not built.
  • No tenancy columns anywhere yet; see the roadmap page.