Documentation pass — reviewed codebase in full, identified stale references, and aligned docs with actual code state. Changes: - CLAUDE.md, PRODUCTION_SETUP.md, LAUNCH_CHECKLIST.md, src/auth.config.ts: middleware path src/middleware.ts → src/proxy.ts (Next.js 16+ convention) - CLAUDE.md, ENVIRONMENT.md, README.md: dev server port localhost:3000 → localhost:4000 (per package.json dev script 'next dev --webpack -H 0.0.0.0 -p 4000') - PRODUCTION_DEPLOYMENT_CHECKLIST.md: rewrote migration list — actual db/migrations/ contains 10 files (0000–0091), not the Supabase-era 001–092 series. Updated platform version 1.6 → 2.0 and last-updated stamp. - MEMORY.md: refreshed 'Last updated' to 2026-06-25 and added a current-state header flagging the 'Auth.js v5 wiring' section as historical (current auth is Neon Auth/Better Auth). Also flags the 'GitHub origin' notes as historical (only Gitea origin exists). - LAUNCH_CHECKLIST.md: added 'Last updated: 2026-06-25' header and refresh note in footer. Verification: - npx tsc --noEmit: pre-existing Stripe API version + test mock errors only (verified by stashing changes and re-running — same error set). - No new errors introduced.
18 KiB
Route Commerce — Production Deployment Checklist
Platform Version: 2.0
Last Updated: 2026-06-25
Environment: Next.js 16 (App Router) · Postgres (direct via pg) · Neon Auth (Better Auth) · Stripe · Square · Resend · FedEx
1. Migration Order
Apply migrations in number order. The full set lives in db/migrations/:
# Push all migrations (idempotent — `_migrations` table tracks what's applied)
npm run migrate
The current migration set is consolidated (the Supabase-era 001–092 series was collapsed into 0001_init.sql plus a handful of incremental files). If db/migrations/ is empty after the Supabase → Postgres migration, see MEMORY.md "Direction Pivot" for the cutover notes.
Current migrations in db/migrations/:
| # | File | Purpose |
|---|---|---|
| 0000 | 0000_qa_neon_auth_stub.sql |
QA-only stub for neon_auth schema (real provisioning via Neon Auth in prod) |
| 0001 | 0001_init.sql |
Core schema: brands, products, orders, stops, customers, communication tables, water log, time tracking, RPCs, indexes, triggers. Fully re-runnable (IF NOT EXISTS + trigger guards). |
| 0002 | 0002_admin_password.sql |
Adds password_hash column to admin_users |
| 0003 | 0003_tour_rpc_functions.sql |
RPCs for tour/stop planning |
| 0040 | 0040_command_center.sql |
Adds command-center dashboard tables |
| 0041 | 0041_command_center_schema_fix.sql |
Schema fixup for command-center |
| 0042 | 0042_drop_command_center.sql |
Drops command-center (rolled back) |
| 0043 | 0043_admin_users_extra_columns.sql |
Adds display_name, phone_number, brand_id, can_manage_pickup/messages/refunds/users, active, must_change_password, auth_provider, auth_subject, last_login columns to admin_users |
| 0090 | 0090_water_log_completion.sql |
Completes water-log tables/RPCs |
| 0091 | 0091_dashboard_summary_rpc.sql |
dashboard_summary RPC for admin dashboard |
Note: The migration runner (
scripts/migrate.js) tracks applied files in_migrationsand skips already-applied files.0001_init.sqlis fully re-runnable, so it is safe to re-apply on a DB that was initialized outside the runner. Stripe subscription tracking, plan-tier billing, brand settings, etc. are all part of0001_init.sql— they were originally separate Supabase-era migrations that were consolidated.
2. Required Environment Variables
Copy .env.local.example to .env.local and fill in all values.
Core Platform
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | Postgres connection string (e.g., postgresql://user:pass@host:5432/dbname?sslmode=require) |
DATABASE_ADMIN_URL |
No | Direct (non-pooled) Postgres connection with elevated perms. Only used by scripts/migrate.js for DDL — falls back to DATABASE_URL if unset. |
NEON_AUTH_BASE_URL |
Yes | Neon Auth (Better Auth) base URL |
NEON_AUTH_COOKIE_SECRET |
Yes | Neon Auth session cookie signing secret (min 32 chars) |
NEXT_PUBLIC_SITE_URL |
Yes | Public site URL used for auth redirects and Origin header |
Payments
| Variable | Required | Description |
|---|---|---|
STRIPE_SECRET_KEY |
Yes (if using Stripe) | Stripe secret key (sk_live_... or sk_test_...) |
STRIPE_WEBHOOK_SECRET |
Yes (if using Stripe webhooks) | Stripe webhook signing secret (whsec_...) — from Stripe CLI or dashboard |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY |
Yes (if using Stripe) | Stripe publishable key (pk_live_... or pk_test_...) |
Stripe Price IDs (create in Stripe Dashboard → Products → add Price):
| Variable | Product |
|---|---|
STRIPE_PRICE_STARTER |
Recurring $49/mo — Starter plan |
STRIPE_PRICE_FARM |
Recurring $149/mo — Farm plan |
STRIPE_PRICE_ENTERPRISE |
Recurring $399/mo — Enterprise plan |
STRIPE_PRICE_HARVEST_REACH |
Recurring $79/mo — Harvest Reach add-on |
STRIPE_PRICE_WHOLESALE_PORTAL |
Recurring $99/mo — Wholesale Portal add-on |
STRIPE_PRICE_WATER_LOG |
Recurring $39/mo — Water Log add-on |
STRIPE_PRICE_AI_TOOLS |
Recurring $59/mo — AI Intelligence add-on |
STRIPE_PRICE_SQUARE_SYNC |
Recurring $39/mo — Square Sync add-on |
STRIPE_PRICE_SMS_CAMPAIGNS |
Recurring $29/mo — SMS Campaigns add-on |
For annual pricing, create separate annual prices (e.g., $441/yr for Starter = $49×12×0.75) and set STRIPE_PRICE_STARTER_ANNUAL, STRIPE_PRICE_FARM_ANNUAL, STRIPE_PRICE_ENTERPRISE_ANNUAL.
| Variable | Product |
|---|---|
STRIPE_PRICE_STARTER_ANNUAL |
Annual Starter ($441/yr) |
STRIPE_PRICE_FARM_ANNUAL |
Annual Farm ($1,341/yr) |
STRIPE_PRICE_ENTERPRISE_ANNUAL |
Annual Enterprise ($3,591/yr) |
STRIPE_PRICE_HARVEST_REACH_ANNUAL |
Annual Harvest Reach ($711/yr) |
STRIPE_PRICE_WHOLESALE_PORTAL_ANNUAL |
Annual Wholesale Portal ($891/yr) |
STRIPE_PRICE_WATER_LOG_ANNUAL |
Annual Water Log ($351/yr) |
STRIPE_PRICE_AI_TOOLS_ANNUAL |
Annual AI Intelligence ($531/yr) |
STRIPE_PRICE_SQUARE_SYNC_ANNUAL |
Annual Square Sync ($351/yr) |
STRIPE_PRICE_SMS_CAMPAIGNS_ANNUAL |
Annual SMS Campaigns ($261/yr) |
| Variable | Required | Description |
|---|---|---|
SQUARE_ACCESS_TOKEN |
Yes (if using Square) | Square access token from Square Developer Dashboard |
SQUARE_APPLICATION_ID |
Yes (if using Square) | Square application ID |
SQUARE_LOCATION_ID |
Yes (if using Square) | Square location ID for inventory sync |
SQUARE_WEBHOOK_SIGNATURE_KEY |
Yes (if using Square webhooks) | Square webhook signature key |
Email (Resend)
| Variable | Required | Description |
|---|---|---|
RESEND_API_KEY |
Yes (if using email) | Resend API key (re_...) |
RESEND_WEBHOOK_SECRET |
Yes (if using Resend webhooks) | Resend webhook signing secret |
Shipping (FedEx)
| Variable | Required | Description |
|---|---|---|
FEDEX_API_KEY |
Yes (if using FedEx) | FedEx API key from FedEx Developer Portal |
FEDEX_API_SECRET |
Yes (if using FedEx) | FedEx API secret |
FEDEX_ACCOUNT_NUMBER |
Yes (if using FedEx) | FedEx account number |
FEDEX_METER_NUMBER |
Yes (if using FedEx) | FedEx meter number |
AI
| Variable | Required | Description |
|---|---|
| OPENAI_API_KEY | Recommended | OpenAI API key (sk-...) — default AI provider for AI Intelligence Pack add-on |
AI Provider Setup (/admin/settings/integrations):
Route Commerce supports multiple AI providers via the Integrations settings page:
| Provider | Auth Method |
|---|---|
| OpenAI | API key (OPENAI_API_KEY env var or per-brand settings) |
| Anthropic | API key (set via /admin/settings/integrations AI Provider panel) |
| Google Gemini | API key |
| xAI (Grok) | API key |
| Custom / OAI-compatible | API key + base URL |
All AI tools (Campaign Writer, Pricing Advisor, Demand Forecasting, Route Optimizer, Stop Blast Advisor) respect the provider chosen in /admin/settings/integrations. The getAIClient() function in src/actions/integrations/ai-providers.ts handles provider instantiation dynamically.
Feature Flags (Optional)
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_ENABLED_ADDONS |
No | Comma-separated list of add-on keys enabled for all brands (e.g., harvest_reach,wholesale_portal). Overrides per-brand brand_features table for white-label/self-hosted deployments. |
3. Webhook Setup
Stripe
- Go to Stripe Dashboard → Developers → Webhooks
- Add endpoint:
https://yourdomain.com/api/stripe/webhook - Select events:
checkout.session.completed— saves subscription, triggers feature synccustomer.subscription.updated— syncs plan tier + add-on features on changescustomer.subscription.deleted— clears subscription, disables all add-onsinvoice.payment_succeeded— updates next billing dateinvoice.payment_failed— marks subscriptionpast_due, sends admin notification
- Copy the Webhook Secret (
whsec_...) toSTRIPE_WEBHOOK_SECRET
Resend
- Go to Resend Dashboard → Webhooks
- Add endpoint:
https://yourdomain.com/api/resend/webhook - Select events:
email.delivered,email.open,email.click,email.bounce,email.complaint - Copy the webhook secret to
RESEND_WEBHOOK_SECRET
Square (optional)
- Go to Square Developer Dashboard → Webhooks
- Add subscription:
https://yourdomain.com/api/square/webhook - Select events:
inventory.count.updated,order.created,payment.completed - Copy the signature key to
SQUARE_WEBHOOK_SIGNATURE_KEY
FedEx (optional)
- FedEx does not use outbound webhooks — track shipments via FedEx Track API polling or webhook subscription via FedEx Freight/LTL services
4. Vercel Cron Jobs
For scheduled tasks (stop reminders, campaign sends), add to vercel.json or configure in Vercel Dashboard → Project → Cron Jobs:
{
"cron": [
{
"path": "/api/cron/send-stop-reminders",
"schedule": "0 8 * * *"
},
{
"path": "/api/cron/process-water-log-reminders",
"schedule": "0 6 * * *"
}
]
}
Note: Stop reminder emails are fired by Vercel cron jobs (/api/cron/send-stop-reminders), not by a database trigger. Verify the cron is configured in vercel.json (or Vercel Dashboard → Project → Cron Jobs).
5. Feature Flag Enablement (Post-Deploy)
After deployment, enable add-ons per brand via the admin UI or direct Postgres:
-- Enable Harvest Reach for a brand
SELECT set_brand_feature('your-brand-id', 'harvest_reach', true);
-- Enable Wholesale Portal
SELECT set_brand_feature('your-brand-id', 'wholesale_portal', true);
-- Enable Water Log (Tuxedo example)
SELECT set_brand_feature('64294306-5f42-463d-a5e8-2ad6c81a96de', 'water_log', true);
-- Enable AI Intelligence
SELECT set_brand_feature('your-brand-id', 'ai_tools', true);
-- Enable Square Sync
SELECT set_brand_feature('your-brand-id', 'square_sync', true);
Or use the admin UI at /admin/settings/apps (after logging in as platform_admin or brand_admin with settings access).
6. Post-Deploy Verification Checklist
Run through these after a successful deploy:
Core Platform
/adminloads with correct nav and section cards- Can create/edit/archive a stop in
/admin/stops - Can create/edit a product in
/admin/products - Cart works — add item, view cart, checkout
- Payment completes successfully (test with Stripe test mode)
Public Storefronts
/pricing— public pricing page loads, monthly/annual toggle works, comparison table shows/hides/tuxedoloads without platform SiteHeader/SiteFooter (brand header/footer only)/indian-river-directloads without platform SiteHeader/SiteFooter- "Admin" link visible in brand footer when logged in as admin
- Stops display: tabbed view works, "This Week" default, "Full Season" pagination works
- Schedule PDF download works (
/api/tuxedo/schedule-pdf,/api/indian-river-direct/schedule-pdf) - Wholesale portal banner shows/hides based on
wholesale_enabledsetting - Zip code search works on IRD homepage
Add-ons
/admin/communicationsloads (Harvest Reach — requires enablement)/admin/wholesaleloads (Wholesale Portal — requires enablement)/admin/settings/ailoads (AI Tools — requires enablement + API key)/admin/settings/square-syncloads (Square Sync — requires enablement + Square credentials)/admin/water-logloads for authorized brand (Water Log — requires enablement)
Admin Settings
/admin/settings— all sections visible including "Add-ons"/admin/settings/brand— can save hero tagline, about headline, feature toggles/admin/settings/apps— can enable/disable add-on features/admin/settings/billing— shows plan tier, subscription status badge, next billing date, Stripe portal button/admin/settings/integrations— AI provider configuration (OpenAI/Anthropic/Google/xAI/custom)- After toggling an add-on: dashboard card updates badge from "Add-on" to "Enabled"
Billing
- Stripe webhook receives
checkout.session.completedand saves subscription /admin/settings/billingshows correct plan tier and add-on status- Subscription status badge shows
active/past_due/canceled - Next billing date displays correctly (from
stripe_current_period_end) - "Manage in Stripe Portal" button opens Stripe Customer Portal
- Stripe Customer Portal shows correct plan + add-ons
- Invoice history table shows mock invoice rows (ready for real Stripe data)
- Monthly/annual toggle recalculates displayed prices on billing page
- Add-on enable/disable triggers Stripe checkout / cancellation
Auth & Permissions
- Login with platform admin — full access to all brands
- Login with brand admin — access limited to assigned brand
- Login with store employee — restricted to pickup and wholesale only
dev_session=platform_admincookie works for local development bypass
Reports
/admin/reports— all tabs load (Overview, Orders by Stop, Sales by Product, Fulfillment, Pickup Status, Contact Growth, Campaigns)- Date range selector works
- CSV export works for each report
7. Rollback Plan
Immediate Rollback (Vercel)
# Rollback to previous deployment in Vercel Dashboard
# Project → Deployments → find last good deployment → "Promote to Production"
Vercel keeps 10 recent deployments. For critical regressions, promote the previous deployment.
Database Rollback
# Revert last migration via Postgres
# (Restore from a `pg_dump` snapshot taken before the migration was applied)
# Or manually revert (example for migration 090)
psql $DATABASE_URL -c "ALTER TABLE brand_settings DROP COLUMN IF EXISTS schedule_pdf_notes;"
Important: Always restore from a known-good backup. In production, manually revert individual migrations to avoid data loss.
Environment Variable Rollback
Environment variables are version-controlled in Vercel. Restore a previous version in Vercel Dashboard → Project → Environment Variables → History.
8. Known Limitations & Future Work
Known Issues
dev_sessionauth: Thedev_sessioncookie bypasses real Neon Auth (Better Auth). In production, ensure nodev_sessioncookie exists for regular users. The auth bypass only works whenNODE_ENV !== "production".- Water Log visible to all brand admins: Currently Tuxedo-only by brand ID check. If another brand admin needs access, the check needs to be made configurable.
- Communications tables (no RLS):
communication_campaigns,communication_templates,communication_contacts,communication_message_logshave no row-level policies. All data access is through SECURITY DEFINER RPCs. Do not expose these tables via direct SQL from client-side code. event_idnot populated in message logs:send_campaignandsend_stop_blastdo not setevent_id. The Resend webhook looks up logs bycustomer_email + subjectinstead. This is a known limitation — if Resend events are delayed, the lookup may fail to match.- Square sync is one-directional: Products import from Square → Route Commerce, but product edits in Route Commerce do not push back to Square.
- SMS opt-in defaults to
FALSE:communication_contacts.sms_opt_indefaults tofalse. Users must explicitly opt in for SMS campaigns. brand_id: nullfor platform admins:getAdminUser()returnsbrand_id: nullforplatform_adminrole. Always pass explicitbrandIdto server action functions.- Square webhook signature mismatch: The Square webhook handler uses
crypto.createHmac("sha256", signatureKey)but Square sends the signature as base64-encoded. Verify signature encoding matches Square's documentation before enabling Square webhooks in production. - Stripe webhook reliability: Stripe webhook delivery is not guaranteed to be in-order or immediate. The webhook handler is idempotent — it can safely receive duplicate events. Consider implementing a webhook event log table to track processed event IDs for deduplication.
NO_RLScommunications tables: All communications data is accessible via SECURITY DEFINER RPCs only — never expose direct SQL to these tables in client-side code.
Future Work (Non-Blocking)
- Cursor-based pagination: Replace offset-based pagination (
page * limit) with cursor-based pagination for large datasets (orders, contacts) - Webhook retry queue: Failed webhooks (Resend, Stripe) are not currently retried — implement a dead-letter queue or retry mechanism
- Import Center: AI-powered import UI (
/admin/import) was designed but not fully implemented - Multi-brand support in public storefront: Currently hardcoded for Tuxedo and IRD — a generic
/[brandSlug]route would support all brands - AI features in reports:
ReportsDashboardhas an "Explain" button for AI-powered explanations but it requires OpenAI key + working API proxy - Square inventory mapping:
syncInventoryToSquareandsyncInventoryFromSquarerequire asquare_catalog_object_idcolumn on products to map RC products to Square catalog items — not yet implemented - Webhook event deduplication: Add a
stripe_events_processedtable or similar to track processed Stripe event IDs and prevent duplicate processing
9. Contacts & On-Call
| Role | Contact |
|---|---|
| Platform Admin | First Neon Auth user becomes the platform admin via provision-admin.ts |
| Kyle Martinez | Primary contact — Route Commerce owner |
| Neon Support | neon.tech/support |
| Vercel Support | vercel.com/support |
For Vercel production incidents: vercel.com/deployments → find deployment → "Support" button.