Files
route-commerce/PRODUCTION_DEPLOYMENT_CHECKLIST.md
T
Nora 49b8e27219 chore(supabase): full purge — remove all Supabase references from codebase
- Delete supabase/ directory (config.toml, push-migrations.js,
  ADMIN_CREATE_STOP_FIX.sql, 137 archived migration files)
- Remove @supabase/ssr and @supabase/supabase-js from package.json
- Strip *.supabase.co from next.config.ts image hostnames
- Strip https://*.supabase.co from vercel.json CSP connect-src
- Remove 'supabase/**' ignore from eslint.config.mjs
- Clean supabase references from src/lib/db.ts, vitest.config.ts,
  db/seeds/2026-tuxedo-tour-stops.sql, src/actions/storefront.ts,
  scripts/import-tuxedo-stops.ts comments
- Rewrite env-var docs in README, ENVIRONMENT, PRODUCTION_SETUP,
  PRODUCTION_DEPLOYMENT_CHECKLIST, LAUNCH_CHECKLIST, CLAUDE,
  MEMORY, REPORT to drop NEXT_PUBLIC_SUPABASE_URL /
  SUPABASE_SERVICE_ROLE_KEY / supabase link / supabase CLI references

The canonical migration runner is now scripts/migrate.js (uses pg
directly via DATABASE_URL). Migrations live in db/migrations/. The
Supabase CLI is no longer in the codebase. The only remaining
'@supabase/*' in the dep tree is @supabase/auth-js as a transitive
of @neondatabase/auth (Neon Auth / Better Auth).

Final source scan: grep -rln '@supabase\|rest/v1\|supabase\.co'
src/ tests/ db/ scripts/ next.config.ts vercel.json eslint.config.mjs
package.json returns zero. Verification: npm install clean
(11 added, 21 removed, 12 changed), tsc shows same 17 pre-existing
errors (Stripe dahlia API version + fetch preconnect mocks),
vitest 172/175 (3 pre-existing failures in getAdminUser.test.ts),
lint shows same 14 pre-existing errors. Live-tested: dev server
boots in 248ms, public storefronts (/) (/login) (/pricing) (/tuxedo)
(/indian-river-direct) all return 200; /admin/v2?demo=1 reaches 200
after dev_session redirect; storefront renders real brand content.
2026-06-25 17:48:32 -06:00

349 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Route Commerce — Production Deployment Checklist
**Platform Version:** 1.6
**Last Updated:** 2026-05-13
**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. All numbered `001``092` are required.
```bash
# Push all migrations (sequential, no Supabase CLI required)
npm run migrate:one 001
npm run migrate:one 002
# ... through ...
npm run migrate:one 092
# Or push all at once
npm run migrate
```
**Key migrations (last 15):**
| # | File | Purpose |
|---|------|---------|
| 078 | `wholesale_deposit_guard.sql` | Adds `amount`, `payment_method` to `wholesale_deposits` |
| 079 | `resend_analytics.sql` | Adds Resend webhook endpoint + analytics tables |
| 080 | `communication_segments.sql` | Adds `communication_segments` table |
| 081 | `stop_blast_campaign.sql` | Adds `stop_blast_campaign` RPC |
| 082 | `brand_name_in_campaign_email.sql` | Adds `brand_name` column to `communication_campaigns` |
| 083 | `shipping_settings.sql` | Adds `shipping_settings` table |
| 084 | `shipments.sql` | Adds `shipments` table for FedEx tracking |
| 085 | `brand_settings.sql` | Adds `brand_settings` table (core) |
| 086 | `brand_settings_email_integration.sql` | Adds email/webhook fields to `brand_settings` |
| 087 | `brand_logos_bucket.sql` | Adds `brand-logos` storage bucket |
| 088 | `brand_features.sql` | Adds `brand_features` table + RPCs for add-on system |
| 089 | `ai_providers_custom_integrations.sql` | Adds AI provider credentials storage |
| 090 | `storefront_settings.sql` | Adds storefront customization fields + `get_brand_settings_by_slug` RPC |
| 091 | `091_brand_plan_tier.sql` | Adds `plan_tier`, limits, `stripe_customer_id`, `get_brand_plan_info` RPC |
| 092 | `092_stripe_subscriptions.sql` | Adds `stripe_subscription_id/status/current_period_end`, `set_brand_subscription`, `get_brand_subscription` RPCs |
> **Note:** Migration 092 adds Stripe subscription tracking. Migration 091 adds plan tier billing. Apply both before deploying billing changes.
---
## 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.