Requirements & dependencies

Everything needed to run, deploy, or take this over.

Accounts and external services

ServiceUsed forBlocking?
Supabase — PROTEIN_BARS_DBAll ingredient and product dataYes
OpenAIgpt-5.6-luna for the SMS agentYes, for SMS
TelnyxNumber, 10DLC, SMS in/outYes, for SMS
Railway — GHR workspaceHosts the Express APIYes
Vercel — clean-box / GHR scopeHosts the Next siteYes
CloudflareDNS for ingredientchecker.appYes, for custom domains
GitHub — JWax21/ingredient-checkerSource (private)Yes

Runtime

VersionNote
Node (Railway)20.xNeeds ws; see below
Node (Vercel)24.xProject default
Next.js16.0.10App Router, Turbopack
React19.2

Backend packages

PackageVersionWhy
express^4.18.2HTTP server
@modelcontextprotocol/sdk^1.30.0MCP server + streamable HTTP transport
openai^6.49.0Agent loop, function calling
@supabase/supabase-js^2.109.0Postgres reads
zod^4.4.3MCP tool input schemas
helmet^7.1.0Security headers
cors^2.8.5CORS
dotenv^16.3.1Local env loading
ws^8.21.3Only to satisfy supabase-js — see the trap below

Two non-obvious constraints

1. ws is not optional on Node 20. supabase-js constructs a realtime client even when you only run REST selects, and that needs a WebSocket global Node 20 lacks. It is passed as realtime.transport purely to let the constructor succeed and is never connected.

2. lib/env.js must be the first import in server.js. ESM hoists all imports above statement code, so any module reading process.env at import time — the Supabase and OpenAI clients both do — would construct before dotenv.config() ran. Reordering that import breaks boot.

Frontend packages

PackageVersion
next16.0.10
react / react-dom^19.2.0
@supabase/supabase-js2.57.4
react-icons^5.5.0
classnames^2.5.1
tailwindcss^3.4.17 (dev)

Environment variables

Backend (Railway)

VariableRequiredPurpose
SUPABASE_URLYesPostgres REST endpoint
SUPABASE_ANON_KEYYesRead access under RLS
OPENAI_API_KEYYesAgent loop
TELNYX_API_KEYYes (SMS)Sending
TELNYX_PHONE_NUMBERYes (SMS)Default sender
TELNYX_PUBLIC_KEYYes (SMS)Webhook signature verification — fails closed, see below
TELNYX_ALLOW_UNVERIFIEDLocal dev only1 accepts unsigned webhooks. Never set in production
TELNYX_MESSAGING_PROFILE_IDOptionalFallback sender if no number
SITE_URLOptionalBase for report links; defaults to prod
OPENAI_MODELOptionalOverrides gpt-5.6-luna
OPENAI_APPS_CHALLENGESubmission onlyServed at /.well-known/openai-apps-challenge

Frontend (Vercel)

VariableRequiredPurpose
NEXT_PUBLIC_SUPABASE_URLYesBuild-time and ISR reads
NEXT_PUBLIC_SUPABASE_ANON_KEYYesSame
NEXT_PUBLIC_SITE_URLYesCanonicals, sitemap, JSON-LD
NEXT_PUBLIC_IGGY_SMS_NUMBEROptionalOverrides the CTA number
NEXT_PUBLIC_CHATGPT_APP_URLOptionalOverrides the ChatGPT CTA target

Placeholders are treated as unset

envOrNull() maps REPLACE_ME, TODO, changeme and empty strings to null. Without this, a truthy-but-invalid TELNYX_PUBLIC_KEY would fail every signature check while looking configured. /health reports what is genuinely set, not what merely holds a value.

Signature verification fails closed

With no TELNYX_PUBLIC_KEY, inbound webhooks are rejected, not accepted. SMS simply will not work until the key is set — which is the correct trade: an unverified webhook lets anyone who learns the URL forge inbound messages, spending our OpenAI budget and, once numbers carry subscriptions, impersonating a paying user.

TELNYX_ALLOW_UNVERIFIED=1 bypasses it for local development only. If SMS goes quiet after a deploy, check this env var before anything else.

NEXT_PUBLIC_ is inlined at build time

Changing one on Vercel has no effect until you redeploy. This is not a runtime lookup.

Local setup

# Frontend
npm install
cp .env.local.example .env.local   # or pull from Vercel
npm run dev                        # :3000

# Backend
cd backend && npm install
# populate .env with the Railway vars
npm run dev                        # :3001