26 water-log rows were POSTed directly to Smartsheet on 2026-07-03 because
the cron drain was failing. Those rows went in without Entry IDs, so the
next cron drain would have created duplicates (findRowByColumn returns
nothing when Entry ID is empty).
This script:
1. Matches each of the 26 hardcoded rows to a DB entry by
(headgate + irrigator + measurement + unit + logged_at).
2. Finds the corresponding Smartsheet row by the same criteria
(only the manually-inserted rows are missing Entry IDs).
3. PUTs the Entry ID back into the sheet row.
4. Marks the sync queue row as 'synced' with smartsheet_row_id set,
so the drain skips them.
5. Writes a sync log entry noting the manual backfill.
Idempotent: re-runs are no-ops. Wired into deploy.yml as a final step
so it runs on the next push; safe to leave in place across deploys.
Local invocation:
npm run smartsheet:backfill-entry-ids
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