diff --git a/DATABASE.md b/DATABASE.md
index cf7cc76..8a28cdc 100644
--- a/DATABASE.md
+++ b/DATABASE.md
@@ -1,5 +1,7 @@
## Database Setup
+> For full deployment instructions (Vercel, DigitalOcean App Platform, Docker on a Droplet), see [docs/DEPLOYMENT.md](./docs/DEPLOYMENT.md).
+
### Fresh deployment
Run **one file**:
diff --git a/app/(app)/payments/payments-client.tsx b/app/(app)/payments/payments-client.tsx
index fe765e4..572e596 100644
--- a/app/(app)/payments/payments-client.tsx
+++ b/app/(app)/payments/payments-client.tsx
@@ -174,7 +174,7 @@ export function PaymentsClient({ payments: initialPayments, members, integration
)}
-
${total.toFixed(0)}
+ {'$'}{total.toFixed(0)}
{txs.length} tx{txs.length !== 1 ? 's' : ''} · {unlinked} unlinked
@@ -217,7 +217,7 @@ export function PaymentsClient({ payments: initialPayments, members, integration
{p.platform?.toUpperCase()}
|
- ${Number(p.amount).toFixed(2)} |
+ {'$'}{Number(p.amount).toFixed(2)} |
{p.from_identifier}{p.from_note ? ` — "${p.from_note}"` : ''}
|
@@ -316,7 +316,7 @@ export function PaymentsClient({ payments: initialPayments, members, integration
Link to Member
- ${linkingPayment.amount} via {linkingPayment.platform?.toUpperCase()} — {linkingPayment.from_identifier}
+ {'$'}{linkingPayment.amount} via {linkingPayment.platform?.toUpperCase()} — {linkingPayment.from_identifier}
diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md
new file mode 100644
index 0000000..9bb503e
--- /dev/null
+++ b/docs/DEPLOYMENT.md
@@ -0,0 +1,616 @@
+# Hackerspace.sh — Deployment Guide
+
+Complete instructions for every supported deployment target.
+
+---
+
+## Table of Contents
+
+1. [Prerequisites](#1-prerequisites)
+2. [Supabase Setup (required for all targets)](#2-supabase-setup)
+3. [Environment Variables Reference](#3-environment-variables)
+4. [Option A — Vercel (recommended)](#4-option-a--vercel)
+5. [Option B — DigitalOcean App Platform](#5-option-b--digitalocean-app-platform)
+6. [Option C — DigitalOcean Droplet (Docker / self-hosted)](#6-option-c--digitalocean-droplet)
+7. [Post-Deploy Checklist](#7-post-deploy-checklist)
+8. [Upgrading an Existing Deployment](#8-upgrading)
+9. [Troubleshooting](#9-troubleshooting)
+
+---
+
+## 1. Prerequisites
+
+| Tool | Version | Purpose |
+|------|---------|---------|
+| Node.js | 20+ | Local development |
+| pnpm | 9+ | Package manager |
+| Git | any | Source control |
+| Supabase account | free tier works | Database + auth + realtime |
+| Vercel / DigitalOcean account | — | Hosting |
+
+Clone the repo:
+
+```bash
+git clone https://github.com/virgilvox/hackerspace-management.git
+cd hackerspace-management
+pnpm install
+```
+
+---
+
+## 2. Supabase Setup
+
+This step is identical regardless of which hosting option you choose.
+
+### 2.1 Create a Supabase project
+
+1. Go to [supabase.com](https://supabase.com) → **New project**
+2. Choose a region close to your users
+3. Set a strong database password and save it — you won't need it directly but keep it safe
+4. Wait ~2 minutes for provisioning
+
+### 2.2 Apply the schema
+
+1. In the Supabase dashboard, go to **SQL Editor**
+2. Click **New query**
+3. Paste the entire contents of `scripts/schema.sql`
+4. Click **Run** (or Ctrl+Enter)
+
+You should see no errors. The script is idempotent — safe to re-run.
+
+### 2.3 Configure Auth
+
+In the Supabase dashboard:
+
+**Authentication → Settings → General**
+- Set **Site URL** to your app's public URL (e.g. `https://yourapp.vercel.app`)
+- Add the same URL to **Redirect URLs**
+- Also add `http://localhost:3000` for local dev
+
+**Authentication → Settings → Email**
+- Enable **Confirm email** if you want verified signups
+- Customize the email templates as needed
+
+**Authentication → Providers**
+- Email/Password is enabled by default — no changes needed
+- Optionally enable Google, GitHub, etc. (social login buttons are wired in the UI)
+
+### 2.4 Enable Realtime
+
+The schema script already runs `ALTER PUBLICATION supabase_realtime ADD TABLE ...` for `comms_messages` and `comms_channels`. Verify in:
+
+**Database → Replication** — confirm both tables appear under `supabase_realtime`.
+
+### 2.5 Get your API keys
+
+**Project Settings → API**
+
+Copy these four values — you'll need them in every deployment:
+
+```
+NEXT_PUBLIC_SUPABASE_URL = https://xxxxxxxxxxxx.supabase.co
+NEXT_PUBLIC_SUPABASE_ANON_KEY = eyJhbGci...
+SUPABASE_SERVICE_ROLE_KEY = eyJhbGci... (keep this SECRET — server-side only)
+```
+
+### 2.6 Create your first Space + Admin user
+
+After deploying the app, navigate to `/signup`. The signup flow will:
+1. Create an `auth.users` row in Supabase
+2. Trigger `handle_space_signup()` which inserts a `space_members` row
+
+To make yourself an admin, run this in the SQL editor (replace values):
+
+```sql
+UPDATE public.space_members
+SET role = 'admin', status = 'current'
+WHERE email = 'your@email.com';
+```
+
+---
+
+## 3. Environment Variables
+
+Create `.env.local` for local dev. For deployed environments, set these in your host's dashboard.
+
+```bash
+# ── Supabase ──────────────────────────────────────────────────
+NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxxxxxx.supabase.co
+NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...
+SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...
+
+# ── App ───────────────────────────────────────────────────────
+NEXT_PUBLIC_APP_URL=https://yourdomain.com
+
+# ── PayPal (optional — set in app Settings UI too) ────────────
+# These are also stored per-space in the integrations table.
+# The API route reads from the DB, not from env vars.
+# No env vars needed for PayPal.
+
+# ── Node ──────────────────────────────────────────────────────
+NODE_ENV=production
+```
+
+**Never commit `.env.local` to git.** The `.gitignore` already excludes it.
+
+---
+
+## 4. Option A — Vercel
+
+The simplest and fastest path. Zero server management.
+
+### 4.1 Deploy via Vercel CLI
+
+```bash
+# Install Vercel CLI globally
+pnpm add -g vercel
+
+# From the project root:
+vercel
+
+# Follow the prompts:
+# - Link to existing project or create new
+# - Framework: Next.js (auto-detected)
+# - Root directory: ./ (default)
+```
+
+### 4.2 Set environment variables
+
+```bash
+vercel env add NEXT_PUBLIC_SUPABASE_URL production
+vercel env add NEXT_PUBLIC_SUPABASE_ANON_KEY production
+vercel env add SUPABASE_SERVICE_ROLE_KEY production
+vercel env add NEXT_PUBLIC_APP_URL production
+```
+
+Or set them in the Vercel dashboard: **Project → Settings → Environment Variables**
+
+### 4.3 Deploy to production
+
+```bash
+vercel --prod
+```
+
+### 4.4 Connect a custom domain (optional)
+
+Vercel dashboard → **Domains** → add your domain → follow DNS instructions.
+
+### 4.5 Continuous deployment
+
+Push to `main` branch → Vercel auto-deploys. Push to any other branch → preview deployment at a unique URL.
+
+```bash
+git push origin main # triggers production deploy
+git push origin feat/my-feature # triggers preview deploy
+```
+
+---
+
+## 5. Option B — DigitalOcean App Platform
+
+Managed container hosting. No Docker knowledge required. Scales automatically.
+
+### 5.1 Push your repo to GitHub
+
+DigitalOcean App Platform deploys directly from GitHub.
+
+```bash
+git remote add origin https://github.com/YOUR_ORG/hackerspace-management.git
+git push -u origin main
+```
+
+### 5.2 Create the app
+
+1. Go to [cloud.digitalocean.com/apps](https://cloud.digitalocean.com/apps)
+2. Click **Create App** → **GitHub** → authorize and select your repo
+3. Select branch: `main`
+4. DigitalOcean auto-detects Next.js — confirm the build settings:
+ - **Build command**: `pnpm install && pnpm build`
+ - **Run command**: `pnpm start`
+ - **HTTP port**: `3000`
+
+### 5.3 Set environment variables
+
+In the App Platform UI, go to **Settings → App-Level Environment Variables** and add:
+
+```
+NEXT_PUBLIC_SUPABASE_URL = (your value)
+NEXT_PUBLIC_SUPABASE_ANON_KEY = (your value)
+SUPABASE_SERVICE_ROLE_KEY = (mark as Encrypted)
+NEXT_PUBLIC_APP_URL = https://your-app.ondigitalocean.app
+NODE_ENV = production
+```
+
+Mark `SUPABASE_SERVICE_ROLE_KEY` as **Encrypted** so it's not visible in logs.
+
+### 5.4 Choose a plan
+
+- **Basic** ($5/mo): fine for small hackerspaces (<50 members)
+- **Professional** ($12/mo): recommended for production — includes more RAM and 99.99% SLA
+
+### 5.5 Deploy
+
+Click **Create Resources** → wait ~3 minutes for the first build.
+
+### 5.6 Custom domain
+
+App Platform dashboard → **Settings → Domains** → add domain → update your DNS with the CNAME provided.
+
+### 5.7 Continuous deployment
+
+App Platform auto-deploys on every push to `main` by default. You can also configure it to deploy only on tagged releases.
+
+---
+
+## 6. Option C — DigitalOcean Droplet
+
+Full self-hosted on a VPS. Maximum control. Use this if you want everything on your own infrastructure.
+
+### 6.1 Create a Droplet
+
+1. Go to [cloud.digitalocean.com/droplets](https://cloud.digitalocean.com/droplets)
+2. **Create Droplet**
+ - Image: **Ubuntu 24.04 LTS**
+ - Plan: **Basic — Regular — $12/mo** (2 vCPU, 2GB RAM) minimum; $24/mo recommended
+ - Datacenter: closest to your users
+ - Authentication: SSH key (strongly recommended over password)
+3. Note the Droplet IP address
+
+### 6.2 Initial server setup
+
+SSH in and harden the server:
+
+```bash
+ssh root@YOUR_DROPLET_IP
+
+# Create a non-root user
+adduser deploy
+usermod -aG sudo deploy
+
+# Copy SSH key to new user
+rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
+
+# Switch to deploy user
+su - deploy
+```
+
+### 6.3 Install Docker and Docker Compose
+
+```bash
+# Install Docker
+curl -fsSL https://get.docker.com | sh
+sudo usermod -aG docker deploy
+
+# Install Docker Compose plugin
+sudo apt-get install -y docker-compose-plugin
+
+# Verify
+docker --version
+docker compose version
+```
+
+### 6.4 Install Nginx and Certbot
+
+```bash
+sudo apt-get update
+sudo apt-get install -y nginx certbot python3-certbot-nginx
+```
+
+### 6.5 Clone the repo on the server
+
+```bash
+cd /opt
+sudo git clone https://github.com/YOUR_ORG/hackerspace-management.git hackerspace
+sudo chown -R deploy:deploy /opt/hackerspace
+cd /opt/hackerspace
+```
+
+### 6.6 Create the Docker Compose file
+
+```bash
+cat > /opt/hackerspace/docker-compose.yml << 'EOF'
+version: "3.9"
+
+services:
+ app:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: hackerspace_app
+ restart: unless-stopped
+ ports:
+ - "3000:3000"
+ env_file:
+ - .env.production
+ environment:
+ NODE_ENV: production
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
+
+networks:
+ default:
+ name: hackerspace_net
+EOF
+```
+
+### 6.7 Create the Dockerfile
+
+```bash
+cat > /opt/hackerspace/Dockerfile << 'EOF'
+FROM node:20-alpine AS base
+RUN corepack enable pnpm
+
+FROM base AS deps
+WORKDIR /app
+COPY package.json pnpm-lock.yaml ./
+RUN pnpm install --frozen-lockfile
+
+FROM base AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+RUN pnpm build
+
+FROM base AS runner
+WORKDIR /app
+ENV NODE_ENV=production
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 nextjs
+
+COPY --from=builder /app/public ./public
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+
+USER nextjs
+EXPOSE 3000
+ENV PORT=3000
+ENV HOSTNAME="0.0.0.0"
+
+CMD ["node", "server.js"]
+EOF
+```
+
+### 6.8 Enable standalone output in Next.js config
+
+Edit `next.config.mjs` and ensure it contains:
+
+```js
+const nextConfig = {
+ output: 'standalone',
+ // ...rest of your config
+}
+```
+
+### 6.9 Create the production env file
+
+```bash
+cat > /opt/hackerspace/.env.production << 'EOF'
+NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxxxxxx.supabase.co
+NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...
+SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...
+NEXT_PUBLIC_APP_URL=https://yourdomain.com
+NODE_ENV=production
+EOF
+
+# Lock down permissions — this file contains secrets
+chmod 600 /opt/hackerspace/.env.production
+```
+
+### 6.10 Build and start the container
+
+```bash
+cd /opt/hackerspace
+docker compose build
+docker compose up -d
+
+# Check logs
+docker compose logs -f app
+```
+
+### 6.11 Configure Nginx as reverse proxy
+
+```bash
+sudo nano /etc/nginx/sites-available/hackerspace
+```
+
+Paste:
+
+```nginx
+server {
+ listen 80;
+ server_name yourdomain.com www.yourdomain.com;
+
+ location / {
+ proxy_pass http://localhost:3000;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ 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_cache_bypass $http_upgrade;
+ proxy_read_timeout 86400s;
+ proxy_send_timeout 86400s;
+ }
+}
+```
+
+Enable and test:
+
+```bash
+sudo ln -s /etc/nginx/sites-available/hackerspace /etc/nginx/sites-enabled/
+sudo nginx -t
+sudo systemctl reload nginx
+```
+
+### 6.12 SSL with Let's Encrypt
+
+Point your domain's A record to the Droplet IP first, then:
+
+```bash
+sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
+```
+
+Certbot will auto-configure HTTPS and set up auto-renewal. Verify renewal works:
+
+```bash
+sudo certbot renew --dry-run
+```
+
+### 6.13 Set up automatic deployments (optional)
+
+Create a deploy script:
+
+```bash
+cat > /opt/hackerspace/deploy.sh << 'EOF'
+#!/bin/bash
+set -e
+cd /opt/hackerspace
+git pull origin main
+docker compose build app
+docker compose up -d --no-deps app
+docker image prune -f
+echo "Deploy complete: $(date)"
+EOF
+chmod +x /opt/hackerspace/deploy.sh
+```
+
+To trigger a deploy from your local machine:
+
+```bash
+ssh deploy@YOUR_DROPLET_IP '/opt/hackerspace/deploy.sh'
+```
+
+Or set up a GitHub Actions workflow:
+
+```yaml
+# .github/workflows/deploy.yml
+name: Deploy to Droplet
+
+on:
+ push:
+ branches: [main]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Deploy via SSH
+ uses: appleboy/ssh-action@v1
+ with:
+ host: ${{ secrets.DROPLET_IP }}
+ username: deploy
+ key: ${{ secrets.DROPLET_SSH_KEY }}
+ script: /opt/hackerspace/deploy.sh
+```
+
+Add `DROPLET_IP` and `DROPLET_SSH_KEY` to your GitHub repo's **Settings → Secrets**.
+
+### 6.14 Firewall
+
+```bash
+sudo ufw allow OpenSSH
+sudo ufw allow 'Nginx Full'
+sudo ufw enable
+sudo ufw status
+```
+
+---
+
+## 7. Post-Deploy Checklist
+
+Run through this after every fresh deployment:
+
+- [ ] App loads at your domain with HTTPS
+- [ ] `/signup` creates a user and redirects to `/dashboard`
+- [ ] Dashboard shows "No members yet" (not an error)
+- [ ] Supabase dashboard → **Authentication → Users** shows the signup
+- [ ] `space_members` table has a row for the new user
+- [ ] Chat page loads channels (general, announcements, ops)
+- [ ] Send a message — it appears immediately without page refresh
+- [ ] Upgrade your user to admin via SQL (see section 2.6)
+- [ ] Settings page → save space settings → no error toast
+- [ ] PayPal integration → save credentials → "LIVE" badge appears
+
+---
+
+## 8. Upgrading
+
+When the schema changes, a new numbered script will be added to `scripts/`. For example `scripts/012_add_feature.sql`. Run only the new script in the Supabase SQL editor — do not re-run `schema.sql` on an existing database.
+
+```
+scripts/
+ schema.sql ← fresh deployment only
+ 012_add_feature.sql ← incremental upgrade
+ 013_...sql
+```
+
+For Vercel / App Platform: push to `main`, the build auto-deploys.
+
+For Droplet: `ssh deploy@IP '/opt/hackerspace/deploy.sh'`
+
+---
+
+## 9. Troubleshooting
+
+### "relation does not exist" errors
+
+The schema was not applied or was partially applied. Re-run `scripts/schema.sql` in the Supabase SQL editor.
+
+### Users can sign up but see no data
+
+The `handle_space_signup` trigger is not firing or the `space_id` metadata was not passed during signup. Check:
+
+```sql
+SELECT * FROM public.space_members ORDER BY joined_at DESC LIMIT 5;
+```
+
+If empty, ensure a `spaces` row exists first:
+
+```sql
+INSERT INTO public.spaces (name, slug, invite_code)
+VALUES ('My Hackerspace', 'my-hackerspace', 'INVITE123');
+```
+
+Then re-run signup with the space's `id` in the metadata.
+
+### Chat messages not appearing in realtime
+
+Verify realtime is enabled:
+
+```sql
+SELECT * FROM pg_publication_tables WHERE pubname = 'supabase_realtime';
+```
+
+Both `comms_messages` and `comms_channels` must appear. If not:
+
+```sql
+ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_messages;
+ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_channels;
+```
+
+### 500 errors on API routes
+
+Check that `SUPABASE_SERVICE_ROLE_KEY` is set in your deployment environment. This key is required for server-side actions.
+
+### Docker container exits immediately
+
+```bash
+docker compose logs app
+```
+
+Most common cause: missing `.env.production` file or a missing required env var.
+
+### SSL certificate not renewing
+
+```bash
+sudo systemctl status certbot.timer
+sudo journalctl -u certbot
+```
+
+If the timer is not running: `sudo systemctl enable --now certbot.timer`
diff --git a/next.config.mjs b/next.config.mjs
index 4cd9948..8bf9d8f 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,5 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
+ // 'standalone' bundles the app into a self-contained output for Docker deployments.
+ // On Vercel this setting is ignored — Vercel handles its own output format.
+ output: process.env.DOCKER_BUILD ? 'standalone' : undefined,
+
typescript: {
ignoreBuildErrors: true,
},