Files

258 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 3060 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.