# 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/`: ```bash # 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 `_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**: ```json { "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: ```sql -- 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) ```bash # 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 ```bash # 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.