Files
route-commerce/PRODUCTION_DEPLOYMENT_CHECKLIST.md
Nora 0ac4beaaa8 fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call
- Convert getAdminUser() → requireAuth() across 73 admin action files
- Add getSession() to public/wholesale server actions
- Fix multi-line return type corruption from earlier auto-fixers
- Move FedEx token cache to non-'use server' module
- Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD,
  EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS
- Update Stripe API version 2026-05-27 → 2026-06-24
- Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient
- Fix 51 TypeScript errors (return type corruption, missing imports)
2026-06-25 23:49:37 -06:00

354 lines
17 KiB
Markdown
Raw Permalink 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) · Supabase · Stripe · Square · Resend · FedEx
---
## 1. Migration Order
Apply migrations in number order. All numbered `001``092` are required.
```bash
# Push all migrations (sequential)
npm run migrate:one 001
npm run migrate:one 002
# ... through ...
npm run migrate:one 092
# Or push all at once via Supabase CLI
supabase db push
```
**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 |
|---|---|---|
| `NEXT_PUBLIC_SUPABASE_URL` | Yes | Supabase project URL (e.g., `https://xxx.supabase.co`) |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Yes | Supabase anon/public key (safe to expose in browser) |
| `SUPABASE_SERVICE_ROLE_KEY` | Yes | Supabase service role key — **never expose in browser**, server-side only |
| `SUPABASE_PAT` | Yes | Supabase Personal Access Token for migrations CLI |
### 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 triggered by a Supabase time-based trigger (`send_stop_reminder_email`). Verify the trigger is active:
```sql
SELECT * FROM pg_publication_tables WHERE tablename = 'stops';
```
If not, recreate: `ALTER PUBLICATION supabase_realtime PUBLISH INSERT, UPDATE ON TABLE stops;`
---
## 5. Feature Flag Enablement (Post-Deploy)
After deployment, enable add-ons per brand via the admin UI or Supabase SQL:
```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 Supabase
supabase db reset --backup-id <backup-before-migration>
# Or manually revert (example for migration 090)
psql $DATABASE_URL -c "ALTER TABLE brand_settings DROP COLUMN IF EXISTS schedule_pdf_notes;"
```
> **Important:** Only use `supabase db reset` on staging. 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 Supabase 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 RLS disabled. All data access is through SECURITY DEFINER RPCs. Do not expose these tables via direct Supabase queries.
- **`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 Supabase queries 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 | Set via Supabase Auth dashboard |
| Kyle Martinez | Primary contact — Route Commerce owner |
| Supabase Support | supabase.com/support |
| Vercel Support | vercel.com/support |
For Vercel production incidents: **vercel.com/deployments** → find deployment → "Support" button.
For Supabase production incidents: **supabase.com/dashboard** → project → "Support" tab.