# 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 website (`:55580`)                 |
| `web-admin`   | `nginx:1.27-alpine`                  | Admin web entrypoint (`:55581`)           |
| `n8n`         | `n8nio/n8n:latest`                   | Automation UI/API (`:55590`)              |
| `gotenberg`   | `gotenberg/gotenberg:8`              | DOCX/PDF conversion (internal only)       |

## Requirements

- Docker Engine with Compose v2 (or Portainer with Stacks)
- Free ports: `55521`, `55530`, `55532`, `55543`, `55580`, `55581`, `55590`

## Run

### Local Dev (Windows/macOS/Linux)

Use the local override so bind mounts point to this repository (for example `./data/db`, `./data/storage`, `./data/n8n`).

Start local stack:

```bash
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build
```

Stop local stack:

```bash
docker compose -f docker-compose.yml -f docker-compose.local.yml down
```

Delete local mounts and recreate from scratch:

```bash
docker compose -f docker-compose.yml -f docker-compose.local.yml down -v --remove-orphans
rm -rf ./data/db ./data/storage ./data/n8n
mkdir -p ./data/db ./data/storage ./data/n8n
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build
```

PowerShell equivalent for cleanup:

```powershell
docker compose -f docker-compose.yml -f docker-compose.local.yml down -v --remove-orphans
Remove-Item .\data\db,.\data\storage,.\data\n8n -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path .\data\db,.\data\storage,.\data\n8n -Force | Out-Null
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build
```

### Via Portainer (recommended)

1. Clone the repo onto the host: `git clone <repo> /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`, `08-backend-pricing-and-security.sql`, `09-site-settings.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://\<host\>:55580             |
| Admin web (dedicated nginx)     | http://\<host\>:55581             |
| Admin page                      | http://\<host\>:55581/admin.html  |
| Supabase Studio                 | http://\<host\>:55530             |
| API gateway (Kong)              | http://\<host\>:55521             |
| n8n                             | http://\<host\>:55590             |
| Postgres                        | `<host>: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 with server-computed pricing. `anon` may `INSERT` only (via `create_lead` RPC); `authenticated` has full CRUD. Status: `new | qualified | disqualified`.
- `public.lead_attachments` — ID documents and income proofs per lead. Max 1 of each enforced by unique partial index.
- `public.customers` — created **only** by qualifying a lead. Hard FK `lead_id` preserves the audit link to the originating lead.
- `public.sales_orders` — rental orders created during qualification, contain pricing snapshot.
- `public.site_settings` — key-value settings table (e.g. `hero_image_url`). Publicly readable, admin-writable.
- RPCs: `calculate_price(uuid, date, date)` (public pricing), `create_lead(...)` (server-side submission), `qualify_lead(uuid, text)`, `disqualify_lead(uuid, text)`, `reopen_lead(uuid)` — transactional, `SECURITY INVOKER`, `authenticated` only (except calculate_price and create_lead which are anon-accessible).
- Realtime: `supabase_realtime` publication broadcasts inserts/updates on leads, customers, vehicles.

## Environment: three variables per deployment

Three variables 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` |
| `N8N_WEBHOOK_URL` | `http://localhost:55521/webhook/manual-email-send` | `https://your.domain.com/webhook/manual-email-send` |

All other GoTrue URLs (`API_EXTERNAL_URL`, `GOTRUE_SITE_URL`, `GOTRUE_URI_ALLOW_LIST`) are derived automatically in `docker-compose.yml`.

### Quick setup with deploy-setup.sh

Use the included deployment script to update all environment variables at once:

```bash
./deploy-setup.sh https://www.mc-cars.at
```

This updates `.env` and outputs the configuration. Then restart:

```bash
docker compose down
docker compose up -d --build
```

### Manual setup (legacy sed method)

```bash
sed -i 's|SITE_URL=.*|SITE_URL=https://www.mc-cars.at|' .env
sed -i 's|SUPABASE_PUBLIC_URL=.*|SUPABASE_PUBLIC_URL=https://www.mc-cars.at|' .env
sed -i 's|N8N_WEBHOOK_URL=.*|N8N_WEBHOOK_URL=https://www.mc-cars.at/webhook/manual-email-send|' .env
docker compose up -d --build
```

### How n8n webhooks work

- n8n runs internally (not exposed to the internet)
- Kong API gateway proxies `/webhook/*` traffic to internal n8n
- Browser requests to `https://your.domain.com/webhook/manual-email-send` route through Kong → n8n
- Frontend config is generated at container startup from `N8N_WEBHOOK_URL` environment variable

See [N8N_WEBHOOK_ROUTING.md](N8N_WEBHOOK_ROUTING.md) for full architecture details.

## 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 `<NAS IP>: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://<NAS IP>: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://<NAS IP>: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://<NAS IP>: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://<NAS IP>: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
│       ├── 08-backend-pricing-and-security.sql  # calculate_price RPC, refactored create_lead, document security
│       └── 09-site-settings.sql                 # site_settings table + hero_image_url seed
├── frontend/
│   ├── nginx.conf
│   ├── index.html             # public DE/EN site, booking form -> leads
│   ├── admin.html             # auth-gated CRM + settings panel
│   ├── app.js                 # dynamic hero image, server-side pricing sidebar
│   ├── admin.js               # realtime + qualify/disqualify + password change + settings
│   ├── config.js              # generated at container start (git-ignored)
│   ├── i18n.js
│   ├── styles.css             # CSS-variable hero image with fallback
│   ├── 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.
