Files
route-commerce/PRODUCTION_DEPLOYMENT_CHECKLIST.md
T
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

340 lines
18 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:** 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 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**:
```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.