Requirements & dependencies
Everything needed to run, deploy, or take this over.
Accounts and external services
| Service | Used for | Blocking? |
|---|---|---|
| Supabase — PROTEIN_BARS_DB | All ingredient and product data | Yes |
| OpenAI | gpt-5.6-luna for the SMS agent | Yes, for SMS |
| Telnyx | Number, 10DLC, SMS in/out | Yes, for SMS |
| Railway — GHR workspace | Hosts the Express API | Yes |
| Vercel — clean-box / GHR scope | Hosts the Next site | Yes |
| Cloudflare | DNS for ingredientchecker.app | Yes, for custom domains |
| GitHub — JWax21/ingredient-checker | Source (private) | Yes |
Runtime
| Version | Note | |
|---|---|---|
| Node (Railway) | 20.x | Needs ws; see below |
| Node (Vercel) | 24.x | Project default |
| Next.js | 16.0.10 | App Router, Turbopack |
| React | 19.2 |
Backend packages
| Package | Version | Why |
|---|---|---|
| express | ^4.18.2 | HTTP server |
| @modelcontextprotocol/sdk | ^1.30.0 | MCP server + streamable HTTP transport |
| openai | ^6.49.0 | Agent loop, function calling |
| @supabase/supabase-js | ^2.109.0 | Postgres reads |
| zod | ^4.4.3 | MCP tool input schemas |
| helmet | ^7.1.0 | Security headers |
| cors | ^2.8.5 | CORS |
| dotenv | ^16.3.1 | Local env loading |
| ws | ^8.21.3 | Only 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
| Package | Version |
|---|---|
| next | 16.0.10 |
| react / react-dom | ^19.2.0 |
| @supabase/supabase-js | 2.57.4 |
| react-icons | ^5.5.0 |
| classnames | ^2.5.1 |
| tailwindcss | ^3.4.17 (dev) |
Environment variables
Backend (Railway)
| Variable | Required | Purpose |
|---|---|---|
| SUPABASE_URL | Yes | Postgres REST endpoint |
| SUPABASE_ANON_KEY | Yes | Read access under RLS |
| OPENAI_API_KEY | Yes | Agent loop |
| TELNYX_API_KEY | Yes (SMS) | Sending |
| TELNYX_PHONE_NUMBER | Yes (SMS) | Default sender |
| TELNYX_PUBLIC_KEY | Yes (SMS) | Webhook signature verification — fails closed, see below |
| TELNYX_ALLOW_UNVERIFIED | Local dev only | 1 accepts unsigned webhooks. Never set in production |
| TELNYX_MESSAGING_PROFILE_ID | Optional | Fallback sender if no number |
| SITE_URL | Optional | Base for report links; defaults to prod |
| OPENAI_MODEL | Optional | Overrides gpt-5.6-luna |
| OPENAI_APPS_CHALLENGE | Submission only | Served at /.well-known/openai-apps-challenge |
Frontend (Vercel)
| Variable | Required | Purpose |
|---|---|---|
| NEXT_PUBLIC_SUPABASE_URL | Yes | Build-time and ISR reads |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | Yes | Same |
| NEXT_PUBLIC_SITE_URL | Yes | Canonicals, sitemap, JSON-LD |
| NEXT_PUBLIC_IGGY_SMS_NUMBER | Optional | Overrides the CTA number |
| NEXT_PUBLIC_CHATGPT_APP_URL | Optional | Overrides 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