Files
route-commerce/PRODUCTION_DEPLOYMENT_CHECKLIST.md
Nora 95eab42f4b docs: refresh stale docs to match current implementation
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, added current-state header
  flagging Auth.js v5 wiring as historical, and documented the SSH/Gitea
  workflow (id_ed25519_crispygoat + no API token → push-hook URL flow
  for opening PRs).
- LAUNCH_CHECKLIST.md:
  added 'Last updated: 2026-06-25' header and refresh note in footer.
- CLAUDE.md:
  new 'Gitea authentication (SSH)' subsection under 'Canonical Remote'
  documenting ~/.ssh/id_ed25519_crispygoat, ssh-agent setup, what works
  (git push) vs what doesn't (REST API without token), and the actual
  PR-creation workflow (push + open the URL the Gitea hook prints).

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.
2026-06-25 20:18:49 -06:00

18 KiB
Raw Permalink Blame History

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 001092 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 _migrations and skips already-applied files. 0001_init.sql is 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 of 0001_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

  1. Go to Stripe Dashboard → Developers → Webhooks
  2. Add endpoint: https://yourdomain.com/api/stripe/webhook
  3. Select events:
    • checkout.session.completed — saves subscription, triggers feature sync
    • customer.subscription.updated — syncs plan tier + add-on features on changes
    • customer.subscription.deleted — clears subscription, disables all add-ons
    • invoice.payment_succeeded — updates next billing date
    • invoice.payment_failed — marks subscription past_due, sends admin notification
  4. Copy the Webhook Secret (whsec_...) to STRIPE_WEBHOOK_SECRET

Resend

  1. Go to Resend Dashboard → Webhooks
  2. Add endpoint: https://yourdomain.com/api/resend/webhook
  3. Select events: email.delivered, email.open, email.click, email.bounce, email.complaint
  4. Copy the webhook secret to RESEND_WEBHOOK_SECRET

Square (optional)

  1. Go to Square Developer Dashboard → Webhooks
  2. Add subscription: https://yourdomain.com/api/square/webhook
  3. Select events: inventory.count.updated, order.created, payment.completed
  4. Copy the signature key to SQUARE_WEBHOOK_SIGNATURE_KEY

FedEx (optional)

  1. 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

  • /admin loads 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
  • /tuxedo loads without platform SiteHeader/SiteFooter (brand header/footer only)
  • /indian-river-direct loads 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_enabled setting
  • Zip code search works on IRD homepage

Add-ons

  • /admin/communications loads (Harvest Reach — requires enablement)
  • /admin/wholesale loads (Wholesale Portal — requires enablement)
  • /admin/settings/ai loads (AI Tools — requires enablement + API key)
  • /admin/settings/square-sync loads (Square Sync — requires enablement + Square credentials)
  • /admin/water-log loads 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.completed and saves subscription
  • /admin/settings/billing shows 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_admin cookie 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_session auth: The dev_session cookie bypasses real Neon Auth (Better Auth). In production, ensure no dev_session cookie exists for regular users. The auth bypass only works when NODE_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_logs have 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_id not populated in message logs: send_campaign and send_stop_blast do not set event_id. The Resend webhook looks up logs by customer_email + subject instead. 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_in defaults to false. Users must explicitly opt in for SMS campaigns.
  • brand_id: null for platform admins: getAdminUser() returns brand_id: null for platform_admin role. Always pass explicit brandId to 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_RLS communications 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: ReportsDashboard has an "Explain" button for AI-powered explanations but it requires OpenAI key + working API proxy
  • Square inventory mapping: syncInventoryToSquare and syncInventoryFromSquare require a square_catalog_object_id column on products to map RC products to Square catalog items — not yet implemented
  • Webhook event deduplication: Add a stripe_events_processed table 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.