# MEMORY.md — Persistent Session Context This file captures key context, decisions, fixes, and state from recent work so it survives across conversations. **Last updated:** 2026-06-25 (added current-state header; restored "Supabase → Postgres" pivot as the canonical entry point for new readers; documented the SSH-based Gitea workflow) > **Current state (read first).** Auth is **Neon Auth (Better Auth)** — see `CLAUDE.md`. The "Auth.js v5 wiring complete — 2026-06-06" entry further down is **historical** (it describes work that was subsequently replaced by the Neon Auth migration; `next-auth` remains in `package.json` as a vestigial dep but is no longer imported from `src/`). The Supabase → Postgres pivot section below is the canonical cutover record. The "GitHub origin" branch notes are also historical — the only remote is the Gitea `origin` (see `CLAUDE.md` "Canonical Remote"). Middleware lives at `src/proxy.ts`, not `src/middleware.ts` (Next.js 16+ convention). Repo operations use **SSH** (`~/.ssh/id_ed25519_crispygoat`) — no Gitea API token is provisioned in this env, so `tea`/`gitea-mcp`/REST API are unavailable for write operations; PRs are opened by `git push` + clicking the URL the Gitea push hook prints. ## 2026-06: `admin_users` schema — extra columns for create-user flow (migration 0043) The 0001 schema's `admin_users` table has just `name` plus the role-derived `can_manage_*` columns (`can_manage_orders`, `can_manage_products`, `can_manage_stops`, `can_manage_customers`, `can_manage_wholesale`, `can_manage_billing`, `can_manage_settings`, `can_manage_water_log`, `can_manage_time_tracking`, `can_manage_route_trace`, `can_manage_reports`, `can_manage_communications`). But `src/actions/admin/users.ts` was written against a richer schema: it INSERTs and SELECTs `display_name`, `phone_number`, `brand_id`, `can_manage_pickup/messages/refunds/users`, `active`, `must_change_password`, `auth_provider`, `auth_subject`, `last_login`. Submitting the create-user form produced `column "display_name" of relation "admin_users" does not exist`. Migration `db/migrations/0043_admin_users_extra_columns.sql` adds all twelve missing columns (`ADD COLUMN IF NOT EXISTS`, re-runnable). `db/schema/brands.ts`'s Drizzle `adminUsers` definition was extended in lockstep. **Important**: the four extra `can_manage_*` flags (pickup / messages / refunds / users) are **vestigial** — `getAdminUser()` in `lib/admin-permissions.ts` derives per-user permissions from the role via `permissionsForRole()`, never reads those columns. They exist only so the create-user form's per-user permission toggles persist; they have no runtime authorization effect. Cleanup target: either (a) read them in the role-derived lookup to support real per-user overrides, or (b) drop them from the form. The `brand_id` column on `admin_users` is a **denormalization** of the `admin_user_brands` link table (which is the source of truth, and is what `getAdminUser()` reads). The action's `getAdminUsers` joins on `au.brand_id` so keeping the column avoids rewriting that SELECT. Future cleanup can drop the denormalized column and migrate the SELECT to join through the link table. ## 2026-06: CI migration failures on re-deploy (0001_init.sql "already exists") Prod DATABASE_URL already had the schema from the first successful bootstrap. The deploy workflow runs `npm run migrate:one` on every push (after neon_auth preflight). `scripts/migrate.js` has `_migrations` tracking + skip, but the row for `0001_init.sql` was never recorded (the tracking logic landed after the initial apply, or an apply happened outside the runner). `db/migrations/0001_init.sql` header *claimed* "CREATE TABLE IF NOT EXISTS" but the actual statements were plain `CREATE TABLE`, plain `CREATE INDEX`, and unguarded `CREATE TRIGGER`. Result: every subsequent deploy hit `relation "admin_users" already exists` (and would have hit index/trigger dups too) inside the runner's BEGIN, causing ROLLBACK + failure of the whole "Run migrations" job. ### Fixes applied - Made `0001_init.sql` truly re-runnable: - All `CREATE TABLE` → `CREATE TABLE IF NOT EXISTS` - All `CREATE INDEX` / `CREATE UNIQUE INDEX` → `... IF NOT EXISTS` - Every `CREATE TRIGGER` wrapped in a `DO $$ IF NOT EXISTS (pg_trigger check) THEN CREATE TRIGGER ... END IF; $$` guard - Removed the file-level `BEGIN; ... COMMIT;` (the runner owns the tx; this also prevents inner-COMMIT from ending the runner tx early). - `0002_admin_password.sql` had its tx wrapper removed for consistency (its ALTER was already `IF NOT EXISTS`). - Hardened `scripts/migrate.js`: - Added `ensureTracked()` repair: for 0001/0002, if the core objects (admin_users table, or the password_hash column) already exist in the target DB but the tracking row is absent, we INSERT the row (ON CONFLICT DO NOTHING) and skip the file. Logs "repaired tracking". - Hardened `.gitea/workflows/deploy.yml` "Run migrations" step: - Added an inline pre-repair node snippet (same idea) right before `npm run migrate:one`. This protects even if an older runner is checked out. - The existing neon_auth preflight + post `admin_users` verification remain as the hard gate. - Updated header comments and docs in the files. After this, `npm run migrate` / deploys on an already-initialized DB will log the "repaired" or "already applied" lines for 0001 and proceed cleanly. The shipped `scripts/migrate.js` + `db/migrations/` on the target server also benefit for emergency recovery runs. See also the plan doc referenced in deploy.yml for the broader reliability work. --- ## 🚨 Direction Pivot (2026-06-06) — Supabase → Postgres **Status: COMPLETE (2026-06).** All Supabase references removed from the codebase: - `supabase/` directory deleted (`config.toml`, `push-migrations.js`, `ADMIN_CREATE_STOP_FIX.sql`, and 137 archived migration files in `supabase/migrations/.archived/` are gone). - `@supabase/ssr` and `@supabase/supabase-js` removed from `package.json`. - `next.config.ts` no longer whitelists `*.supabase.co` for images; `vercel.json` CSP no longer allows `https://*.supabase.co` in `connect-src`. - `eslint.config.mjs` no longer ignores `supabase/**`. - All `src/`, `tests/`, and `db/` files are free of `@supabase/*` imports, `rest/v1/` calls, and `*.supabase.co` references. - Migrations are now applied by `scripts/migrate.js` (which uses `pg` + `DATABASE_URL`); the Supabase CLI branch is gone with the deleted script. This section is kept as **historical record** of the cutover. The "Supabase CLI + Migrations Tooling" section further below is also historical. ### Original pivot summary (kept for context) The platform moved off Supabase entirely. We connect to **Postgres directly** (via `pg`), with **Neon Auth (Better Auth)** handling authentication. See `CLAUDE.md` for the current architecture. ### What changed at the time - **DB connection**: `DATABASE_URL` (with `DATABASE_ADMIN_URL` optional override for DDL) is the only required DB env var. No more `NEXT_PUBLIC_SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`, or `SUPABASE_ANON_KEY`. - **Migrations**: `scripts/migrate.js` is the canonical runner. It reads `DATABASE_URL` from `.env.local` via `dotenv`, tracks applied files in `_migrations`, and applies any new files in `db/migrations/` in lexical order inside a transaction. - **Code**: zero `@supabase/*` imports, zero `rest/v1/` REST fetches, zero Supabase JS client usage. One shared `pg` `Pool` in `src/lib/db.ts`; Drizzle on top of it at `db/client.ts` for typed reads + RLS-style brand scoping. - **Auth**: Neon Auth (Better Auth) is the production path. `dev_session` cookie is the local-only bypass. - **Storage**: object store migration (S3-compatible) is independent of the Supabase purge and is tracked separately. ### 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 row-level policies, app-layer scoping" model still holds but the policies are inert in the new world. ### Historical sections below The "Supabase CLI + Migrations Tooling" section further down describes the *previous* tooling. It is kept as **historical record** of work that was already applied to the Supabase project. Do **not** follow its CLI instructions — use `scripts/migrate.js` (or `npm run migrate`) 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; script and CLI are no longer in the repo)* ### Login + Link (done in this session) - User ran `supabase login` - We ran: `supabase link --project-ref wnzkhezyhnfzhkhiflrp` - Link succeeded. Project appears as **LINKED** (●) in `supabase projects list`. - Link state lived under `supabase/.temp/` (project-ref, linked-project.json, pooler-url, etc.). **Not** the legacy `.supabase/config.toml` at project root. - This enabled `supabase db query --linked`, `supabase migration list`, etc. ### Important Environment Note - Direct Postgres connections (`db.wnzkhezyhnfzhkhiflrp.supabase.co:5432` using service role key as password) **do not work** from this workspace: - `getaddrinfo ENOTFOUND` - IPv6 "network is unreachable" in some cases - HTTPS / Supabase REST / Management API work fine. - Therefore the `push-migrations.js` **CLI path** (using linked project) was the only reliable way to apply migrations here. ### Migration Script (deleted 2026-06 — use `scripts/migrate.js` instead) File: `supabase/push-migrations.js` *(deleted)* Key behavior (historical): - Detection logic recognized modern link: `supabase/.temp/project-ref` (in addition to old `.supabase/config.toml`). - `pushWithCli()` applied files one-by-one using: ```bash supabase db query --linked --file "supabase/migrations/NNN_foo.sql" ``` (Previously tried `db push --db-url ...` which used direct connection and failed here.) - Fell back to direct `pg` only if CLI path failed. **Historical commands (Supabase CLI path — both scripts are gone):** ```bash # Supabase CLI path (legacy — script deleted, do not use) supabase login supabase link --project-ref wnzkhezyhnfzhkhiflrp node supabase/push-migrations.js 148 # CLI path (script deleted) # or npm run migrate:one 148 ``` ```bash # Direct pg path (this was the future — now the only path, via scripts/migrate.js) DATABASE_URL=postgres://... npm run migrate:one 148 ``` `npm run migrate` (no arg) pushes every `*.sql` in `db/migrations/` in order (use with caution). --- ## Migration Files Patched in This Session These changes were required to make the files actually apply successfully against the live remote schema (column drift, syntax errors, non-idempotent statements, pre-existing tables). ### 091_brand_plan_tier.sql - Fixed syntax error: extra `)` in `get_brand_plan_info`: ```sql ... date >= date_trunc('month', now())); -- was broken ``` → `now());` ### 145_create_product_images_bucket.sql - Column name: `allowedMimeTypes` → `allowed_mime_types` (current Supabase storage.buckets schema uses snake_case). - Made idempotent: - Bucket insert now uses `WHERE NOT EXISTS (...)` (handles both id and name unique constraints). - Added `DROP POLICY IF EXISTS ...` before each `CREATE POLICY` (so re-runs don't fail). Policies created: - "Public can view product-images" - "Admins can upload product-images" ### 148_public_stops_rpc.sql - `time` is a reserved word in Postgres. - Quoted the output column and the source reference: ```sql RETURNS TABLE ( ..., "time" TEXT, ... ) ... s."time", ``` - GRANTs to anon/authenticated/service_role added at bottom. ### 200_production_features.sql - `user_activity_logs` table originated in `036_user_activity_logs.sql` (no `brand_id`, different column names: `activity_type` + `details`). - 200's `CREATE TABLE IF NOT EXISTS` was a no-op. - Its policies + indexes assumed `brand_id`, `action`, `resource_type`, `resource_id`, `metadata`. - Added after the CREATE TABLE block: ```sql ALTER TABLE user_activity_logs ADD COLUMN IF NOT EXISTS brand_id UUID REFERENCES brands(id) ON DELETE SET NULL, ... (other columns) ... ``` - This unblocked policy creation and indexes. Also added many new tables (referral_*, changelogs, onboarding_progress, api_keys, notification_preferences) + RLS policies + indexes. ### 201_seed_data.sql - Heavily truncated. - Original contained large demo INSERTs for products, stops, orders, order_items, wholesale_customers, communication_contacts, water_*, admin_users, etc. - These used outdated column lists vs. the live DB (e.g. products: `unit`/`category`/`sku`/`is_active` vs current `type`/`active`/`pickup_type`/`is_taxable`; stops used `name`/`scheduled_at`/`postal_code` vs `location`/`date`+`time`/`zip`/`slug`). - Kept only the 3 demo brands INSERT (now works because 091 added `plan_tier` + limit columns). - Added explanatory comment. - File now ends cleanly after `COMMIT;`. (The demo brands with enterprise/farm/starter + plan limits were successfully inserted.) ### Other notes on 147 / 202 - `147_admin_create_stop_rpcs.sql` and `202_fix_admin_create_stop.sql` were also present/modified in the working tree (related to admin stop creation fixes mentioned in CLAUDE.md). --- ## Successfully Applied (this session, via updated script) Batches included (not exhaustive): - 084 (shipments) - 091 (plan tier + get_brand_plan_info) - 142 (integration_credentials / resend + twilio RPCs) - 143 (enable_route_trace via set_brand_feature) - 144 (time_tracking_worker_number_rls) - 145 (product-images bucket + policies) - 146 (sitemap stops RPC) - 147 (admin create stop RPCs) - 148 (public stops RPC) — verified working - 200 (production features) - 201 (brands seed only) - 202 (admin create stop fix) Verification queries (post-apply) confirmed: - shipments table exists - get_resend_credentials / get_public_stops_for_brand / get_active_stops_with_brand functions exist - product-images bucket + its two policies exist - demo brand (sunrise-farms) has plan_tier = 'enterprise' - etc. --- ## Current State / Gotchas (2026-06) - Use `DATABASE_URL` + `pg` directly (via `scripts/migrate.js`). The Supabase CLI and `supabase/push-migrations.js` are no longer in the repo. - `npm run migrate` applies every pending `db/migrations/*.sql` in lexical order inside transactions; `_migrations` tracks which files are already applied. - Many migrations are intentionally written to be re-runnable (`CREATE OR REPLACE FUNCTION`, `CREATE TABLE IF NOT EXISTS`, guarded triggers). Re-pushing a prefix is safe. - When adding **new** migrations, use the established numeric prefix style (`NNN_descriptive_name.sql`) and place them in `db/migrations/`. No Supabase CLI command is involved. - 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 (plus the per-request `app.current_brand_id` GUC used by Drizzle). - CLAUDE.md already documents the overall migrate story and the `get_brand_settings` gotcha. --- ## 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. --- ## Unrelated / Other Changes in Tree (as of last git status) (See `git status --short` and `git diff` for full picture. Includes stop-related action + RPC fixes, various debug scripts in `scripts/`, a pricing assessment doc, etc.) This MEMORY.md originally focused on the Supabase login / link / migration tooling + SQL repair work from the immediate request. The pivot section now records the completion of the Supabase → Postgres migration. --- ## Admin Functionality Pass (Codex Review Fixes) — 2026-06-03 **Source:** Codex AI review of admin "apps" + explicit user request for a full admin functions pass (testing for functionality across orders, products, stops, communications/Harvest Reach, wholesale, water-log, time-tracking, route-trace, settings/billing/integrations/ai, import, reports, etc.). **Key fixes implemented in this pass (prioritized from review blockers):** - **Order creation restored** (biggest blocker): - New server action `src/actions/orders/create-admin-order.ts` — wraps the existing `create_order_with_items` RPC with full adminUser + can_manage_orders + brand scoping. - Updated `AdminOrdersPanel` (and orders server page) to support `?new=true` (from dashboard quick actions): opens a functional modal with customer fields, stop selector (or ship-only), dynamic product item picker (qty/price/fulfillment), live total, and submit. - Fixed "Create your first order" link and added `/admin/orders/new` redirect page for legacy links. - Success: toast + redirect to clean list (new order visible after refresh). - **Product edit** made reliable: confirmed flow through `ProductEditForm` + `updateProduct` + router.refresh(). The list client (`ProductsClient`) also has its own edit paths; save now has clear success state. - **Form accessibility & validation foundation** (cross-cutting, affects dozens of admin forms): - Enhanced `AdminInput` + inputs in `src/components/admin/design-system/AdminFormElements.tsx`: proper `htmlFor`/`id` generation + association, forwards `required` + `aria-required` + `aria-describedby` (for help/error). - Consumers now get real programmatic labels and required semantics (visual * was already there). - **Integrations fields no longer wrongly masked**: - `IntegrationsClientPage.tsx`: non-secret fields (From Email, From Name, Phone Number for Resend/Twilio) now render as plain text. Only `isSecret` fields default to password + toggle. - **Advanced / AI settings routes** now expose real (if lightweight) content: - `/admin/advanced` renders a useful landing with direct links to the actual sub-config (AI, Integrations, Square, Shipping) instead of pure redirect. - **Water Log admin actions hardened** (example of systematic permission pass): - `createWaterHeadgate` (and similar creates noted in audit) now call `getAdminUser` + `can_manage_water_log` guard + service key (were previously using anon key with no check in the action). - **Other admin surface improvements** (spot checks across the "few different apps"): - Stops, Wholesale, Time Tracking, Route Trace, Import, Users, Reports, etc. — CRUD links, permission gates, and empty states exercised via code paths. No new breakage introduced. - Added "New Order" button directly inside the Orders panel for discoverability. - Minor: order "new" handling no longer 404s. **Billing & Comms** (noted as confusing in review): - Billing reconciliation and Harvest Reach compose dedup + preview visibility left as high-priority follow-ups (data sources are in `getBrandPlanInfo` + client layers; comms has two composer mounts). The foundation (action + a11y + links) makes further polish straightforward. - "Invalid scope" warnings: not reproduced in admin paths during this pass (likely originated in homepage/GSAP or unauthed feature flag calls with null brandId); admin flows now consistently thread brandId. **Non-destructive approach followed** (per Codex + user): focused on reads + safe test creates/updates via dev auth. No production sends, billing changes, or mass deletes. **How to test the admin pass (dev mode)**: - `npm run dev` - Login via dev flow as `platform_admin` (full) or `brand_admin`. - Dashboard → New Order (or /admin/orders?new=true) → create a test order with items → verify appears in list + detail. - Products list → edit a product → save → confirm update visible. - Settings → Integrations: non-secret fields (email/name/phone) visible as text. - Settings → Advanced: now has real cards/links. - Forms across admin: labels are clickable (focus input), required fields have `required` + visual *. - Water Log (if enabled): headgate/irrigator creates now properly gated. - Run `npx tsc --noEmit` and `npm run build` for type/build health. **Remaining per review (recommended next after this pass)**: - Full billing state unification (one source of truth for "active sub + addons + usage"). - Harvest Reach: single compose experience + guaranteed visible preview result. - Homepage/animations/counters, Tuxedo buyer path (product overlap, 0x0 cart buttons, empty checkout), public nav leaks, contact label bug, year strings. - Deeper empty-state + error UX polish in the remaining admin apps. - Add a formal `docs/ADMIN_FUNCTIONALITY_CHECKLIST.md` for future regression passes. **Files changed in this pass (high level):** new create-admin-order action + supporting UI in orders panel/page + new redirect page, design-system a11y enhancements, integrations masking fix, advanced page content, water guard improvement, dashboard link fix, various small admin surface tweaks + this MEMORY update. See the session plan.md for the full detailed execution guide that was followed. **Status:** Core admin blockers from the Codex review (order creation, product edit reliability, forms a11y, integrations, advanced settings, permission examples) addressed. The different backend "apps" are now significantly more testable/functional. --- ## Codex Review — Round 2 Pass (2026-06-03) Follow-up pass on the original Codex review covering public site, buyer path, billing, comms, layout/a11y. Five sub-agents + one manual fix. ### Round 2 commits 1. `fix(home): resilient homepage animations + anchors + counters` (subagent 1) - GSAP scope guards, reduced-motion support, counter animation switch to proxy object (textContent tween was unreliable), IO + rAF fallback - Section ids: #features, #stats, #reviews (anchors now resolve) - Story section 300vh → 140vh (kills blank scroll region) - Removed duplicate inline footer in HeroSection (LandingPageWrapper `