Wire up NextAuth v5 with @auth/pg-adapter, JWT sessions (edge-friendly), and a dev Credentials provider for local testing without Google OAuth. Stack - next-auth@5.0.0-beta.31, @auth/pg-adapter@1.11.2, @types/pg - Google OAuth provider via GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET (falls back to AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET) - Postgres adapter wired to a single pg.Pool in src/lib/db.ts style — reads DATABASE_URL with SUPABASE_DB_URL / POSTGRES_URL fallbacks - JWT session strategy (edge-safe) so the proxy can verify sessions without a DB round-trip Files - src/auth.config.ts edge-safe config (Google + authorized cb) - src/lib/auth.ts server config (adapter + dev Credentials) - src/proxy.ts Next.js 16 proxy (was middleware.ts) - src/app/api/auth/[...nextauth]/route.ts catch-all handler - src/app/protected-example/page.tsx demo page that renders auth() session - src/actions/auth-signin.ts signInWithGoogle, signInWithDev, signOutAction server actions - src/app/login/LoginClient.tsx added "Sign in with Google" + dev form - supabase/migrations/204_authjs_tables.sql users / accounts / sessions / verification_token schema (UUID-keyed) - .env.example AUTH_SECRET, AUTH_URL, GOOGLE_CLIENT_*, DATABASE_URL, ALLOW_DEV_LOGIN Removed - src/middleware.ts deleted; Next.js 16 only runs one proxy (the new src/proxy.ts is canonical) Routes - /login, /admin, /admin/*, /protected-example proxy matcher - /api/auth/{providers,csrf,signin/<provider>,callback/<provider>, session,signout} standard Auth.js endpoints Local dev - npm run dev (now runs on port 4000) - push migration 204 then visit /login - dev signin works with any non-empty username/password (hidden when ALLOW_DEV_LOGIN=false) - Google signin requires real GOOGLE_CLIENT_ID + redirect URI http://localhost:4000/api/auth/callback/google Verified - tsc --noEmit clean - /admin, /admin/orders, /protected-example → 307 to /login when unauthenticated - /api/auth/session returns user after signin - /protected-example renders session info - /api/auth/providers returns google + dev-login Docs - CLAUDE.md and MEMORY.md updated to reflect the Supabase → Postgres + Auth.js v5 pivot Gradual migration in progress - src/lib/admin-permissions.ts still uses dev_session / rc_auth_uid; the admin shell will show 'Access Denied' for Auth.js-only sessions until each page is flipped over - @supabase/* packages remain in package.json for the same reason - production deployment (AUTH_URL=https://, __Secure- cookies) is out of scope for this pass
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)
- 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