From ec1506dc82bbfa2b69198dc012292427b2caf313 Mon Sep 17 00:00:00 2001
From: default
Date: Sat, 6 Jun 2026 03:38:00 +0000
Subject: [PATCH] feat(auth): Auth.js v5 + Postgres adapter for local smoke
test
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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/,callback/,
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
---
.env.example | 82 +++++++++++++
CLAUDE.md | 64 +++++++---
MEMORY.md | 59 ++++++++--
middleware.ts | 64 ----------
package.json | 5 +-
src/actions/auth-signin.ts | 56 +++++++++
src/app/api/auth/[...nextauth]/route.ts | 16 +++
src/app/login/LoginClient.tsx | 77 ++++++++++++
src/app/protected-example/page.tsx | 124 ++++++++++++++++++++
src/auth.config.ts | 105 +++++++++++++++++
src/lib/auth.ts | 136 ++++++++++++++++++++++
src/middleware.ts | 93 ---------------
src/proxy.ts | 26 +++++
supabase/migrations/204_authjs_tables.sql | 73 ++++++++++++
14 files changed, 798 insertions(+), 182 deletions(-)
create mode 100644 .env.example
delete mode 100644 middleware.ts
create mode 100644 src/actions/auth-signin.ts
create mode 100644 src/app/api/auth/[...nextauth]/route.ts
create mode 100644 src/app/protected-example/page.tsx
create mode 100644 src/auth.config.ts
create mode 100644 src/lib/auth.ts
delete mode 100644 src/middleware.ts
create mode 100644 src/proxy.ts
create mode 100644 supabase/migrations/204_authjs_tables.sql
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..80019c6
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,82 @@
+# ============================================================================
+# Route Commerce — Environment variables
+# ============================================================================
+# Copy to `.env.local` and fill in real values for local development.
+# Production: set these in your hosting dashboard (Vercel / Netlify / etc.).
+# ============================================================================
+
+# ── App ─────────────────────────────────────────────────────────────────────
+NEXT_PUBLIC_BASE_URL=http://localhost:4000
+NEXT_PUBLIC_SITE_URL=http://localhost:4000
+
+# ── Database (Postgres, direct — Supabase is being removed) ────────────────
+# Single connection string used by `pg.Pool` in src/lib/auth.ts and the
+# admin-permissions / data-service layer. Format:
+# postgresql://USER:PASS@HOST:PORT/DBNAME?sslmode=require
+DATABASE_URL=postgresql://postgres:postgres@localhost:5432/route_commerce
+
+# ── Auth.js (NextAuth v5) ───────────────────────────────────────────────────
+# Generate with: npx auth secret
+# Or: openssl rand -base64 32
+AUTH_SECRET=replace-me-with-a-32-byte-base64-string
+
+# Base URL used to build OAuth callback URLs. In dev:
+AUTH_URL=http://localhost:4000
+# In production, set to https://yourdomain.com
+
+# Google OAuth provider.
+# 1. Go to https://console.cloud.google.com/apis/credentials
+# 2. Create an OAuth 2.0 Client ID (type: Web application)
+# 3. Add Authorized redirect URI: http://localhost:3000/api/auth/callback/google
+# 4. Copy client id + client secret below
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
+
+# Auth.js also reads AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET if you prefer the
+# default NextAuth variable names. The code in src/auth.config.ts falls
+# back to those names if GOOGLE_CLIENT_ID is unset.
+
+# Set to "false" to disable the in-app dev credentials provider even in
+# development. Default: enabled in dev only.
+ALLOW_DEV_LOGIN=true
+
+# ── Supabase (legacy, being removed) ────────────────────────────────────────
+# Still used by the existing admin pages, server actions, and the
+# `getAdminUser` flow. Once the auth migration is complete and the
+# @supabase/* packages are removed, these can go away.
+NEXT_PUBLIC_SUPABASE_URL=
+NEXT_PUBLIC_SUPABASE_ANON_KEY=
+SUPABASE_SERVICE_ROLE_KEY=
+
+# ── Stripe ─────────────────────────────────────────────────────────────────
+STRIPE_SECRET_KEY=
+STRIPE_WEBHOOK_SECRET=
+NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
+STRIPE_PRICE_STARTER=
+STRIPE_PRICE_FARM=
+STRIPE_PRICE_ENTERPRISE=
+STRIPE_PRICE_HARVEST_REACH=
+STRIPE_PRICE_WHOLESALE_PORTAL=
+STRIPE_PRICE_WATER_LOG=
+STRIPE_PRICE_AI_TOOLS=
+STRIPE_PRICE_SQUARE_SYNC=
+STRIPE_PRICE_SMS_CAMPAIGNS=
+
+# ── Resend (transactional email) ────────────────────────────────────────────
+RESEND_API_KEY=
+RESEND_WEBHOOK_SECRET=
+
+# ── AI providers ────────────────────────────────────────────────────────────
+OPENAI_API_KEY=
+ANTHROPIC_API_KEY=
+GOOGLE_API_KEY=
+XAI_API_KEY=
+MINIMAX_API_KEY=
+MINIMAX_BASE_URL=https://api.minimax.io/v1
+
+# ── Square (optional) ───────────────────────────────────────────────────────
+SQUARE_APP_SECRET=
+SQUARE_ENVIRONMENT=sandbox
+
+# ── Cron / automation ───────────────────────────────────────────────────────
+CRON_SECRET=replace-me-with-a-random-string
diff --git a/CLAUDE.md b/CLAUDE.md
index f4e9f3e..69ba6b7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -6,7 +6,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
Route Commerce is a multi-tenant B2B e-commerce platform for fresh produce wholesale distribution. Brands sell to customers who pick up at scheduled stops or receive shipments. The platform includes admin dashboards for order management, stop/route scheduling, product catalogs, payment processing (Stripe + Square), and a communications module ("Harvest Reach") for email/SMS campaigns.
-Tech stack: Next.js 16 (App Router) · Supabase (auth + Postgres + RLS) · Stripe · Square · Resend (email) · Tailwind CSS v4
+Tech stack: Next.js 16 (App Router) · **Postgres** (direct — Supabase is being removed) · Auth.js (NextAuth v5, in-progress migration from bespoke cookie auth) · Stripe · Square · Resend (email) · Tailwind CSS v4
+
+> **Direction:** Supabase is being removed in favor of a direct Postgres connection. The `supabase/` directory is kept as a path for migrations tooling only (no Supabase platform/CLI/auth). Until the Auth.js migration ships, auth still flows through the `dev_session` / `rc_auth_uid` cookies — see the Authentication section. New DB code should connect to Postgres directly (via `pg` or the chosen driver — see Database section) and **must not** import from `@supabase/*` or call Supabase REST.
---
@@ -21,14 +23,12 @@ npx tsc --noEmit # TypeScript check (no emit)
npx playwright test # Run E2E tests (Playwright)
```
-> The migrate script auto-detects Supabase CLI first, then falls back to direct PostgreSQL.
-> For CLI mode: `brew install supabase/tap/supabase` then `supabase link --project-ref wnzkhezyhnfzhkhiflrp`
-> For direct PG mode: `pg` and `dotenv` are already in devDependencies.
-> If `get_brand_settings` migration fails with "cannot change return type", the function signature changed — drop and recreate it first.
+> The migrate script (`supabase/push-migrations.js`) now only uses the direct `pg` path — the Supabase CLI branch is legacy. It reads `DATABASE_URL` from `.env.local` via `dotenv`. `pg` is already in devDependencies.
+> If a migration fails with "cannot change return type", the function signature changed — drop and recreate it first.
**Recent migration work is documented in `MEMORY.md`** (Supabase login + link process, updates to `push-migrations.js` for modern CLI, specific SQL patches made to 091/145/148/200/201 so they would apply cleanly, and which migrations were pushed in the session). Cat `MEMORY.md` for details.
-No test suite currently exists. E2E tests use Playwright (`tests/` or `test-e2e.ts`).
+E2E tests live in `tests/` and run via Playwright. Specs include `tests/smoke.spec.ts` and `tests/login/login-flow.spec.ts`. **Note: `playwright.config.ts` defaults `baseURL` to production** (`https://route-commerce-platform.vercel.app`); override with `PLAYWRIGHT_URL=http://localhost:3000` for local runs, or pass `--config` with a local config.
---
@@ -41,10 +41,24 @@ No test suite currently exists. E2E tests use Playwright (`tests/` or `test-e2e.
- `dev_session=brand_admin` — full access to assigned brand only
- `dev_session=store_employee` — limited access (orders, pickup, wholesale only)
-`src/lib/admin-permissions.ts` is the single source of truth for the current admin user. It uses a `dev_session` cookie in development and Supabase Auth in production. **Never import this file directly into Client Components** — use the `getCurrentAdminUser` server action from `@/actions/admin-user` instead.
+`src/lib/admin-permissions.ts` is the single source of truth for the current admin user. It uses a `dev_session` cookie in development and the legacy `rc_auth_uid` cookie in production (set by the pre-Auth.js `/api/login`) — never import this file directly into Client Components. Use the `getCurrentAdminUser` server action from `@/actions/admin-user` instead.
+
+The middleware (`src/middleware.ts`) guards `/admin/:path*` and `/login`. It auto-issues a `dev_session=platform_admin` cookie for the demo flow when no auth is present. A `clerk-auth.ts` helper exists in `src/lib/` but is currently a stub — do not depend on it.
The `AdminUser` type lives in `src/lib/admin-permissions-types.ts` and is shared across server/client boundary.
+#### Auth.js (NextAuth v5) migration — in progress
+
+The platform is migrating from the bespoke `dev_session` + `rc_auth_uid` cookie flow to **Auth.js (NextAuth v5)**, with Supabase as the database adapter and email/OAuth providers. While the migration is in flight:
+
+- Do **not** add new code that depends on the `dev_session` or `rc_auth_uid` cookies — write against the Auth.js API (`auth()`, `signIn`, `signOut`, `getSession`) instead.
+- New env vars: `AUTH_SECRET`, `AUTH_URL`, and provider-specific keys (`AUTH_GITHUB_ID`/`SECRET`, `AUTH_GOOGLE_ID`/`SECRET`, etc.). See `.env.example` for the full list.
+- A new route handler at `src/app/api/auth/[...nextauth]/route.ts` will replace the ad-hoc `/api/login`, `/api/auth/uid`, and `/api/logout` endpoints.
+- The middleware (`src/middleware.ts`) will eventually use `auth()` from NextAuth to populate the session; the existing `dev_session` auto-login branch is a temporary fallback for demos.
+- `src/lib/admin-permissions.ts` will keep its public surface (`getAdminUser`, `getCurrentAdminUser`) but read the session from NextAuth internally — the `AdminUser` type does not need to change.
+- `clerk-auth.ts` is being removed in favor of Auth.js; do not extend it.
+- Until the migration ships, the `dev_session` and `rc_auth_uid` paths remain the source of truth — see the section above for current behavior.
+
### Server Actions Pattern
All database writes go through server actions in `src/actions/`. These:
@@ -55,9 +69,19 @@ All database writes go through server actions in `src/actions/`. These:
Server actions are "use server" files that export async functions. Client components import and call them directly.
-### SECURITY DEFINER RPCs + Brand Scoping
+### Database (Postgres, direct)
-The app uses **PostgreSQL SECURITY DEFINER functions** for all data access. These run with the function owner's privileges and bypass RLS entirely. This means:
+The app connects to **Postgres directly** — no Supabase platform, JS client, or REST gateway. Server actions use the `pg` driver (or whatever the chosen connection layer is) to call `SECURITY DEFINER` PL/pgSQL functions. Storage of files (product images, etc.) is moving to an S3-compatible object store; until that's wired up, image references can stay as URLs.
+
+#### Connection
+
+- `DATABASE_URL` in `.env.local` (and hosting dashboard) is the only required DB env var.
+- A single shared `pg` `Pool` is exported from `src/lib/db.ts` (TBD — to be created/confirmed during the migration). Server actions and API routes import it and call `pool.query(...)` against RPC names.
+- No `NEXT_PUBLIC_SUPABASE_URL` / `SUPABASE_SERVICE_ROLE_KEY` / `@supabase/*` imports — these are being purged from the codebase.
+
+#### SECURITY DEFINER RPCs + Brand Scoping
+
+The app uses **PostgreSQL SECURITY DEFINER functions** for all data access. These run with the function owner's privileges and bypass any future RLS. This means:
- Brand isolation must be enforced at the **application layer** (in server actions), not in database policies
- Every RPC that touches brand-scoped data accepts a `p_brand_id UUID` parameter and filters by it
@@ -182,10 +206,19 @@ For annual pricing, create separate annual prices in Stripe (e.g., $441/yr for S
### Communications Module ("Harvest Reach")
-The communications system (`/admin/communications`) uses a separate set of tables that are **NOT protected by RLS** — they rely entirely on the SECURITY DEFINER RPCs + application-layer brand scoping. Key tables: `communication_campaigns`, `communication_templates`, `communication_contacts`, `communication_message_logs`.
+The communications system (`/admin/communications`) uses a separate set of tables that are **NOT protected by RLS** — they rely entirely on the SECURITY DEFINER RPCs + application-layer brand scoping. Key tables: `communication_campaigns`, `communication_templates`, `communication_contacts`, `communication_message_logs`. (The "no RLS" framing carries over from the Supabase era; on raw Postgres this just means no row-level policies — scoping is still enforced by RPC + app layer.)
`send_campaign` / `send_stop_blast` RPCs insert into `communication_message_logs` but do NOT populate `event_id`. The Resend webhook (`src/app/api/resend/webhook/route.ts`) must therefore look up logs by `customer_email + subject + created_at` (7-day window), not by `event_id`.
+**Scheduled automations** (declared in `vercel.json`):
+- `POST /api/email-automation/abandoned-cart` — every 6h, fires abandoned-cart sequence emails
+- `POST /api/email-automation/welcome-sequence` — every 6h, fires welcome onboarding sequence
+- `POST /api/cron/send-scheduled` — daily 09:00, sends scheduled campaigns
+- `POST /api/wholesale/notifications/{send,dispatch,pickup-reminder}` — wholesale lifecycle
+- `POST /api/square/process-queue` — every 2 min, drains Square sync queue
+
+These endpoints are also reachable via curl for manual triggering; the email-automation routes accept `Authorization: Bearer $CRON_SECRET`.
+
### Payments
- **Stripe** — primary payment processor; `src/actions/payments.ts` and `src/app/api/stripe/` handle checkout, webhooks, refunds
@@ -200,7 +233,7 @@ Separate from orders/stops — tracks irrigation/water usage per brand. `src/act
## Key Conventions
-- All DB mutations use Supabase REST API (`fetch` to `${supabaseUrl}/rest/v1/rpc/...`) from server actions, NOT the Supabase JS client (avoids SSR cookie issues)
+- All DB access goes through a shared `pg` `Pool` (see Database section). Server actions call SECURITY DEFINER RPCs via `pool.query('SELECT * FROM fn_name($1, $2)', [...])`. Do not introduce `@supabase/*` imports or REST fetch to `*/rest/v1/`.
- `gen_random_uuid()` used in migrations for primary keys
- Migrations use `CREATE OR REPLACE FUNCTION` for idempotency — never `DROP` then `CREATE`
- Status enums stored as TEXT — no PostgreSQL ENUM type
@@ -217,11 +250,11 @@ Separate from orders/stops — tracks irrigation/water usage per brand. `src/act
|---|---|
| Admin auth + permissions | `src/lib/admin-permissions.ts`, `src/lib/admin-permissions-types.ts` |
| Middleware (route protection) | `src/middleware.ts` |
-| Server actions | `src/actions/*.ts` (one file per domain) |
+| Server actions | `src/actions/*.ts` (one file per domain; also grouped into `src/actions/{admin,ai,billing,communications,harvest-reach,integrations,orders,products,settings,shipping,stops,water-log,platform,route-trace,time-tracking,email-automation}/`) |
| Admin pages | `src/app/admin/[module]/page.tsx` |
| Admin client components | `src/components/admin/*.tsx` |
-| Migrations | `supabase/migrations/` |
-| Supabase client | `src/lib/supabase.ts` |
+| Migrations | `supabase/migrations/` (kept for now; will likely move to `db/migrations/` in a later pass) |
+| Postgres pool / driver | `src/lib/db.ts` (TBD — create during the Supabase removal pass) |
| Email templates | `src/lib/email-templates.ts` |
| Date formatting | `src/lib/format-date.ts` |
| Feature flags | `src/lib/feature-flags.ts` |
@@ -238,7 +271,8 @@ Separate from orders/stops — tracks irrigation/water usage per brand. `src/act
## Gotchas
- **Dev mode `brand_id: null`**: `getAdminUser()` returns `brand_id: null` for platform_admin dev sessions. Always pass explicit `brandId` to server action functions that accept it — don't rely on `adminUser.brand_id` alone.
-- **Communications = no RLS**: The communications tables (campaigns, templates, contacts, message_logs) have RLS disabled. All brand scoping must be enforced in server actions.
+- **Communications = no RLS**: The communications tables (campaigns, templates, contacts, message_logs) have no row-level policies. All brand scoping must be enforced in server actions.
+- **Supabase residue in the wild**: `grep -r "@supabase" src/` will still find imports during the transition. Do not add new ones; if you're touching a file that imports from Supabase, replace the call with the equivalent `pg`-pool call before merging.
- **Webhook event_id**: `log_communication_messages` never populates `event_id`, so the Resend webhook uses `customer_email + subject` lookup instead.
- **Mixed fulfillment orders**: An order can have both pickup and ship items. `get_shipping_orders` RPC returns orders with at least one `fulfillment = 'ship'` item.
- **SMS opt-in defaults**: `communication_contacts.sms_opt_in` defaults to `FALSE` (opt-out by default). `email_opt_in` defaults to `TRUE`. Always check `sms_opt_in` specifically for SMS sends, not `email_opt_in`.
\ No newline at end of file
diff --git a/MEMORY.md b/MEMORY.md
index 96d35ff..917ecd2 100644
--- a/MEMORY.md
+++ b/MEMORY.md
@@ -2,11 +2,40 @@
This file captures key context, decisions, fixes, and state from recent work so it survives across conversations.
-**Last updated:** 2026-06-03 (during Supabase migration apply session)
+**Last updated:** 2026-06-06 (Supabase → Postgres pivot)
---
-## Supabase CLI + Migrations Tooling
+## 🚨 Direction Pivot (2026-06-06) — Supabase → Postgres
+
+The platform is moving off Supabase entirely. We are connecting to **Postgres directly** (via `pg`), with **Auth.js (NextAuth v5)** handling authentication. See `CLAUDE.md` for the full updated architecture.
+
+### What changes immediately
+- **DB connection**: `DATABASE_URL` is the only required DB env var. No more `NEXT_PUBLIC_SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`, or `SUPABASE_ANON_KEY`.
+- **Migrations**: only the `pg` direct path in `supabase/push-migrations.js` is supported going forward. The Supabase CLI branch is dead code. The script reads `DATABASE_URL` from `.env.local` via `dotenv`.
+- **Code**: no new `@supabase/*` imports, no `rest/v1/` REST fetch, no Supabase JS client usage. Use a shared `pg` `Pool` (target location: `src/lib/db.ts`, **TBD — create during the cutover**).
+- **Auth**: legacy `rc_auth_uid` cookie + bespoke `/api/login` is being replaced by Auth.js. Until the Auth.js migration ships, the `dev_session` cookie remains the source of truth.
+- **Storage**: Supabase Storage (e.g. the `product-images` bucket created in migration 145) is going away. Need an S3-compatible alternative — **TBD**.
+
+### What's TBD / needs follow-up
+- [ ] `DATABASE_URL` for local dev (Neon? local Postgres? — user hasn't specified the Postgres host yet)
+- [ ] New connection layer: raw `pg` Pool vs Drizzle vs Prisma vs Neon serverless driver — **not decided**
+- [ ] Auth.js migration actually landing (currently "in progress" per CLAUDE.md)
+- [ ] Where do product images, brand logos, etc. live now? S3? Cloudflare R2? Re-encode as URL strings?
+- [ ] Whether the Supabase project (`wnzkhezyhnfzhkhiflrp`) gets shut down or kept read-only for the transition
+- [ ] Cutover sequencing: do we delete `@supabase/*` from `package.json` in one PR or incrementally?
+
+### Migration content that's now obsolete
+- **145 (product-images bucket)**: Supabase Storage bucket + RLS policies. Replaced by object store of choice.
+- **Any RLS policy on tables** (200 added several): the "no RLS, app-layer scoping" model still holds but the policies are inert in the new world.
+- The `supabase link --project-ref wnzkhezyhnfzhkhiflrp` setup is no longer needed for ongoing work.
+
+### Historical sections below
+The "Supabase CLI + Migrations Tooling" section that used to live at the top of this file describes the *previous* tooling. It is kept below as **historical record** of work that was already applied to the Supabase project. Do **not** follow its CLI instructions — use the `pg` direct path instead. Migration-file patch notes (091, 145, 148, 200, 201) are also kept as historical record of what got applied.
+
+---
+
+## Supabase CLI + Migrations Tooling *(SUPERSEDED — see Direction Pivot above)*
### Login + Link (done in this session)
- User ran `supabase login`
@@ -35,15 +64,23 @@ Key changes:
- Falls back to direct `pg` only if CLI path fails.
- Header comments updated with current recommended workflow.
-**Recommended commands now:**
+**Recommended commands now (Supabase CLI path — being phased out, use `pg` direct path going forward):**
```bash
+# Supabase CLI path (legacy — do not use going forward)
supabase login
supabase link --project-ref wnzkhezyhnfzhkhiflrp
-node supabase/push-migrations.js 148 # or any prefix
+node supabase/push-migrations.js 148 # CLI path
# or
npm run migrate:one 148
```
+```bash
+# Direct pg path (this is the future — only the pg branch is kept alive in the script)
+DATABASE_URL=postgres://... node supabase/push-migrations.js 148
+# or
+DATABASE_URL=postgres://... npm run migrate:one 148
+```
+
`npm run migrate` (no arg) will push every `*.sql` in order (use with caution).
---
@@ -133,19 +170,23 @@ Verification queries (post-apply) confirmed:
---
-## Current State / Gotchas
+## Current State / Gotchas (2026-06-06)
-- `supabase migration list` will show a lot of "pending" because the project's custom numeric migrations (00x_*.sql) were historically applied via the raw-SQL push script, not registered in Supabase's `schema_migrations` tracking table. Only early ones (001/002) + many timestamped migrations from other activity are tracked.
-- Many migrations are intentionally written to be re-runnable. Re-pushing a prefix is the supported workflow for this project.
-- When adding **new** migrations in the future, prefer the standard `supabase migration new` flow if possible, but the custom `push-migrations.js` + numeric prefix style is still the established pattern here.
-- Storage policies, RLS, SECURITY DEFINER functions, and brand-scoped data are all over the place — test carefully after big applies.
+- The Supabase CLI is no longer the recommended path. Use `DATABASE_URL` + `pg` directly. The `supabase/` directory is kept as a path for migrations tooling only.
+- The Postgres host/URL for local dev is **TBD** (not yet decided by the user). Until it's set, `npm run migrate` will fail at the `pg` connect step. (The Supabase project at `wnzkhezyhnfzhkhiflrp` may still exist as a fallback read-only target — unconfirmed.)
+- `supabase migration list` will show a lot of "pending" because the project's custom numeric migrations (00x_*.sql) were historically applied via the raw-SQL push script, not registered in Supabase's `schema_migrations` tracking table. This is mostly irrelevant now that we're moving off Supabase.
+- Many migrations are intentionally written to be re-runnable. Re-pushing a prefix is the supported workflow for this project — this still holds under direct `pg`.
+- When adding **new** migrations, use the established `supabase/push-migrations.js` + numeric prefix style (`NNN_descriptive_name.sql`). Do not introduce `supabase migration new` — that flow is going away with the CLI branch.
+- Storage policies (145), RLS policies (200), SECURITY DEFINER functions, and brand-scoped data are still in Postgres — test carefully after big applies. Brand scoping still relies on `p_brand_id` parameters in RPCs.
- CLAUDE.md already documents the overall migrate story and the `get_brand_settings` gotcha.
+- **Open question for next session:** confirm Postgres host + connection layer (raw `pg` vs Drizzle/Prisma) and start the actual cutover (drop `@supabase/*` deps, create `src/lib/db.ts`, replace cookie auth with Auth.js).
---
## How to Use This Memory
- Cat this file at the start of future sessions if context is needed: `cat MEMORY.md`
+- **Read the Direction Pivot section first** — it supersedes the older Supabase-flavored instructions.
- Update this file with new key facts, applied migrations, or new gotchas.
- Feel free to add dated sections.
diff --git a/middleware.ts b/middleware.ts
deleted file mode 100644
index 9b2287e..0000000
--- a/middleware.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { NextResponse, type NextRequest } from "next/server";
-
-const DEV_UID = "dev-user-00000000-0000-0000-0000-000000000000";
-
-export async function middleware(request: NextRequest) {
- const response = NextResponse.next({ request });
-
- // ── Dev session bypass (enabled in all envs for demo) ──────────────
- // Allow dev cookies via: document.cookie = "dev_session=platform_admin; path=/"
- const devSession = request.cookies.get("dev_session")?.value;
- const isDevMode = devSession === "platform_admin" || devSession === "brand_admin" || devSession === "store_employee";
- const rcAuthUid = request.cookies.get("rc_auth_uid")?.value;
-
- let authUid: string | null = null;
-
- if (isDevMode) {
- // Dev session only valid in development
- authUid = DEV_UID;
- } else if (rcAuthUid) {
- // rc_auth_uid is set by /api/login — treat as authenticated
- authUid = rcAuthUid;
- }
- // No rc_auth_uid in production → authUid stays null → redirect to /login
-
- const isAdmin = request.nextUrl.pathname.startsWith("/admin");
- const isLogin = request.nextUrl.pathname === "/login";
-
- if (isAdmin && !authUid) {
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
- // Auto-login for demo: no Supabase configured, no auth cookie present
- if (!supabaseUrl || !supabaseUrl.includes("supabase.co")) {
- const url = request.nextUrl.clone();
- url.pathname = "/admin";
- url.searchParams.set("demo", "1");
- const response = NextResponse.redirect(url);
- response.cookies.set("dev_session", "platform_admin", {
- path: "/",
- maxAge: 60 * 60 * 24,
- httpOnly: true,
- sameSite: "strict",
- });
- return response;
- }
- const url = request.nextUrl.clone();
- url.pathname = "/login";
- return NextResponse.redirect(url);
- }
-
- if (isLogin && authUid) {
- const url = request.nextUrl.clone();
- url.pathname = "/admin";
- return NextResponse.redirect(url);
- }
-
- return response;
-}
-
-export const config = {
- matcher: [
- "/admin/:path*",
- "/admin",
- "/login",
- ],
-};
\ No newline at end of file
diff --git a/package.json b/package.json
index f91f6b6..99c5f69 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "1.0.0",
"private": true,
"scripts": {
- "dev": "node fix-agents.js && next dev --webpack -H 0.0.0.0",
+ "dev": "node fix-agents.js && next dev --webpack -H 0.0.0.0 -p 4000",
"build": "next build --webpack",
"start": "next start",
"lint": "eslint",
@@ -15,6 +15,7 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.96.0",
+ "@auth/pg-adapter": "^1.11.2",
"@clerk/nextjs": "^7.4.2",
"@google/generative-ai": "^0.24.1",
"@gsap/react": "^2.1.2",
@@ -30,6 +31,7 @@
"gsap": "^3.15.0",
"lucide-react": "^1.17.0",
"next": "^16.2.6",
+ "next-auth": "^5.0.0-beta.31",
"next-themes": "^0.4.6",
"openai": "^6.37.0",
"papaparse": "^5.5.3",
@@ -50,6 +52,7 @@
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/papaparse": "^5.5.2",
+ "@types/pg": "^8.20.0",
"@types/qrcode": "^1.5.6",
"@types/react": "^19",
"@types/react-dom": "^19",
diff --git a/src/actions/auth-signin.ts b/src/actions/auth-signin.ts
new file mode 100644
index 0000000..88ed86b
--- /dev/null
+++ b/src/actions/auth-signin.ts
@@ -0,0 +1,56 @@
+"use server";
+
+import { signIn, signOut } from "@/lib/auth";
+import { AuthError } from "next-auth";
+
+/**
+ * Server actions that wrap the Auth.js v5 `signIn` / `signOut` API for
+ * use from client components.
+ *
+ * Why server actions?
+ * • The Auth.js v5 `signIn` function has to run on the server (it
+ * needs to set the session cookie, talk to the database adapter,
+ * and redirect the user to the OAuth provider).
+ * • Calling it from a client component via a server action keeps the
+ * client bundle small and avoids exposing the OAuth client secret.
+ *
+ * Usage from a client component:
+ *
+ *
+ * Usage for the dev credentials provider (dev only):
+ *
+ */
+
+export async function signInWithGoogle(): Promise {
+ await signIn("google", { redirectTo: "/admin" });
+}
+
+export async function signInWithDev(formData: FormData): Promise {
+ const username = String(formData.get("username") ?? "admin");
+ const password = String(formData.get("password") ?? "dev");
+ try {
+ await signIn("dev-login", {
+ username,
+ password,
+ redirectTo: "/admin",
+ });
+ } catch (e) {
+ // signIn() throws a `NEXT_REDIRECT` to navigate — let that through
+ // so the redirect actually happens. Re-throw any other error so the
+ // caller can render a meaningful message.
+ if (e instanceof AuthError) {
+ throw new Error(`Dev sign-in failed: ${e.type}`);
+ }
+ throw e;
+ }
+}
+
+export async function signOutAction(): Promise {
+ await signOut({ redirectTo: "/login" });
+}
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000..93eb609
--- /dev/null
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,16 @@
+import { handlers } from "@/lib/auth";
+
+/**
+ * Auth.js v5 catch-all route handler. Exposes:
+ * GET /api/auth/signin
+ * GET /api/auth/signout
+ * GET /api/auth/session
+ * GET /api/auth/csrf
+ * GET /api/auth/providers
+ * POST /api/auth/callback/:provider
+ * POST /api/auth/signin/:provider
+ * POST /api/auth/signout
+ *
+ * The actual OAuth + session logic is in `src/lib/auth.ts`.
+ */
+export const { GET, POST } = handlers;
diff --git a/src/app/login/LoginClient.tsx b/src/app/login/LoginClient.tsx
index 75c7889..e08fbe7 100644
--- a/src/app/login/LoginClient.tsx
+++ b/src/app/login/LoginClient.tsx
@@ -3,6 +3,7 @@
import { useState, useEffect, useCallback, Suspense } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
+import { signInWithGoogle, signInWithDev } from "@/actions/auth-signin";
function LoginForm() {
const [email, setEmail] = useState("");
@@ -124,6 +125,82 @@ function LoginForm() {
+ {/* Auth.js v5 — primary sign-in: Google OAuth */}
+
+
+ {/* Dev login (only visible in development) */}
+ {process.env.NODE_ENV !== "production" && (
+
+ )}
+
+
+
+
+
+ or sign in with email
+
+
+
+