Route Commerce
A multi-tenant B2B e-commerce platform for fresh produce wholesale distribution. Brands manage products, stops, orders, and wholesale customers from a single admin dashboard.
What It Does
Route Commerce helps produce brands run their wholesale operations:
- Product catalogs — Manage products with pricing, variants, and images
- Stop/route scheduling — Schedule pickup stops across regions with customer tracking
- Order management — Handle pickup and shipping orders, track fulfillment
- Wholesale portal — Self-service portal for wholesale customers to place orders
- Billing — Stripe-based subscription billing with plan tiers and add-ons
- Communications — Email/SMS campaigns via Harvest Reach module
- Square sync — Optional inventory sync with Square POS
Tech Stack
- Framework: Next.js 16 (App Router)
- Database: Supabase (Postgres + Auth + RLS)
- Payments: Stripe + Square
- Email/SMS: Resend
- Styling: Tailwind CSS v4
Getting Started
1. Clone and install
git clone <your-repo-url>
cd route-commerce-platform
npm install
2. Set up environment variables
Create a .env.local file with:
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Stripe
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_STARTER=price_...
STRIPE_PRICE_FARM=price_...
STRIPE_PRICE_ENTERPRISE=price_...
# Resend (email)
RESEND_API_KEY=re_...
3. Set up the database
Link your Supabase project, then push migrations:
supabase link --project-ref <your-project-ref>
npm run migrate
Or push a single migration:
npm run migrate:one 83
4. Start the dev server
npm run dev
Open http://localhost:3000. The dev server auto-runs fix-agents.js to patch Next.js App Router agent issues.
5. Dev auth bypass
In development, /login sets a dev_session cookie for full platform access:
dev_session=platform_admin— full access, all brandsdev_session=brand_admin— brand-scoped access only
Visit /login as a shortcut to set the platform_admin session.
Key Commands
npm run dev # Start dev server
npm run build # Production build
npm run lint # ESLint
npm run migrate # Push all pending migrations
npm run migrate:one 83 # Push migration 083_*.sql only
npx tsc --noEmit # TypeScript check (no emit)
npx playwright test # Run E2E tests
Project Structure
src/
├── actions/ # Server actions (database writes)
├── app/
│ ├── admin/ # Admin dashboard pages
│ ├── api/ # API routes (Stripe, Resend, Square webhooks)
│ └── storefront/ # Public brand storefront pages
├── components/ # Shared React components
│ ├── admin/ # Admin-specific components
│ └── storefront/ # Storefront components
└── lib/ # Utilities, auth, feature flags, formatting
supabase/
└── migrations/ # SQL migrations (numbered sequentially)
Plan Tiers
| Tier | Price | Includes |
|---|---|---|
| Starter | $49/mo | Products, 10 stops/mo, Orders, Basic Pickup, 1 user, 25 products |
| Farm | $149/mo | Everything in Starter + Wholesale Portal, Harvest Reach, unlimited stops/products, 5 users |
| Enterprise | Custom | Everything in Farm + AI Intelligence Pack, SMS Campaigns, Square Sync, Water Log, unlimited users |
Add-ons (à la carte on any plan):
- Wholesale Portal ($99/mo)
- Harvest Reach email/SMS ($79/mo)
- AI Intelligence Pack ($59/mo)
- Water Log ($39/mo)
- Square Sync ($39/mo)
- SMS Campaigns ($29/mo)
Time Tracking
Adding Workers
- Go to
/admin/settings→ expand Workers & PINs - Click + Add Worker — enter name, role (Worker or Time Admin), language (EN/ES)
- PINs are auto-generated. Click Reset PIN next to any worker to issue a new one
Adding Tasks
- Go to
/admin/settings→ expand Tasks - Click + Add Task — enter EN name, optional ES name, unit (hours/pieces/units), sort order
- Tasks appear in the time clock app for workers to clock into
Notification Alerts
- Go to
/admin/settings→ expand General Settings - Add email addresses and/or SMS numbers under Notification Recipients
- Configure daily/weekly overtime alert thresholds
Admin Settings Page
All brand configuration lives at /admin/settings with five sections:
- General — Pay period, overtime thresholds, alert settings, notification recipients
- Workers & PINs — Manage workers, reset PINs, toggle active/inactive
- Tasks — Define tasks workers can clock into
- Users & Permissions — Manage admin users and role flags
- Integrations — Stripe, Resend, Twilio, OpenAI credentials and sync options
Admin Modules
The admin dashboard lives at /admin:
- Command Center — Overview dashboard
- Orders — All orders with pickup/ship status
- Stops — Route/stop scheduling and management
- Products — Product catalog management
- Customers — Wholesale customer management
- Communications — Harvest Reach campaign manager
- Wholesale — Wholesale portal settings
- Billing — Plan and subscription management
- Water Log — Irrigation tracking (add-on) — see docs/water-log.md
- Settings — Brand settings, payments, apps
Email Automations (Harvest Reach)
Automated email sequences for wholesale customer engagement.
Abandoned Cart Recovery
3-email sequence (1hr → 24hr → 48hr) triggered when a wholesale customer adds items to cart but doesn't checkout.
Cron schedule: Every 6 hours via Vercel crons (vercel.json)
Dashboard: /admin/communications/abandoned-carts
How it works:
detect_abandoned_wholesale_cartsRPC finds pending wholesale orders not yet enrolledenroll_abandoned_cartRPC creates a tracking record with first email scheduled in 1 hour- Cron job calls
get_active_abandoned_cartsto find carts wherenext_email_at <= now() sendAbandonedCartEmailfires the email via Resend, then updatessequence_step,next_email_at, andstatus
Admin actions:
- View — see all carts with status (Active/Recovered/Expired/Manually Closed), step, items, total
- Close — marks cart as manually_closed (stops further emails)
- Resend — resets
next_email_atto now() and fires email immediately
Welcome Email Sequence
4-email onboarding series triggered when a wholesale contact subscribes (email_opt_in = TRUE).
Cron schedule: Every 6 hours (same as abandoned cart)
Dashboard: /admin/communications/welcome-sequence
How it works:
- Contact subscribes via wholesale portal →
enroll_welcome_sequenceRPC is called - Cron job calls
get_active_welcome_sequencefor entries wherenext_email_at <= now() sendWelcomeEmailfires via Resend, updates step, schedules next email 24h later- After step 4, status →
completed
Admin actions:
- Resend — resets entry to step 1 and fires immediately
- Unsubscribed/bounced contacts are skipped automatically
Manual Trigger (curl)
# Abandoned cart — simulate cron
curl -X POST https://route-commerce-platform.vercel.app/api/email-automation/abandoned-cart \
-H "Authorization: Bearer $CRON_SECRET" \
-H "Content-Type: application/json"
# Welcome sequence
curl -X POST https://route-commerce-platform.vercel.app/api/email-automation/welcome-sequence \
-H "Authorization: Bearer $CRON_SECRET" \
-H "Content-Type: application/json"
# Check status / health
curl https://route-commerce-platform.vercel.app/api/email-automation/abandoned-cart
Environment Variables Required
RESEND_API_KEY=re_... # Resend API key for sending emails
FROM_EMAIL="Tuxedo Corn <no-reply@routecommerce.com>" # Sender address
CRON_SECRET=... # Bearer token to secure cron endpoints (generate a long random string)
Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
| Emails not sending | RESEND_API_KEY not set |
Check env var in Vercel dashboard |
| 401 Unauthorized on cron | CRON_SECRET mismatch |
Verify local .env.local matches Vercel env |
| Empty abandoned cart table | No pending wholesale orders | Create test order, add items, abandon checkout |
| Welcome sequence not enrolling contacts | enroll_welcome_sequence not called |
Ensure subscription UI calls the RPC on email_opt_in = TRUE |
Testing Locally
# Run abandoned cart with verbose output
curl -X POST http://localhost:3000/api/email-automation/abandoned-cart \
-H "Authorization: Bearer local-dev-secret" \
-H "Content-Type: application/json"
# Response: { ok: true, results: { "Tuxedo Corn": { sent: 0, failed: 0, skipped: 0 }, ... } }
Set CRON_SECRET=local-dev-secret in .env.local for local testing.
Go-Live Checklist
Use this checklist before adding real products and doing a soft launch.
Pre-Launch
- Set
CRON_SECRETin Vercel project environment variables (generate withopenssl rand -hex 32) - Verify
RESEND_API_KEYis set in Vercel (for email sending) - Verify
FROM_EMAILis set (e.g."Tuxedo Corn <no-reply@routecommerce.com>") - Run
npm run buildlocally — must pass clean with zero TypeScript errors - Deploy latest commit to production:
vercel --prod - Smoke test: visit
/admin,/admin/settings,/tuxedo— no console errors
Adding Your First Brand's Products
- Go to
/admin/products→ Add Product - For each product: name, wholesale price, image, shipping type (pickup/ship/all)
- Set
is_taxableif Colorado sales tax applies - Make products active — inactive products don't show on storefront
- For Tuxedo Corn: use brand slug
tuxedo, set brand accentgreen
Adding Pickup Stops
- Go to
/admin/stops→ Create Stop - Fill city, state, address, date, time, cutoff date
- Assign brand — stops show on storefront at
/{brand-slug}#stops - Set
is_public: trueto display on storefront
Verifying Wholesale Portal
- Go to
/admin/wholesale— confirm portal is enabled - Visit
/{brand-slug}/wholesale/portalas a customer - Add items to cart, start checkout — confirm abandoned cart appears in
/admin/communications/abandoned-cartswithin minutes
Verifying Email Automations
# Manually trigger welcome sequence (use a real email you control)
curl -X POST https://route-commerce-platform.vercel.app/api/email-automation/welcome-sequence \
-H "Authorization: Bearer $CRON_SECRET"
# Check Resend dashboard — should see sent emails
# Check /admin/communications/welcome-sequence — entry should show "Active"
Monitoring Production
- Check Vercel dashboard → Functions logs for any 500 errors
- Check
/admin/communications/abandoned-cartsfor recovered orders - Vercel crons fire every 6 hours — check function invocations in dashboard
Notes
- All database writes go through server actions (
src/actions/), not the Supabase JS client directly - Dev mode bypasses Supabase auth via
dev_sessioncookie — never use this in production - Brand-scoped data is enforced at the application layer via
p_brand_idparameters in SECURITY DEFINER RPCs - Display dates use
formatDate()(MM/DD/YYYY) — never rawtoLocaleDateString() - Migration files are numbered sequentially — never reuse numbers