Tyler 11cd2fd01a
Deploy to route.crispygoat.com / deploy (push) Successful in 4m33s
Water Log: production-ready module (schema, auth, CRUD, UI, tests, docs)
Completes the Tuxedo Water Log feature end-to-end. Builds on the existing
water_* tables in 0001_init.sql; extends them with 0090 and rewrites the
server actions, settings page, export route, and admin panel to actually
function.

Schema (db/migrations/0090_water_log_completion.sql, db/schema/water-log.ts)
- headgate_token + status + max_flow_gpm + thresholds + notes on water_headgates
- role + phone + notes on water_irrigators
- method + total_gallons + photo_url + logged_date + lat/lon on water_log_entries
- new tables: water_admin_settings, water_admin_sessions, water_audit_log, water_alert_log

Auth (src/lib/water-log-pin.ts, src/lib/water-log-audit.ts)
- scrypt N=16384 per-PIN random salt, self-describing hash format
- weak-PIN detection (repeats, monotonic, palindromes)
- 200ms delay + timingSafeEqual on failed verify
- 4-8 digit PINs, generatePin() rejects weak patterns

Server actions (src/actions/water-log/{admin,field,settings}.ts)
- full Drizzle CRUD: headgates, irrigators, entries (with audit hooks)
- verifyPin + 8h field sessions, admin PIN + configurable 1-168h sessions
- regenerateAdminPin invalidates existing admin sessions
- threshold breach detection + alert log writes
- getWaterAdminSession() helper for admin pages

API routes
- /api/water-admin-auth: PIN-protected, generic error (no enum leak)
- /api/water-logs/export: admin-gated JSON/CSV, correct headers

UI (Field Almanac — cream + forest, Fraunces display, § section markers)
- src/components/admin/WaterLogAdminPanel.tsx: full rewrite
- src/components/water/WaterFieldClient.tsx: matches palette
- src/components/water/icons/{WaterGauge,SeasonMark}.tsx: custom SVGs
- src/app/admin/water-log/settings/page.tsx: full rewrite, Field Almanac

Tests
- tests/unit/water-log-pin.test.ts (21 cases): validate, hash, verify, generate
- tests/unit/water-log-reporting.test.ts (31 cases): season, filter, CSV, report
- tests/water-log.spec.ts (Playwright E2E): PIN form, auth gates, API gates

Docs
- docs/water-log.md: full admin guide, security model, data dictionary, checklist
- README: link to docs/water-log.md in admin modules list
- .env.example: Water Log section comment

Type-check: clean. Tests: 70 pass, 3 fail (3 pre-existing getAdminUser
failures on main, unrelated to this work).
2026-06-17 11:36:00 -06:00

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 brands
  • dev_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

  1. Go to /admin/settings → expand Workers & PINs
  2. Click + Add Worker — enter name, role (Worker or Time Admin), language (EN/ES)
  3. PINs are auto-generated. Click Reset PIN next to any worker to issue a new one

Adding Tasks

  1. Go to /admin/settings → expand Tasks
  2. Click + Add Task — enter EN name, optional ES name, unit (hours/pieces/units), sort order
  3. Tasks appear in the time clock app for workers to clock into

Notification Alerts

  1. Go to /admin/settings → expand General Settings
  2. Add email addresses and/or SMS numbers under Notification Recipients
  3. 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:

  1. detect_abandoned_wholesale_carts RPC finds pending wholesale orders not yet enrolled
  2. enroll_abandoned_cart RPC creates a tracking record with first email scheduled in 1 hour
  3. Cron job calls get_active_abandoned_carts to find carts where next_email_at <= now()
  4. sendAbandonedCartEmail fires the email via Resend, then updates sequence_step, next_email_at, and status

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_at to 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:

  1. Contact subscribes via wholesale portal → enroll_welcome_sequence RPC is called
  2. Cron job calls get_active_welcome_sequence for entries where next_email_at <= now()
  3. sendWelcomeEmail fires via Resend, updates step, schedules next email 24h later
  4. 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_SECRET in Vercel project environment variables (generate with openssl rand -hex 32)
  • Verify RESEND_API_KEY is set in Vercel (for email sending)
  • Verify FROM_EMAIL is set (e.g. "Tuxedo Corn <no-reply@routecommerce.com>")
  • Run npm run build locally — 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

  1. Go to /admin/productsAdd Product
  2. For each product: name, wholesale price, image, shipping type (pickup/ship/all)
  3. Set is_taxable if Colorado sales tax applies
  4. Make products active — inactive products don't show on storefront
  5. For Tuxedo Corn: use brand slug tuxedo, set brand accent green

Adding Pickup Stops

  1. Go to /admin/stopsCreate Stop
  2. Fill city, state, address, date, time, cutoff date
  3. Assign brand — stops show on storefront at /{brand-slug}#stops
  4. Set is_public: true to display on storefront

Verifying Wholesale Portal

  1. Go to /admin/wholesale — confirm portal is enabled
  2. Visit /{brand-slug}/wholesale/portal as a customer
  3. Add items to cart, start checkout — confirm abandoned cart appears in /admin/communications/abandoned-carts within 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-carts for 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_session cookie — never use this in production
  • Brand-scoped data is enforced at the application layer via p_brand_id parameters in SECURITY DEFINER RPCs
  • Display dates use formatDate() (MM/DD/YYYY) — never raw toLocaleDateString()
  • Migration files are numbered sequentially — never reuse numbers
S
Description
No description provided
Readme 145 MiB
Languages
TypeScript 77.8%
PLpgSQL 18.4%
CSS 1.5%
JavaScript 1.5%
Shell 0.5%
Other 0.2%