Data model
Supabase project PROTEIN_BARS_DB (bsyktcmaolhwzynkxjeb, us-east-2). Shared with proteinbarnerd.com.
Tables this project reads
| Table | Rows | Used by |
|---|---|---|
FULL_REPORT_INGREDIENTS | 514 | Backend tools, /full-report, /search |
whats_in_the_bar_reports | 767 | lookup_product, /bars, /brands |
product_ingredient_snapshot | — | Bar ingredient breakdown |
listicle_transformed_stage_2 | — | Copied route code |
listicle_segments | — | Copied route code |
REVIEWS | — | Copied 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.
| Group | Columns |
|---|---|
| Identity | ingredient_name, url_target, ingredient_category, subcategory |
| Regulatory | us_fda_gras_or_food_additive, eu_status, codex_ins, jecfa_adi, efsa_noael |
| Toxicology | genotoxicity, carcinogenicity, reproductive_developmental_toxicity, infant_specific_guidance |
| Processing | processing_level, process_steps, chemicals_used, method |
| Assessment | overall_health_score, bottom_line, summary, p_potential_concerns |
| Alternatives | alternatives_array + alternative_1..3_* (derivation, why healthier) |
| Other | citations, 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:
| Table | anon can |
|---|---|
| whats_in_the_bar_reports | SELECT |
| FULL_REPORT_INGREDIENTS | SELECT |
| listicle_transformed_stage_2 | SELECT |
| product_ingredient_snapshot / _preview | SELECT |
| listicle_segments, REVIEWS | SELECT |
| EMAIL_LIST | INSERT only — no read-back |
| everything else | nothing — 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 pointThe 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_INGREDIENTSis referenced by copied frontend code but does not exist.product_ingredient_previewexists 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.