238 lines
11 KiB
Markdown
238 lines
11 KiB
Markdown
# 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: 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 (example):
|
||
```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
|
||
```
|
||
|
||
For mc-cars.at:
|
||
```bash
|
||
sed -i 's|SITE_URL=.*|SITE_URL=https://www.mc-cars.at|' .env
|
||
sed -i 's|SUPABASE_PUBLIC_URL=.*|SUPABASE_PUBLIC_URL=https://wwww.mc-cars.at|' .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 `<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.
|