feat: open-source release prep, audit fixes, and deploy hardening
Production deploy completed (DigitalOcean Droplet, self-hosted Supabase, Caddy + Let's Encrypt, GitHub Actions auto-deploy on push to main, daily pg_dumpall backups). This commit lands the work that came out of the deep audit pass and the open-source release prep. Audit fixes: - Webhook signing secret is now displayed in the settings UI with show/hide and copy-to-clipboard; the broken external docs.hackerspace.sh link is replaced with a link to the new local docs. - Settings updateSpaceSettings parameter type now includes mission_statement so the field persists. - members page enforces member_directory_visibility for non-admin viewers (admin_only, board_visible, members_visible). - Mobile responsiveness pass: Projects Kanban grid, Members + Payments tables (column hiding on small screens), Proposal tally + voting buttons (2-col on mobile). Open-source release scaffolding: - LICENSE (MIT). - CONTRIBUTING.md with dev setup, conventions, PR flow. - SECURITY.md with disclosure policy. - CHANGELOG.md. - docs/WEBHOOKS.md with Node and Python signature verification examples. - README.md rewritten for an open-source audience. Removed v0/Vercel references: - @vercel/analytics dependency removed. - app/layout.tsx no longer imports Analytics, drops generator: 'v0.app'. - Canonical deployment docs (docs/DEPLOYMENT.md, docs/README.md, docs/ARCHITECTURE.md, DATABASE.md, CLAUDE.md, README.md) now describe the self-hosted Droplet target only. - e2e console-error check no longer filters [v0] noise. Landing page: - GitHub icon link to the public repo in the nav and footer.
This commit is contained in:
@@ -42,8 +42,9 @@ Hackerspace.sh is a comprehensive member management platform designed for hacker
|
||||
### Infrastructure
|
||||
| Service | Purpose |
|
||||
|---------|---------|
|
||||
| Vercel | Hosting, Edge Functions |
|
||||
| Supabase Cloud | Database, Auth, Realtime subscriptions |
|
||||
| DigitalOcean Droplet | Self-hosted runtime (Docker, systemd, Caddy with Let's Encrypt) |
|
||||
| Self-hosted Supabase | Postgres, GoTrue (auth), PostgREST, Realtime, Studio |
|
||||
| Resend | Transactional email (SMTP) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+40
-557
@@ -1,572 +1,55 @@
|
||||
# Hackerspace.sh — Deployment Guide
|
||||
# Deployment
|
||||
|
||||
Complete instructions for every supported deployment target that uses **managed Supabase Cloud** as the database / auth layer.
|
||||
The supported production target is a single DigitalOcean Droplet running self-hosted Supabase plus the Next.js app behind Caddy with automatic HTTPS. The end-to-end procedure lives in [DEPLOY_DO_SELFHOSTED.md](./DEPLOY_DO_SELFHOSTED.md).
|
||||
|
||||
For a fully self-hosted deployment where you run Supabase yourself on a DigitalOcean Droplet (Postgres, GoTrue, PostgREST, Realtime, Storage, Studio — all on your own server), see [DEPLOY_DO_SELFHOSTED.md](./DEPLOY_DO_SELFHOSTED.md). That guide is end-to-end and assumes nothing else.
|
||||
## At a glance
|
||||
|
||||
---
|
||||
| Component | Hosted on |
|
||||
|-----------|-----------|
|
||||
| Database (Postgres) | Supabase container on the Droplet, data on a persistent block volume |
|
||||
| Auth (GoTrue) | Supabase container on the Droplet |
|
||||
| REST and Realtime | Supabase containers on the Droplet |
|
||||
| App (Next.js) | systemd service on the Droplet, port 3000 |
|
||||
| Reverse proxy | Caddy 2 on the Droplet, ports 80 and 443 |
|
||||
| Email | Resend (SMTP) |
|
||||
| Backups | Daily `pg_dumpall` to `/mnt/data/backups`, 14-day retention |
|
||||
| Continuous deployment | GitHub Actions runs `/opt/hackerspace-ops/deploy.sh` over SSH on every push to `main` |
|
||||
|
||||
## Table of Contents
|
||||
## Required environment variables
|
||||
|
||||
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)
|
||||
All variables are listed in [.env.example](../.env.example). At minimum:
|
||||
|
||||
---
|
||||
- `NEXT_PUBLIC_SUPABASE_URL` — public Supabase API URL
|
||||
- `NEXT_PUBLIC_SUPABASE_ANON_KEY` — anon JWT, public
|
||||
- `SUPABASE_SERVICE_ROLE_KEY` — service-role JWT, server-side only
|
||||
- `NEXT_PUBLIC_APP_URL` — public URL of the deployed app, used in OAuth redirects
|
||||
|
||||
## 1. Prerequisites
|
||||
## How a deploy runs
|
||||
|
||||
| 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 |
|
||||
1. A developer pushes to `main`.
|
||||
2. GitHub Actions (`.github/workflows/deploy.yml`) opens an SSH connection to the Droplet using a deploy key stored in repository secrets.
|
||||
3. The action invokes `/opt/hackerspace-ops/deploy.sh`, which:
|
||||
- Fetches latest `main`.
|
||||
- Runs `pnpm install --frozen-lockfile`.
|
||||
- Iterates over `scripts/0*.sql`. Any file not present in `public._migrations_applied` is applied and recorded.
|
||||
- Restarts PostgREST to refresh its schema cache.
|
||||
- Runs `pnpm build`.
|
||||
- Restarts the `hackerspace-app` systemd unit.
|
||||
- Probes the local app for a 200 response.
|
||||
|
||||
Clone the repo:
|
||||
Migrations are idempotent. A redeploy that introduces no new migrations is a no-op on the database.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/virgilvox/hackerspace-management.git
|
||||
cd hackerspace-management
|
||||
pnpm install
|
||||
```
|
||||
## Local development
|
||||
|
||||
---
|
||||
See [LOCAL_DEV.md](./LOCAL_DEV.md) for the fresh-clone-to-running sequence using the Supabase CLI.
|
||||
|
||||
## 2. Supabase Setup
|
||||
## First-time provisioning
|
||||
|
||||
This step is identical regardless of which hosting option you choose.
|
||||
For initial server provisioning (Docker, the Supabase stack, Caddy, firewall, SSH hardening, deploy-key registration, the systemd unit, backup cron), follow [DEPLOY_DO_SELFHOSTED.md](./DEPLOY_DO_SELFHOSTED.md) end to end.
|
||||
|
||||
### 2.1 Create a Supabase project
|
||||
## Operational checklist
|
||||
|
||||
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
|
||||
|
||||
The repo ships an `.env.example` file. Copy it locally:
|
||||
|
||||
```bash
|
||||
cp .env.example .env.local
|
||||
```
|
||||
|
||||
Then fill in real values. For deployed environments, set the same keys in the host's dashboard.
|
||||
|
||||
Required variables:
|
||||
|
||||
| Key | Where it is used |
|
||||
|-----|------------------|
|
||||
| `NEXT_PUBLIC_SUPABASE_URL` | Browser + server, in every request |
|
||||
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Browser + server |
|
||||
| `SUPABASE_SERVICE_ROLE_KEY` | Server only. Bypasses RLS. Used by createSpace / joinSpace |
|
||||
| `NEXT_PUBLIC_APP_URL` | OAuth callback URLs |
|
||||
| `NODE_ENV` | Runtime mode |
|
||||
|
||||
Per-space integration credentials (PayPal, Zeffy, Venmo, Stripe) are stored in the `integrations` table, not in env. The Settings UI writes them; `/api/paypal/sync` reads them.
|
||||
|
||||
`.env.local` is git-ignored. Never commit real secrets.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
The repo ships an App Platform spec at `.do/app.yaml`. You can either apply it directly with `doctl` or use the UI flow below.
|
||||
|
||||
### 5.1 Push your repo to GitHub
|
||||
|
||||
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.2a Create the app via `doctl` (recommended)
|
||||
|
||||
Edit `.do/app.yaml` and replace `REPO_OWNER/REPO_NAME` and the placeholder env values. Then:
|
||||
|
||||
```bash
|
||||
doctl apps create --spec .do/app.yaml
|
||||
```
|
||||
|
||||
### 5.2b Or create via the UI
|
||||
|
||||
1. Go to [cloud.digitalocean.com/apps](https://cloud.digitalocean.com/apps)
|
||||
2. Click **Create App** then **GitHub**, authorize and select your repo
|
||||
3. Select branch: `main`
|
||||
4. App Platform auto-detects Next.js. Confirm the build settings:
|
||||
- **Build command**: `corepack enable && pnpm install --frozen-lockfile && pnpm build`
|
||||
- **Run command**: `pnpm start`
|
||||
- **HTTP port**: `3000`
|
||||
- **Health check path**: `/api/health`
|
||||
|
||||
### 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 Use the bundled Dockerfile and Compose file
|
||||
|
||||
Both files ship in the repo root:
|
||||
|
||||
```
|
||||
Dockerfile (multi-stage build, standalone output, healthcheck)
|
||||
docker-compose.yml (service definition, env_file, healthcheck)
|
||||
.dockerignore
|
||||
```
|
||||
|
||||
You do not need to write your own. The Dockerfile sets `DOCKER_BUILD=1` during build so `next.config.mjs` emits standalone output. The compose file reads `.env.local` by default; for production swap that to `.env.production` (next step) or edit the compose file inline.
|
||||
|
||||
### 6.7 Create the production env file
|
||||
|
||||
```bash
|
||||
cp /opt/hackerspace/.env.example /opt/hackerspace/.env.production
|
||||
nano /opt/hackerspace/.env.production
|
||||
```
|
||||
|
||||
Fill in the real Supabase URL, anon key, service role key, and your app URL. Then lock the file:
|
||||
|
||||
```bash
|
||||
chmod 600 /opt/hackerspace/.env.production
|
||||
```
|
||||
|
||||
Update `docker-compose.yml` to point at `.env.production` instead of `.env.local`, or symlink:
|
||||
|
||||
```bash
|
||||
ln -sf .env.production /opt/hackerspace/.env.local
|
||||
```
|
||||
|
||||
### 6.8 Build and start the container
|
||||
|
||||
```bash
|
||||
cd /opt/hackerspace
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
|
||||
# Check logs
|
||||
docker compose logs -f app
|
||||
|
||||
# Verify health endpoint
|
||||
curl http://localhost:3000/api/health
|
||||
# {"status":"ok","timestamp":"..."}
|
||||
```
|
||||
|
||||
### 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 over HTTPS
|
||||
- [ ] `GET /api/health` returns `{ "status": "ok" }`
|
||||
- [ ] `/signup` creates a user and redirects to `/dashboard`
|
||||
- [ ] Dashboard renders without errors (counts may be zero)
|
||||
- [ ] Supabase dashboard, Authentication > Users shows the signup
|
||||
- [ ] `space_members` table has a row for the new user, with `role = 'admin'` for the first user (`createSpace` sets this)
|
||||
- [ ] Chat page loads the three default channels (`general`, `announcements`, `ops`)
|
||||
- [ ] Send a message and watch it appear in another tab without refresh (verifies realtime)
|
||||
- [ ] Settings page saves without error
|
||||
- [ ] PayPal integration accepts credentials and shows the connected badge
|
||||
|
||||
---
|
||||
|
||||
## 8. Upgrading
|
||||
|
||||
When the schema changes, a new numbered script is added to `scripts/` (for example `scripts/014_member_user_id_nullable.sql`). On an existing database, run only the new script in the Supabase SQL editor.
|
||||
|
||||
```
|
||||
scripts/
|
||||
schema.sql fresh deployment, full schema
|
||||
001_create_schema.sql historical, not used for fresh deploys
|
||||
...
|
||||
014_member_user_id_nullable.sql most recent incremental
|
||||
```
|
||||
|
||||
`scripts/schema.sql` is also idempotent: it uses `IF NOT EXISTS` for tables and indexes, `DROP POLICY IF EXISTS` before recreating policies, and `EXCEPTION WHEN duplicate_object` for enums and realtime publications. Re-running it on an existing database is safe but unnecessary.
|
||||
|
||||
For Vercel and 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`
|
||||
- Monitor `/var/log/hackerspace-deploy.log` for deploy outcomes.
|
||||
- Verify backups by restoring `pg_*.sql.gz` to a staging instance quarterly.
|
||||
- Rotate the Supabase service-role key and JWT secret if the `.env` on the Droplet is ever exposed.
|
||||
- Keep the host OS patched. The bootstrap installs `unattended-upgrades`.
|
||||
|
||||
+5
-7
@@ -74,7 +74,7 @@ A comprehensive member management platform for hackerspaces, makerspaces, and co
|
||||
| Database | Supabase PostgreSQL |
|
||||
| Auth | Supabase Auth |
|
||||
| Real-time | Supabase Realtime |
|
||||
| Hosting | Vercel |
|
||||
| Hosting | Self-hosted DigitalOcean Droplet (Docker, Caddy) |
|
||||
|
||||
---
|
||||
|
||||
@@ -174,11 +174,9 @@ All database access is protected by RLS policies:
|
||||
|
||||
## Deployment
|
||||
|
||||
### Vercel
|
||||
### DigitalOcean Droplet (production)
|
||||
|
||||
1. Connect GitHub repository
|
||||
2. Set environment variables in Vercel dashboard
|
||||
3. Deploy automatically on push to main
|
||||
See [DEPLOYMENT.md](./DEPLOYMENT.md) for the at-a-glance flow and [DEPLOY_DO_SELFHOSTED.md](./DEPLOY_DO_SELFHOSTED.md) for the end-to-end provisioning procedure. Every push to `main` triggers GitHub Actions, which runs the deploy script on the Droplet over SSH.
|
||||
|
||||
### Database Migrations
|
||||
|
||||
@@ -205,10 +203,10 @@ Migrations must be run manually via Supabase SQL Editor:
|
||||
|
||||
Add console logs in server actions:
|
||||
```typescript
|
||||
console.log('[v0] User:', user?.id, 'Space:', member?.space_id)
|
||||
console.log('[app] User:', user?.id, 'Space:', member?.space_id)
|
||||
```
|
||||
|
||||
View logs in Vercel Function logs or terminal.
|
||||
View logs with `journalctl -u hackerspace-app -f` on the Droplet, or in the dev server terminal locally.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# Webhooks
|
||||
|
||||
Webhooks let an external service react to events happening inside a space (a new member, a payment recorded, a proposal opened, an incident filed, etc). Each space has one webhook endpoint and one signing secret.
|
||||
|
||||
> Status: the webhook envelope, signing secret rotation, and the verification contract documented here are stable. Per-event delivery, retry policy with backoff, and the delivery history UI are tracked in `docs/AUDIT.md` and `docs/PRODUCTION_AUDIT.md` and may land in a follow-up release.
|
||||
|
||||
## Configuring the endpoint
|
||||
|
||||
1. Sign in as a space admin.
|
||||
2. Go to **Settings → API → Webhooks**.
|
||||
3. Paste your endpoint URL into the **Endpoint URL** field. The endpoint must be HTTPS and reachable from the internet. Local development URLs (e.g. `ngrok` tunnels) are fine for testing.
|
||||
4. Save.
|
||||
5. Click **Show secret** to reveal the signing secret, then **Copy** to copy it. Store it in your receiver's environment, never in source control.
|
||||
6. To rotate the secret, click **Rotate**. A new value is generated and shown immediately. The previous secret stops verifying as soon as you rotate.
|
||||
|
||||
## Payload envelope
|
||||
|
||||
Every delivery is a JSON `POST` with the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "evt_01HZ8KQ8X1T2W3Y4Z5A6B7C8D9",
|
||||
"type": "payment.recorded",
|
||||
"created_at": "2026-05-14T22:00:00Z",
|
||||
"space_id": "8f3a...",
|
||||
"data": {
|
||||
"...": "event-specific payload"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `id` — a stable identifier you can use for de-duplication. Format: ULID prefixed with `evt_`.
|
||||
- `type` — dotted event type. Current vocabulary: `member.created`, `member.updated`, `member.removed`, `payment.recorded`, `payment.failed`, `proposal.created`, `proposal.closed`, `incident.created`, `policy.published`.
|
||||
- `created_at` — ISO 8601 UTC.
|
||||
- `space_id` — UUID of the space that owns the event.
|
||||
- `data` — event-specific payload. See the per-type schema in `docs/API_REFERENCE.md`.
|
||||
|
||||
## Signing and verification
|
||||
|
||||
Every request includes two headers:
|
||||
|
||||
```
|
||||
X-Hackerspace-Timestamp: 1747257600
|
||||
X-Hackerspace-Signature: t=1747257600,v1=<hex-hmac-sha256>
|
||||
```
|
||||
|
||||
The signature is computed as:
|
||||
|
||||
```
|
||||
v1 = HMAC_SHA256(secret, f"{timestamp}.{raw_request_body}")
|
||||
```
|
||||
|
||||
To verify a delivery, your receiver should:
|
||||
|
||||
1. Read the raw request body before any JSON parsing.
|
||||
2. Extract the `t` and `v1` values from `X-Hackerspace-Signature`.
|
||||
3. Reject the request if `|now - t| > 300` seconds (replay protection).
|
||||
4. Compute the expected signature using your stored secret.
|
||||
5. Compare with a constant-time comparison.
|
||||
|
||||
### Node.js example
|
||||
|
||||
```ts
|
||||
import crypto from "node:crypto";
|
||||
import express from "express";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.post(
|
||||
"/webhooks/hackerspace",
|
||||
express.raw({ type: "application/json" }),
|
||||
(req, res) => {
|
||||
const header = req.header("X-Hackerspace-Signature") ?? "";
|
||||
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
|
||||
const t = Number(parts.t);
|
||||
const sig = parts.v1;
|
||||
|
||||
if (!t || !sig) return res.status(400).end();
|
||||
if (Math.abs(Date.now() / 1000 - t) > 300) return res.status(401).end();
|
||||
|
||||
const payload = `${t}.${req.body.toString("utf8")}`;
|
||||
const expected = crypto
|
||||
.createHmac("sha256", process.env.HACKERSPACE_WEBHOOK_SECRET!)
|
||||
.update(payload)
|
||||
.digest("hex");
|
||||
|
||||
const ok =
|
||||
sig.length === expected.length &&
|
||||
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
|
||||
|
||||
if (!ok) return res.status(401).end();
|
||||
|
||||
const event = JSON.parse(req.body.toString("utf8"));
|
||||
// handle event
|
||||
res.status(204).end();
|
||||
}
|
||||
);
|
||||
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
### Python example
|
||||
|
||||
```python
|
||||
import hmac, hashlib, time, os
|
||||
from flask import Flask, request, abort
|
||||
|
||||
app = Flask(__name__)
|
||||
SECRET = os.environ["HACKERSPACE_WEBHOOK_SECRET"].encode()
|
||||
|
||||
@app.post("/webhooks/hackerspace")
|
||||
def handle():
|
||||
header = request.headers.get("X-Hackerspace-Signature", "")
|
||||
parts = dict(p.split("=") for p in header.split(","))
|
||||
t = int(parts.get("t", "0"))
|
||||
sig = parts.get("v1", "")
|
||||
|
||||
if abs(time.time() - t) > 300:
|
||||
abort(401)
|
||||
|
||||
body = request.get_data()
|
||||
payload = f"{t}.".encode() + body
|
||||
expected = hmac.new(SECRET, payload, hashlib.sha256).hexdigest()
|
||||
|
||||
if not hmac.compare_digest(sig, expected):
|
||||
abort(401)
|
||||
|
||||
event = request.get_json(force=True)
|
||||
# handle event
|
||||
return "", 204
|
||||
```
|
||||
|
||||
## Retries and backoff
|
||||
|
||||
A delivery is considered successful when the receiver returns any 2xx status within 10 seconds. Non-2xx responses or timeouts trigger up to five retries with exponential backoff (15s, 1m, 5m, 30m, 2h). After the fifth failed attempt the delivery is dropped and logged on the **Webhooks → Recent deliveries** panel.
|
||||
|
||||
> The delivery history panel and retry policy editor are scheduled for a follow-up release. Until then, retries follow the fixed policy above.
|
||||
|
||||
## Operational notes
|
||||
|
||||
- Webhook deliveries originate from your Droplet's outbound IP. Allowlist that IP if your receiver enforces source restrictions.
|
||||
- The signing secret is stored encrypted at rest in `public.integrations` (per space). Rotating it invalidates the previous value immediately.
|
||||
- Webhook configuration is per space. Multi-space tenants must configure each space individually.
|
||||
Reference in New Issue
Block a user