Files
mc_cars_gmbh_infraestructure/AGENT.md
T

9.8 KiB
Raw Blame History

AGENT.md Operational notes for working on MC Cars

A compact field guide for anyone (human or AI) making changes to this stack. Skim this first.


1. The deployment model

  1. Portainer-native. No build: steps. Every service pulls a pre-built image. The web service uses nginx:1.27-alpine with bind-mounted static files.
  2. Absolute host paths. All bind mounts point to /mnt/user/appdata/mc-cars/.... Runtime state lives under /mnt/user/appdata/mc-cars/data/.
  3. No named volumes. Wiping data means rm -rf /mnt/user/appdata/mc-cars/data/db — not docker volume rm.
  4. Secrets live in .env; the .env ships with dev defaults. Any non-local deployment must rotate every key (see §6).
  5. External ports are in the 555xx range to avoid collisions on a busy host (55521 Kong, 55530 Studio, 55532 Postgres, 55543 Kong TLS, 55580 web).

2. Migration lifecycle

Migrations come in two flavours:

First-boot only (runs iff /mnt/user/appdata/mc-cars/data/db is empty)

  • supabase/migrations/00-run-init.sh — wrapper that runs 01-init.sql via psql so we can pre-create Supabase service roles (anon, authenticated, service_role, authenticator, supabase_auth_admin, supabase_storage_admin) that the official images otherwise assume already exist on their supabase/postgres image. We use plain postgres:15-alpine, so we create them ourselves.
  • supabase/migrations/01-init.sql — vehicles table, RLS, bucket row, seed demo vehicles.

Postgres' official entrypoint only processes /docker-entrypoint-initdb.d/ on an empty data dir. To re-run: wipe /mnt/user/appdata/mc-cars/data/db.

Post-boot (every time, idempotent)

  • supabase/migrations/post-boot.sql — seeds the admin auth.users row with must_change_password=true, creates the vehicle-photos bucket row if missing. Parameterized: psql -v admin_email=... -v admin_password=....
  • supabase/migrations/02-leads.sql — leads + customers + RPCs + supabase_realtime publication. Uses create if not exists, create or replace, and do $$ ... $$ with exists-checks so every object is idempotent.

Both are run by the post-init service, which gates on auth.users and storage.buckets being queryable before touching anything.

Adding a new migration:

  • If it's schema the app can't start without on a fresh DB → extend 01-init.sql.
  • If it's ongoing / idempotent → new file 03-*.sql, mount it in post-init, append one more psql -f line to the entrypoint.

3. Roles, RLS, and the mental model

Role How obtained Can do
anon Anon JWT (shipped to browser) select on active vehicles; insert on leads only
authenticated JWT from signInWithPassword Full CRUD on vehicles/leads/customers, execute RPCs
service_role Service JWT (never in browser) Bypasses RLS
  • Anon cannot read leads. The booking form is fire-and-forget by design. An abuser submitting with a stolen anon key sees only HTTP 201 with no row data.
  • RPCs are SECURITY INVOKER. They run as the caller, so auth.uid() returns the actual admin; RLS still applies.
  • qualify_lead is idempotent — calling it twice returns the existing customer row instead of raising.

4. Realtime wiring

  • Postgres command line forces wal_level=logical, max_wal_senders=10, max_replication_slots=10. Without logical, supabase/realtime crash-loops.
  • supabase/realtime:v2.30.23 connects as DB_USER=postgres (not supabase_admin). We're on the plain postgres:15-alpine image; the supabase_admin role does not exist.
  • SECRET_KEY_BASE must be ≥ 64 chars; the hardcoded value in docker-compose.yml is a dev default — rotate it on real deploys.
  • Replica identity full is set on leads/customers/vehicles so updates broadcast the complete row, not just the PK.
  • Kong route /realtime/v1/http://realtime:4000/socket (websocket upgrade headers handled by Kong's default config).

5. Kong traps

  • On Windows/WSL2, Docker Desktop's wslrelay intercepts 127.0.0.1:8000. On the production host Kong is exposed on 55521 (container 8000).
  • On a busy Unraid/Docker host all MC Cars ports live in the 555xx range to avoid collisions.
  • Kong needs KONG_PLUGINS: bundled,request-transformer,cors,key-auth,acl,basic-auth. Omitting acl breaks Studio.
  • kong.yml has one consumer per role with acl groups; plugins key-auth + acl gate which apikey (anon vs service_role) can reach which route.

6. Password rotation & admin bootstrap

The .env ADMIN_PASSWORD is a one-shot bootstrap seed. On first login:

  1. admin.js reads user.user_metadata.must_change_password.
  2. If true, the UI shows the "Passwort setzen" screen and refuses to proceed.
  3. Submit calls supabase.auth.updateUser({ password, data: { must_change_password: false } }).
  4. The frontend also rejects newPassword === loginPassword before sending.

Result: the live admin password differs from .env from the very first session. The .env value is useful only for a brand-new ./data/db.

Password min length is enforced server-side by GOTRUE_PASSWORD_MIN_LENGTH=10.


7. Frontend conventions

  • Only the anon key leaves the server. config.js is generated at container start via an inline entrypoint: command in docker-compose.yml (writes window.MCCARS_CONFIG={SUPABASE_URL,SUPABASE_ANON_KEY}). The service_role key is never mounted into the web container.
  • No entrypoint script file. An earlier design used frontend/99-config.sh bind-mounted into /docker-entrypoint.d/. Abandoned because Unraid's filesystem does not preserve Unix execute bits on bind mounts — the script was silently skipped. The inline entrypoint: in compose needs no file permissions.
  • frontend/config.js is git-ignored (generated at runtime, would commit stale localhost values otherwise).
  • index.html / admin.html load config.js with a ?v=<timestamp> query string (generated by a small inline document.write snippet) to defeat both browser and proxy caching.
  • The web service uses nginx:1.27-alpine directly (no build:). Static files and nginx.conf are bind-mounted. Updating the frontend is git pull + docker compose up -d --force-recreate web.
  • frontend/app.js (public) creates a Supabase client with persistSession: false — the public site never needs a session.
  • frontend/admin.js uses persistSession: true, storageKey: "mccars.auth" and a single realtime channel mccars-admin that subscribes to leads/customers/vehicles.
  • app.js writes vehicle_id (uuid) and denormalized vehicle_label ("BMW M3") into the lead at submit time, so the admin UI renders even if the vehicle is later deleted.

8. Common failure modes

Symptom Cause / Fix
auth container restart loop, "role supabase_auth_admin does not exist" 00-run-init.sh didn't run. Wipe /mnt/user/appdata/mc-cars/data/db, retry.
realtime container exits, "wal_level must be logical" db command: missing wal_level=logical.
502 on /realtime/v1/ through Kong Missing acl plugin in KONG_PLUGINS, or realtime not listed in kong.yml consumers.
Admin login works but Leads tab is empty RLS: session is anon instead of authenticated. Check that signInWithPassword succeeded and token is attached to subsequent requests.
Booking form submits but no row appears anon lacks INSERT grant on public.leads. Check 02-leads.sql ran (see post-init logs).
.sh files reject with \r: command not found CRLF line endings. dos2unix or re-save as LF.
Port 8000 "connection refused" on Windows Docker Desktop wslrelay. Use 55521 (production port).
config.js not generated, website uses fallback URL Unraid doesn't preserve execute bits — entrypoint script skipped. Fixed: config.js is now written by an inline entrypoint: command in docker-compose, no file needed.
Website loads but API calls fail ("Failed to fetch") config.js cached by NPM or browser with old localhost URL. The ?v=timestamp cache-buster on the <script> tag prevents this. Hard-refresh or check NPM "Cache Assets" toggle.
Git pull fails with "Your local changes would be overwritten" .env was modified on the NAS. Use git checkout -- .env && git pull, then re-apply the two URL lines with sed.

9. How to verify a working stack

docker compose ps                                            # all services up (post-init exited 0)
curl http://localhost:55521/rest/v1/vehicles?select=brand -H "apikey: $ANON" # 6 demo cars

In the browser:

  1. Open http://<host>:55580, submit the booking form.
  2. Open http://<host>:55580/admin.html, log in with .env bootstrap creds.
  3. Rotate the password when prompted.
  4. The lead you submitted appears in "Aktive Leads" — in real time.
  5. Click Qualifizieren. Row disappears from active, a new row appears in Kunden with the lead_id displayed.
  6. Open Supabase Studio at http://<host>:55530 and confirm the customers.lead_id FK matches.

10. Things explicitly NOT in this stack

  • No n8n. Email automation is out of scope here; do it downstream.
  • No Google Analytics / Google Reviews widget.
  • No "Anmelden/Registrieren" on the public site. Admin access is intentionally unlinked.
  • No logflare / analytics.
  • No edge-functions container — RPCs live in Postgres.