# MC Cars – Dockerized Supabase CRM Self-hosted Supabase stack + bilingual (DE/EN) public website + lead-management admin panel. Designed for Portainer deployment — no `build:` steps, all services use pre-built images with bind mounts. The host deployment root is `/mnt/user/appdata/mc-cars`. ## What's inside | Service | Image | Purpose | | ------------- | ------------------------------------ | ----------------------------------------- | | `db` | `postgres:15-alpine` | Postgres with `wal_level=logical` | | `auth` | `supabase/gotrue:v2.158.1` | Email+password auth (signup disabled) | | `rest` | `postgrest/postgrest:v12.2.0` | Auto-generated REST API | | `storage` | `supabase/storage-api:v1.11.13` | S3-like bucket API (backed by `./data/storage`) | | `imgproxy` | `darthsim/imgproxy:v3.8.0` | On-the-fly image transforms | | `realtime` | `supabase/realtime:v2.30.23` | Live `postgres_changes` subscriptions | | `meta` | `supabase/postgres-meta:v0.84.2` | Schema introspection for Studio | | `post-init` | `postgres:15-alpine` | Idempotent bootstrap: seed admin + migrations | | `kong` | `kong:2.8.1` | Single API gateway at `:55521` | | `studio` | `supabase/studio` | Supabase dashboard (`:55530`) | | `web` | `nginx:1.27-alpine` | Public site + admin panel (`:55580`) | ## Requirements - Docker Engine with Compose v2 (or Portainer with Stacks) - Free ports: `55521`, `55530`, `55532`, `55543`, `55580` ## Run ### Via Portainer (recommended) 1. Clone the repo onto the host: `git clone /mnt/user/appdata/mc-cars` 2. `mkdir -p /mnt/user/appdata/mc-cars/data/{db,storage}` 3. Edit `.env`: set `SITE_URL` and `SUPABASE_PUBLIC_URL` to your domain (see below) 4. Portainer → Stacks → Add stack → paste `docker-compose.yml` → paste `.env` into Environment variables → Deploy. > No `chmod` needed. `config.js` is generated by an inline shell command in `docker-compose.yml`, not a bind-mounted script file. ### Via CLI ```bash cd /mnt/user/appdata/mc-cars docker compose up -d ``` First boot pulls ~1.5 GB of images and runs migrations (`01-init.sql`, `post-boot.sql`, `02-leads.sql`). Give it 30–60 s to settle. ### Stop / reset ```bash docker compose down # stop, keep data rm -rf /mnt/user/appdata/mc-cars/data/db # FULL DB wipe (re-runs first-boot migrations) ``` ## URLs | Purpose | URL | | ------------------------------- | --------------------------------- | | Public website | http://\:55580 | | Admin panel | http://\:55580/admin.html | | Supabase Studio | http://\:55530 | | API gateway (Kong) | http://\:55521 | | Postgres | `:55532` | > Admin access is deliberately **not** linked from the public site. Bookmark it. ## Credentials (dev defaults – rotate before any deployment) | Account | Value | | -------------------- | ---------------------------------------- | | Admin bootstrap user | `admin@mccars.local` / `mc-cars-admin` | | Postgres superuser | `postgres` / see `POSTGRES_PASSWORD` | The admin is seeded with `must_change_password = true` in `raw_user_meta_data`. On first login the UI **forces** a rotation and refuses to reuse the bootstrap password. The real working password never equals `.env`. ## Data model - `public.vehicles` — fleet, public-readable where `is_active`. - `public.leads` — booking form submissions. `anon` may `INSERT` only; `authenticated` has full CRUD. Status: `new | qualified | disqualified`. - `public.customers` — created **only** by qualifying a lead. Hard FK `lead_id` preserves the audit link to the originating lead. - RPCs: `qualify_lead(uuid, text)`, `disqualify_lead(uuid, text)`, `reopen_lead(uuid)` — transactional, `SECURITY INVOKER`, `authenticated` only. - Realtime: `supabase_realtime` publication broadcasts inserts/updates on leads, customers, vehicles. ## Environment: two variables per deployment Only two lines in `.env` need changing between environments: | Variable | Local dev | Production | |---|---|---| | `SITE_URL` | `http://localhost:55580` | `https://your.domain.com` | | `SUPABASE_PUBLIC_URL` | `http://localhost:55521` | `https://your.domain.com` | All other GoTrue URLs (`API_EXTERNAL_URL`, `GOTRUE_SITE_URL`, `GOTRUE_URI_ALLOW_LIST`) are derived automatically in `docker-compose.yml`. On the NAS: ```bash sed -i 's|SITE_URL=.*|SITE_URL=https://your.domain.com|' .env sed -i 's|SUPABASE_PUBLIC_URL=.*|SUPABASE_PUBLIC_URL=https://your.domain.com|' .env docker compose up -d --force-recreate web ``` ## Deployment & portability Runtime state under `/mnt/user/appdata/mc-cars/data/`: ``` data/ ├── db/ # Postgres cluster (bind mount) └── storage/ # vehicle-photos bucket content ``` All bind mounts in `docker-compose.yml` use absolute paths under `/mnt/user/appdata/mc-cars`. Clone the repo there, deploy as a Portainer stack, done. No `build:` steps — every service pulls a pre-built image. To put behind **Nginx Proxy Manager** with a single public domain: *Details tab:* Scheme `http`, Forward to `:55580`, **Cache Assets OFF**, **Websockets Support ON**. *SSL tab:* your cert, **Force SSL ON**, **HTTP/2 Support ON**. *Advanced tab (⚙️):* paste these location blocks: ```nginx location /auth/v1/ { proxy_pass http://:55521; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /rest/v1/ { proxy_pass http://:55521; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /realtime/v1/ { proxy_pass http://:55521; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } location /storage/v1/ { proxy_pass http://:55521; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } ``` Do **not** expose `/pg/` or Studio publicly. ## Project layout ``` MC Cars/ ├── docker-compose.yml # full stack, bind-mount portability ├── .env # dev secrets / tunables ├── data/ # runtime state (git-ignored) ├── supabase/ │ ├── kong.yml # gateway routes (/auth, /rest, /realtime, /storage, /pg) │ └── migrations/ │ ├── 00-run-init.sh # creates supabase service roles │ ├── 01-init.sql # vehicles + bucket + seed cars │ ├── post-boot.sql # admin user (must_change_password) + bucket row │ └── 02-leads.sql # leads, customers, RPCs, realtime publication ├── frontend/ │ ├── nginx.conf │ ├── index.html # public DE/EN site, booking form -> leads │ ├── admin.html # auth-gated CRM │ ├── app.js │ ├── admin.js # realtime + qualify/disqualify + password change │ ├── config.js # generated at container start (git-ignored) │ ├── i18n.js │ ├── styles.css │ ├── impressum.html │ └── datenschutz.html ├── .gitattributes # enforces LF on .sh files ├── AGENT.md # findings, conventions, traps └── ARQUITECTURE.md # architecture deep-dive ``` See [AGENT.md](AGENT.md) and [ARQUITECTURE.md](ARQUITECTURE.md) for everything deeper.