diff --git a/.env.example b/.env.example index c8fd284..7f6fe5c 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,7 @@ JWT_SECRET="change-me-in-production" # ── Stripe ──────────────────────────────────────────────────────────────────── STRIPE_SECRET_KEY="sk_test_..." +STRIPE_PUBLISHABLE_KEY="pk_test_..." STRIPE_WEBHOOK_SECRET="whsec_..." # ── Twilio Verify (SMS OTP) ─────────────────────────────────────────────────── diff --git a/ops/README.md b/ops/README.md index 789e3ad..8e89311 100644 --- a/ops/README.md +++ b/ops/README.md @@ -47,10 +47,10 @@ Prisma Studio: `npm run db:studio` ## Unraid Deployment -1. Place the repo in `/mnt/user/appdata/storybid/`. -2. Copy `.env.example` → `.env` and fill in all values. -3. From the Unraid terminal: `docker compose up -d` -4. Optionally add a **Community Applications** custom app pointing to `docker-compose.yml`. +See the full step-by-step guide: [unraid-install.md](./unraid-install.md) + +Covers CLI (SSH) and GUI (Compose Manager plugin) methods, all environment variable +paths, reverse proxy setup, Stripe webhook registration, and troubleshooting. ## UniFi Event-Network Setup diff --git a/ops/unraid-install.md b/ops/unraid-install.md new file mode 100644 index 0000000..6202f93 --- /dev/null +++ b/ops/unraid-install.md @@ -0,0 +1,558 @@ +# Unraid Installation Guide + +Storybid runs as four Docker containers (PostgreSQL, Redis, server, client) managed +by a single `docker-compose.yml`. This guide covers two installation paths: + +- **CLI** — SSH into Unraid and use the terminal directly. Recommended; always works. +- **GUI** — Use the Community Applications *Compose Manager* plugin to manage the + stack from the Unraid web interface. + +Both methods produce an identical running system. + +--- + +## Prerequisites + +| Requirement | Notes | +|---|---| +| Unraid 6.12 or later | Earlier versions lack the built-in Compose support CLI needs | +| Community Applications plugin | Required for GUI method; install from CA store | +| Docker enabled | Unraid → Settings → Docker → Enable Docker = Yes | +| At least 2 GB free RAM | PostgreSQL + Redis + Node API + Nginx | +| A user share for appdata | Default: `/mnt/user/appdata/` | +| A public domain name | For Stripe webhooks and the public bidder URL | +| Stripe account | Live or test; publishable + secret key + webhook secret | +| Twilio Verify service | For SMS OTP login (optional but recommended) | +| SMTP relay | Any transactional email provider (Gmail, Postmark, Mailgun…) | + +--- + +## Directory Layout + +All persistent data lives under a single appdata directory. The recommended path is: + +``` +/mnt/user/appdata/storybid/ +├── repo/ ← cloned source code + docker-compose.yml +│ ├── .env ← your live config (never commit this file) +│ └── docker-compose.yml +├── postgres/ ← PostgreSQL data volume (managed by Docker) +├── redis/ ← Redis data volume (managed by Docker) +└── uploads/ ← uploaded item media (mapped from media_data volume) +``` + +> **Tip:** Keep `repo/` on a cache-backed share so builds are fast, but ensure the +> share has `Use Cache: Prefer` or `Only` so data doesn't move to array disks mid-event. + +--- + +## Environment Variables Reference + +Copy `.env.example` to `.env` and fill in every value before starting the stack. + +```bash +cp /mnt/user/appdata/storybid/repo/.env.example \ + /mnt/user/appdata/storybid/repo/.env +``` + +### Core app + +| Variable | Example | Description | +|---|---|---| +| `NODE_ENV` | `production` | Must be `production` for live deployments | +| `PORT` | `3001` | Internal port the API server listens on | +| `PUBLIC_URL` | `https://bid.example.org` | Externally reachable HTTPS URL. Used in magic-link emails and as the first-choice socket endpoint | +| `LOCAL_HOSTNAME` | `auction.event.lan` | LAN hostname for event-night failover (see [unifi-dns.md](./unifi-dns.md)). Must resolve on the event Wi-Fi. | +| `JWT_SECRET` | `64-char random string` | Signs all auth tokens. Generate with `openssl rand -hex 32` | +| `CLIENT_URL` | `https://bid.example.org` | Allowed CORS origin for the client; usually same as `PUBLIC_URL` | + +### Database + +| Variable | Example | Description | +|---|---|---| +| `DATABASE_URL` | `postgresql://storybid:PASS@db:5432/storybid` | Postgres connection string. The hostname is `db` (the Docker service name) in production | + +> **Change the password** in both `DATABASE_URL` and the `docker-compose.yml` +> `POSTGRES_PASSWORD` entry before first boot. + +### Redis + +| Variable | Example | Description | +|---|---|---| +| `REDIS_URL` | `redis://redis:6379` | Redis connection string. The hostname is `redis` inside the compose network | + +### Stripe + +| Variable | Example | Description | +|---|---|---| +| `STRIPE_SECRET_KEY` | `sk_live_…` | Stripe secret key from Dashboard → Developers → API keys | +| `STRIPE_PUBLISHABLE_KEY` | `pk_live_…` | Stripe publishable key — returned to bidder browsers to initialise payment forms | +| `STRIPE_WEBHOOK_SECRET` | `whsec_…` | Signing secret for `POST /api/webhooks/stripe`. Created when you register the webhook endpoint in Stripe Dashboard | + +### Twilio Verify (SMS OTP) + +| Variable | Example | Description | +|---|---|---| +| `TWILIO_ACCOUNT_SID` | `ACxxxx…` | Found on your Twilio Console dashboard | +| `TWILIO_AUTH_TOKEN` | `xxxx…` | Found on your Twilio Console dashboard | +| `TWILIO_VERIFY_SERVICE_SID` | `VAxxxx…` | Create a Verify service in Twilio Console → Verify → Services | + +Leave all three blank to disable SMS login (email magic links still work). + +### Email + +| Variable | Example | Description | +|---|---|---| +| `SMTP_HOST` | `smtp.postmarkapp.com` | Outbound SMTP server | +| `SMTP_PORT` | `587` | SMTP port (587 = STARTTLS, 465 = SSL) | +| `SMTP_USER` | `noreply@example.org` | SMTP username / sender address | +| `SMTP_PASS` | `…` | SMTP password or API key | +| `EMAIL_FROM` | `Storybid ` | Display name + address in the From header | + +### Media storage + +| Variable | Default | Description | +|---|---|---| +| `UPLOAD_DIR` | `/app/uploads` | Absolute path inside the server container where media files are written. Maps to `media_data` Docker volume | +| `MEDIA_BASE_URL` | `/media` | URL prefix the server uses to serve media. Change to an absolute URL if you front uploads with a CDN | + +--- + +## Method 1 — CLI via SSH + +### 1. Enable SSH on Unraid + +**Unraid web UI → Settings → Management Access → SSH → Enable SSH = Yes** + +Connect from your workstation: + +```bash +ssh root@192.168.1.X # replace with your Unraid IP +``` + +### 2. Clone the repository + +```bash +mkdir -p /mnt/user/appdata/storybid +cd /mnt/user/appdata/storybid +git clone https://github.com/YOUR_ORG/storybid.git repo +cd repo +``` + +If you don't have `git` on Unraid, install it via the **NerdTools** Community +Applications plugin, or transfer a zip via SCP: + +```bash +# From your workstation +scp storybid.zip root@192.168.1.X:/mnt/user/appdata/storybid/ +# On Unraid +cd /mnt/user/appdata/storybid && unzip storybid.zip && mv storybid repo +``` + +### 3. Configure the environment file + +```bash +cd /mnt/user/appdata/storybid/repo +cp .env.example .env +nano .env # or vi .env +``` + +Minimum required edits: + +```bash +NODE_ENV=production +PUBLIC_URL=https://bid.example.org +JWT_SECRET=$(openssl rand -hex 32) # paste output, don't run inline in .env + +DATABASE_URL=postgresql://storybid:CHANGE_ME@db:5432/storybid +STRIPE_SECRET_KEY=sk_live_... +STRIPE_PUBLISHABLE_KEY=pk_live_... +STRIPE_WEBHOOK_SECRET=whsec_... +SMTP_HOST=smtp.example.com +SMTP_USER=noreply@example.org +SMTP_PASS=... +EMAIL_FROM=Storybid +``` + +Also update `docker-compose.yml` to match the Postgres password you set above: + +```bash +# In docker-compose.yml, under the db service: +POSTGRES_PASSWORD: CHANGE_ME +# And in the server service environment: +DATABASE_URL: postgresql://storybid:CHANGE_ME@db:5432/storybid +``` + +### 4. Update volume paths (optional — recommended) + +By default Docker manages named volumes internally. To make them visible in the +Unraid GUI and ensure they survive array changes, pin them to explicit paths: + +```yaml +# In docker-compose.yml, replace the volumes block at the bottom: +volumes: + postgres_data: + driver: local + driver_opts: + type: none + o: bind + device: /mnt/user/appdata/storybid/postgres + + redis_data: + driver: local + driver_opts: + type: none + o: bind + device: /mnt/user/appdata/storybid/redis + + media_data: + driver: local + driver_opts: + type: none + o: bind + device: /mnt/user/appdata/storybid/uploads +``` + +Create the directories first: + +```bash +mkdir -p /mnt/user/appdata/storybid/{postgres,redis,uploads} +``` + +### 5. Build and start the stack + +```bash +cd /mnt/user/appdata/storybid/repo +docker compose up -d --build +``` + +First build downloads base images and compiles the TypeScript source — allow 3–5 +minutes. Watch progress: + +```bash +docker compose logs -f +``` + +All four containers should reach **Up** state: + +``` +NAME STATUS +storybid-db-1 Up (healthy) +storybid-redis-1 Up (healthy) +storybid-server-1 Up +storybid-client-1 Up +``` + +### 6. Run database migrations + +Run once after the first boot, and again after any update that includes schema changes: + +```bash +docker compose exec server npx prisma migrate deploy +``` + +### 7. Create the first admin user + +```bash +docker compose exec server node -e " +const { prisma } = await import('./dist/lib/prisma.js'); +await prisma.organization.create({ + data: { + name: 'My Charity', + staffUsers: { + create: { + name: 'Admin', + email: 'admin@example.org', + role: 'admin', + passwordHash: null, + } + } + } +}); +console.log('Done'); +await prisma.\$disconnect(); +" +``` + +Then use the magic-link login flow at `http://UNRAID-IP:8080` to sign in with +`admin@example.org`. The magic link will arrive at the SMTP address you configured. + +--- + +## Method 2 — GUI via Compose Manager Plugin + +The **Docker Compose Manager** plugin lets you manage compose stacks entirely from +the Unraid web interface without touching the terminal. + +### 1. Install the plugin + +1. Open **Unraid web UI → Apps** (Community Applications). +2. Search for **"Compose Manager"**. +3. Click **Install** on the result by *dcflachs*. +4. After installation, a new **Compose** tab appears in the Docker section. + +### 2. Upload the source files + +The plugin reads compose files from `/boot/config/plugins/compose.manager/projects/`. +Each project is a subdirectory containing `docker-compose.yml` and optionally a `.env`. + +**Option A — File Manager plugin (easiest)** + +1. Install **Unraid File Manager** from Community Applications. +2. Navigate to `/boot/config/plugins/compose.manager/projects/`. +3. Create a folder named `storybid`. +4. Upload `docker-compose.yml` and your completed `.env` into that folder. + +**Option B — SCP from your workstation** + +```bash +# From your workstation +scp docker-compose.yml root@192.168.1.X:/boot/config/plugins/compose.manager/projects/storybid/ +scp .env root@192.168.1.X:/boot/config/plugins/compose.manager/projects/storybid/ +``` + +### 3. Set explicit volume bind mounts + +Before starting, edit `docker-compose.yml` to replace Docker-managed volumes with +explicit host paths so Unraid can show them in the GUI (same change as step 4 of the +CLI method above): + +```yaml +volumes: + postgres_data: + driver: local + driver_opts: + type: none + o: bind + device: /mnt/user/appdata/storybid/postgres + + redis_data: + driver: local + driver_opts: + type: none + o: bind + device: /mnt/user/appdata/storybid/redis + + media_data: + driver: local + driver_opts: + type: none + o: bind + device: /mnt/user/appdata/storybid/uploads +``` + +Create the host directories via **Unraid → Tools → New Terminal**: + +```bash +mkdir -p /mnt/user/appdata/storybid/{postgres,redis,uploads} +``` + +### 4. Configure environment variables in the GUI + +1. In the **Compose** tab, click the **storybid** project row. +2. Click **Edit** to open the compose file editor — verify paths look correct. +3. Click the **⚙ Environment** button (or edit the `.env` file directly via File + Manager). +4. Set every variable listed in the [Environment Variables Reference](#environment-variables-reference) + above. At minimum: + + | Key | Value | + |---|---| + | `NODE_ENV` | `production` | + | `PUBLIC_URL` | `https://bid.example.org` | + | `JWT_SECRET` | *(64-char random string — generate in Terminal with `openssl rand -hex 32`)* | + | `DATABASE_URL` | `postgresql://storybid:CHANGE_ME@db:5432/storybid` | + | `STRIPE_SECRET_KEY` | `sk_live_…` | + | `STRIPE_PUBLISHABLE_KEY` | `pk_live_…` | + | `STRIPE_WEBHOOK_SECRET` | `whsec_…` | + | `SMTP_HOST` | your SMTP server | + | `SMTP_USER` | sender address | + | `SMTP_PASS` | SMTP password | + | `EMAIL_FROM` | `Storybid ` | + +5. Click **Save**. + +### 5. Build and start the stack + +1. In the **Compose** tab, click **Up** (▶) next to **storybid**. +2. The compose manager opens a build log window. First build takes 3–5 minutes. +3. When all containers show **Running**, close the log. + +### 6. Run migrations via the terminal + +The GUI plugin does not yet have a built-in `exec` interface, so open a terminal for +this one step: + +**Unraid → Tools → New Terminal** + +```bash +docker exec storybid-server-1 npx prisma migrate deploy +``` + +> The container name format is `--1`. If yours differs, run +> `docker ps` to find the exact name. + +--- + +## Reverse Proxy (Nginx Proxy Manager) + +Storybid needs HTTPS for Stripe webhooks, PWA installation, and the camera API used +by the check-in page. **Nginx Proxy Manager** is the easiest option on Unraid. + +### Install Nginx Proxy Manager + +Search **"Nginx Proxy Manager"** in Community Applications and install it. Default +ports: HTTP 80, HTTPS 443, Admin UI 81. + +### Add a proxy host for the client + +| Field | Value | +|---|---| +| Domain Names | `bid.example.org` | +| Scheme | `http` | +| Forward Hostname / IP | Unraid LAN IP (e.g. `192.168.1.50`) | +| Forward Port | `8080` | +| SSL Certificate | Request via Let's Encrypt (built into the GUI) | +| Force SSL | ✅ | +| HTTP/2 Support | ✅ | + +### Add a proxy host for the API + WebSocket + +Create a **second** proxy host (or add a location block) if your client and server are +on different subdomains, otherwise the single `bid.example.org` host handles both +because the Nginx config inside the client container already proxies `/api` and +`/socket.io` to `server:3001`. + +If you prefer separate subdomains: + +| Field | Value | +|---|---| +| Domain Names | `api.example.org` | +| Scheme | `http` | +| Forward Hostname / IP | `192.168.1.50` | +| Forward Port | `3001` | +| SSL | Let's Encrypt | +| WebSockets Support | ✅ | + +Then update `PUBLIC_URL` and `CLIENT_URL` in `.env` accordingly. + +--- + +## Register the Stripe Webhook + +1. Go to **Stripe Dashboard → Developers → Webhooks → Add endpoint**. +2. Endpoint URL: `https://bid.example.org/api/webhooks/stripe` +3. Events to listen for: + - `payment_intent.succeeded` + - `payment_intent.payment_failed` +4. Click **Add endpoint**. +5. Copy the **Signing secret** (`whsec_…`) into `STRIPE_WEBHOOK_SECRET` in `.env`. +6. Restart the server container to pick up the new value: + + ```bash + # CLI + docker compose restart server + + # GUI — Compose tab → storybid → Restart + ``` + +--- + +## Updating Storybid + +### CLI + +```bash +cd /mnt/user/appdata/storybid/repo +git pull +docker compose up -d --build +docker compose exec server npx prisma migrate deploy +``` + +### GUI + +1. Transfer the updated `docker-compose.yml` to the project folder (same path as + installation). +2. **Compose tab → storybid → Pull + Up** — this rebuilds images and restarts containers. +3. Open **Unraid → Tools → New Terminal** and run: + ```bash + docker exec storybid-server-1 npx prisma migrate deploy + ``` + +--- + +## Port Reference + +| Port | Service | Exposed to | +|---|---|---| +| `3001` | API server | Reverse proxy / LAN | +| `8080` | Client (Nginx) | Reverse proxy / LAN | +| `5432` | PostgreSQL | LAN only (close in production if not needed) | +| `6379` | Redis | LAN only (close in production if not needed) | + +To restrict Postgres and Redis to the Docker internal network only (recommended for +production), remove their `ports:` entries from `docker-compose.yml`: + +```yaml +# Remove or comment out these blocks: + db: + ports: + - "5432:5432" # ← remove + + redis: + ports: + - "6379:6379" # ← remove +``` + +--- + +## Troubleshooting + +### Containers restart immediately + +```bash +docker compose logs server # check for DATABASE_URL or JWT_SECRET errors +docker compose logs db # check for volume permission issues +``` + +### Migration fails with "relation already exists" + +The database was already migrated. This is safe to ignore, or run: + +```bash +docker compose exec server npx prisma migrate status +``` + +### `STRIPE_SECRET_KEY is not configured` error in logs + +The `.env` file is not being loaded. Verify: + +- `.env` is in the same directory as `docker-compose.yml`. +- The `server` service in `docker-compose.yml` has `env_file: .env`. +- No trailing whitespace or Windows line endings in `.env` + (`dos2unix .env` fixes CRLF issues). + +### Media uploads return 404 + +Check that the `uploads` bind-mount directory exists and is writable: + +```bash +ls -la /mnt/user/appdata/storybid/uploads +chmod 755 /mnt/user/appdata/storybid/uploads +docker compose restart server +``` + +### Bidders can't connect via local LAN hostname + +1. Verify the DNS record resolves from a device on the event Wi-Fi: + ```bash + nslookup auction.event.lan + ``` +2. Confirm `LOCAL_HOSTNAME` in `.env` matches the DNS record exactly. +3. See [unifi-dns.md](./unifi-dns.md) for full UniFi DNS setup steps. + +### Compose Manager shows "project not found" + +The project directory must be inside `/boot/config/plugins/compose.manager/projects/`. +Files on `/mnt/user/` shares are not read by the plugin. Use SCP or File Manager to +place files in the correct location on `/boot`.