1. Replace stale supabase import with api in sweet-corn-box page —
src/lib/supabase.ts was removed when migrating to better-auth.
2. Stub bun-sqlite-dialect from @better-auth/kysely-adapter via webpack
alias — it imports DEFAULT_MIGRATION_TABLE which doesn't exist in
kysely 0.29.x and is never used (we use PostgresDialect, not bun-sqlite).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Batching all migrations into one psql session caused a hard error to put
Postgres into an aborted transaction state, blocking every subsequent
migration. Back to per-file calls with PAGER= still set to prevent hang.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
psql was launching less/more for query output in the CI shell, then
blocking indefinitely on "Press RETURN to continue". Also batch all
numbered migrations into one psql session instead of one process per file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dynamic port leasing was the root cause of all the "address already in use"
failures. Production now always uses 3010; dev still uses 3001. No more
.postgrest-port file, no more port scanning loop, no more race conditions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Multiple --filter name= flags are ANDed in docker ps, so combining
route_commerce_ and route-commerce- in one call matched nothing and
left db/minio running. Split into two calls, dedup with sort -u.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docker compose down was silently failing (|| true) when the compose file
was stale, leaving db/minio running on the old network subnet. The new
postgrest container then couldn't bind because Docker tried to reuse the
same subnet with conflicting container IPs.
Now: rm -f all containers matching route_commerce_/route-commerce- by name
first (works regardless of compose file state), then remove the network
explicitly so Docker allocates a clean subnet on the next up.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The three hardcoded pkill lines only covered ports 3011-3013. docker-proxy
can linger after compose down, causing "address already in use" when the new
stack races to bind the same port. Replace with a single pattern covering all
30xx ports and bump sleep to 5s.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Start Docker stack step was racing itself: docker compose down
errors were silently swallowed (|| true), leaving the previous
postgrest container in a half-torn-down state. fuser -k would kill
the listener, ss would see 3011 as free, the script would pick 3011,
and then docker compose up would fail to bind because the port was
in TIME_WAIT (or a stale docker-proxy listener was still holding it).
Three changes:
1. Don't swallow docker compose down errors. If the previous stack
isn't actually down, fail loudly so we can investigate.
2. Explicitly remove any surviving route_commerce_postgrest container
and kill any leftover docker-proxy listeners for ports 3011-3013
before picking a port.
3. Verify the postgrest container is gone after the cleanup. If it's
not, exit 1 instead of continuing into a guaranteed bind failure.
4. Port-picking loop now retries with a 1s sleep on each port (handles
TIME_WAIT settling), and uses a stricter ss grep that matches
127.0.0.1:<port> anchored on whitespace to avoid false positives
from substring matches (e.g. :30110 matching :3011).
Reproduces the failure mode from the run on 2026-06-06: postgrest
failed to bind 127.0.0.1:3011 with 'address already in use' even
though the script had just reported it as free.
- deploy/deploy.sh: idempotent deploy script with dynamic port
allocation (3011..30200), flock-based concurrency, atomic
.postgrest-port/.nextjs-port writes, port cleanup of the previous
deploy + dev stack, nginx config rendering+reload, healthchecks
with rollback, optional image pruning
- deploy/docker-compose.yml + Dockerfile.nextjs: example stack
consuming ${POSTGREST_HOST_PORT} / ${NEXTJS_HOST_PORT} (kept as
reference; the repo's root docker-compose.yml is the source of
truth for the actual production stack)
- deploy/nginx.conf.template: /api/* -> PostgREST, /* -> Next.js
- deploy/.env.production.example: managed port block + preserved secrets
- deploy/healthcheck.sh: standalone health probe (cron-friendly)
- deploy/Makefile: deploy/status/health/logs/down/rollback targets
- deploy/GITEA_SETUP.md: webhook vs Actions runner instructions
- deploy/README.md + deploy/.gitignore
Note: .gitea/workflows/deploy.yml was deliberately not added — the
existing workflow at that path on Gitea main is the source of truth
and is left untouched.
Previous attempts hardcoded 3011, but something on tyler's host is
holding 3011 too. Instead of guessing ports, make the deploy probe
for the first free port starting from 3011 and thread that choice
through the rest of the workflow.
How it works:
1. Start Docker stack frees 3001 + the previous deploy's chosen port
(read from .postgrest-port) so the lowest free port can be picked
back up
2. Loops from 3011 upward, checking ss -tln until it finds a free
port in the 3011-30200 range
3. Writes the chosen port to .postgrest-port in the workspace
4. Sets NEXT_PUBLIC_API_URL based on that port
5. Writes POSTGREST_HOST_PORT and NEXT_PUBLIC_API_URL to the .env
that docker compose reads
6. Build and Deploy steps read .postgrest-port to set their
NEXT_PUBLIC_API_URL dynamically — no more hardcoded 3011
The dev PostgREST (started outside docker compose for local development)
and the production PostgREST (started by docker compose up) were both
trying to bind 127.0.0.1:3001. Even with fuser -k in the deploy cleanup,
something on tyler's host (likely a process supervisor restarting the
dev PostgREST) keeps reclaiming 3001 between our check and the
docker bind.
Move the production PostgREST to host port 3011, keeping the container's
internal port at 3001 (PostgREST's default). This lets dev and prod
coexist on the same host without fighting over a port.
Update NEXT_PUBLIC_API_URL to http://localhost:3011 in:
- Build step env (so the build bakes in the right URL)
- Start Docker stack .env append (so docker compose + app see the URL)
- Deploy step .env.production writing (so runtime app uses 3011)
The previous attempt's `docker compose down` removed the project's
network, but on the subsequent `docker compose up`, the postgrest
container was being recreated rather than created from scratch, and
its network attachment still pointed to the now-orphaned old network
ID. The new db/minio containers attached to the freshly-created
network fine; only postgrest failed with 'network ... not found'.
`-d --force-recreate` tells docker compose to throw away any
existing container state and build from scratch, eliminating stale
network references.
On hosts with leftover dev processes or stuck containers from prior
attempts, docker compose fails with 'address already in use' when
trying to bind PostgREST to 127.0.0.1:3001. The previous deploy
attempt actually got as far as starting Postgres and MinIO, then
PostgREST failed at port binding — leaving the Postgres container
running, which would persist for the next attempt and keep
re-occupying 3001 indirectly.
Add a defensive pre-check: if 3001 is bound, kill the holder
(fuser -k) and remove any docker container publishing 3001, then
docker compose down --remove-orphans to clear stuck state. Sleep 3
to let the kernel release the port.
Same first-deploy bootstrap pattern as .env.example: 'Start Docker stack'
needs docker-compose.yml at $APP_DIR for 'docker compose up', and
'Apply migrations' needs supabase/migrations/ at $APP_DIR for psql.
Neither was copied there until the 'Deploy' step runs at the end of
the job — so the first deploy on a fresh host fails before any
runtime file is ever placed.
Each step now seeds the file/dir it needs from the workspace before
using it. The 'Deploy' step's later copy remains as a refresh for
subsequent deploys.
The Start Docker stack step does `cd $APP_DIR && [ -f .env ] || cp .env.example .env`
but never ensured .env.example existed in APP_DIR. The rsync in the Deploy step
also doesn't copy it. The first deploy on a fresh host therefore failed with
'cp: cannot stat .env.example' before ever bringing up the stack.
Fix: copy .env.example from the workspace into APP_DIR if it isn't already
there, before the cd.
Runs typecheck, lint, and build on every push to main and on pull requests.
Targets the self-hosted Gitea Actions runner (crispygoat-host-runner)
using labels [self-hosted, linux, ubuntu-latest].
Uses the local dev stack endpoints (PostgREST on :3001, MinIO on :9000,
Postgres on :5432) for env vars so no secrets need to be wired in.
Validates the full local development stack:
- Postgres connectivity
- PostgREST API on :3001
- MinIO object storage on :9000
- Next.js dev server on :4000
- Brand isolation via SECURITY DEFINER RPCs
- Auth via dev_session cookie (3 roles)
The captured production data still includes 3 test brands (Sunrise,
Green Valley, Orchard) created during the migration work. Promote the
note in 'Notes' to a fully reproducible post-restore step with the
exact DELETE statement and verification query.
Download 3 Tuxedo brand logos from Supabase Storage to local MinIO and
point the DB at portable /storage/... paths. Add a Next.js rewrite in
next.config.ts that proxies /storage/* to the configured MinIO endpoint.
Why: Next.js's image optimizer refuses to fetch upstream images whose
hostname resolves to a private IP. Local MinIO lives on 127.0.0.1, so
direct URLs (e.g. http://localhost:9000/...) get blocked with
"upstream image resolved to private ip [::1,127.0.0.1]".
The rewrite keeps URLs same-origin in the browser, so the optimizer's
private-IP check is bypassed. For production, set STORAGE_PUBLIC_URL to
the public MinIO endpoint and the same DB values work without change.
Also fix the same pattern in 3 client-rendered components that
hardcoded publicUrl(BUCKETS.BRAND_LOGOS, ...) for fallback assets:
- TuxedoVideoHero (hero video + Olathe Sweet dark logo)
- TimeTrackingFieldClient (field UI logo)
- tuxedo/about (about page logo)
email-service.ts keeps publicUrl() because Resend fetches URLs
server-side from its own network.
Documented the workflow and gotchas in docs/SUPABASE_DUMP_GUIDE.md.
Major changes:
- Drop synthesized base schema (supabase/synthesized/000_base_schema.sql) —
real pg_dump from Supabase is now the source of truth
- Switch better-auth from better-auth/minimal to better-auth (full)
- Add localhost:9000 (MinIO) to next.config images allowlist
- Add .data/, bin/ to gitignore (local runtime artifacts)
- Ignore supabase/captured/ (regenerate from Supabase as needed)
DB state after dump:
- 65 real tables (vs 70 synthesized)
- 252 functions
- 5 brands: Tuxedo Corn, Indian River Direct + 3 test brands
- 3 admin users, 1 communication template, 7 brand features
Pooler URL: aws-1-us-east-1.pooler.supabase.com:5432
user: postgres.wnzkhezyhnfzhkhiflrp
(NOT aws-0 — that one doesn't know this project)
Covers:
- Connection test (direct vs pooler, IPv4 vs IPv6)
- pg_dump commands (schema-only and data-only)
- Restore steps to local DB
- Verification queries
- Schema-per-brand restructure (future work)
Temporary schema derived from migration column references. Provides
workable local structure BEFORE the real Supabase pg_dump arrives.
When brother removes the Supabase spend cap:
1. pg_dump the real Supabase schema to supabase/captured_schema.sql
2. pg_dump the data to supabase/captured_data.sql
3. Drop everything in public schema
4. Apply the captured schema + data
5. Drop the synthesized file
The synthesized schema is NOT for production.
- docker-compose.yml: Postgres 16 Alpine with named volume, healthcheck
- .env.example: POSTGRES_* and DATABASE_URL template
- .gitignore: exclude db_data/ volume
Starting fresh, no data migration. App still wired to Supabase;
DB is ready for migrations to be applied.
- Add available_from and available_until fields to ProductFormValues type
- Add date picker inputs for seasonal availability in ProductFormModal
- Wire availability dates through ProductsClient to modal initial values
- Create migration 149 for database columns (already applied)
Replace the basic add/edit product modal with an editorial two-column
layout featuring Fraunces display serif, IBM Plex Mono section labels,
a custom drop zone, visual type cards, and pill toggles. Add a brand
selector visible only to platform admins to fix the 'Brand ID is
required' / RLS error when creating products without an assigned brand.
- Add ProductFormModal with two-column layout (image hero + form)
- Add Fraunces / IBM Plex Mono / Inter Tight via next/font/google
- Add atelier-* design system to globals.css
- Wire brand picker through page server component -> client
- Convert ProductsClient image handling to callback shape
- Move all submit/validation/upload logic into the new modal
The stops.date column was created as TIMESTAMPTZ but is used as a
calendar date throughout the app. Supabase returned it as a
'YYYY-MM-DD HH:MM:SS+TZ' string, which made the existing
'new Date(s.date + "T00:00:00")' parsing in the client produce
Invalid Date objects — leaving the Stops & Routes calendar empty.
Changing the column to DATE makes Supabase return a clean 'YYYY-MM-DD'
string that the existing local-time parsing handles correctly.
- ALTER stops.date TYPE DATE USING date::DATE
- Recreate get_public_stops_for_brand with date DATE in RETURNS TABLE
(DROP + CREATE required because CREATE OR REPLACE refuses to change
the OUT-parameter row type — PostgreSQL error 42P13)
The time (TEXT 'HH:MM') and cutoff_time (TIMESTAMPTZ) columns are
unchanged — they legitimately carry time-of-day information.
- Add @stripe/stripe-js + @stripe/react-stripe-js
- New src/lib/stripe-client.ts: cached loadStripe helper
- New src/actions/billing/retail-payment-intent.ts: server action
that creates a PaymentIntent with automatic_payment_methods
- New src/components/storefront/StripeExpressCheckout.tsx: embedded
ExpressCheckoutElement (Apple Pay, Google Pay, Link, PayPal) +
PaymentElement (card) + hosted-checkout fallback
- /checkout form is now controlled; StripeExpressCheckout reads
name/email/stop from form state and confirms in-page via
stripe.confirmPayment
- /checkout/success handles both ?session_id= and ?payment_intent=
so embedded + hosted flows both land on the same order-creation
page using the pending_checkout sessionStorage payload
- Export StopInfo from CartContext and select 'time' on the stops
fetch so the local Stop shape matches the context's StopInfo
- QuickCartSheet express buttons are visual shortcuts only;
/checkout auto-renders the real Apple Pay / Google Pay buttons
Requires NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY for the embedded path;
if missing the embedded section falls back to the hosted
Stripe Checkout button. npx tsc --noEmit passes.
Replace the table-only stops view with a three-tab 'Harvest Dispatch'
almanac dashboard.
- Calendar tab: month grid with stops as status-colored pins, day-detail
side panel, prev/next/today nav, weekend tint, sunrise gradient on
the current day.
- Locations tab: cards grouped by city+state with per-location stats
(stops/active/upcoming), date range, next-stop ribbon, and inline
expansion to list every stop at that location. Top of the tab shows
a six-card almanac stats strip (Locations/Cities/Stops/Active/Upcoming/
Drafts).
- List tab: condensed read-only list of all stops in date order.
Typography: load Fraunces (display), Manrope (body), and Fragment Mono
(mono) via next/font/google, wired into the Tailwind theme as
--font-display, --font-sans, --font-mono. Add typescript to devDeps
for tsc --noEmit in this environment.
Visual signature: cream parchment cards, forest green + clay accents,
Roman-numeral stat cells, paper-grain noise, binder-tab nav, route
number watermarks on location cards, harvest-pin SVG markers.
Verified with tsc --noEmit, eslint on the new files, and a 200 from
GET /admin/stops in the dev server.
- New route /tuxedo/products/sweet-corn-box with editorial Fraunces/JetBrains
Mono typography, paper-grain hero, tilted 'Peak Season 2026' stamp, and
sticky buy-box (desktop) + sticky mobile bottom bar with 48px+ tap targets
- QuickCartSheet slide-up drawer: mobile bottom sheet (spring + drag-to-close)
vs desktop right drawer, with Apple Pay / Google Pay / Shop Pay express row
and one-page 'Checkout' CTA — enables the user's specified 2-3 click flow
- CartContext.buyNow() — single-item cart replace + clear stop for 1-tap
'Quick Buy' that pushes directly to /checkout
- 4-feature check list, 100% Sweetness Guarantee, FAQ accordion, dark final
CTA strip, and proper SEO/Open Graph meta