From 010c811a482f7e82ace827afe06c48598744a9f7 Mon Sep 17 00:00:00 2001 From: lashman Date: Sat, 21 Mar 2026 19:29:46 +0200 Subject: [PATCH] prep for self-hosting - updated readme, env example, dockerfile, compose, dockerignore --- .dockerignore | 18 ++++++ .env.example | 19 +++---- Dockerfile | 3 +- README.md | 136 +++++++++++++++++++++++++++++++++------------ docker-compose.yml | 1 + 5 files changed, 131 insertions(+), 46 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fe8d14e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +node_modules +dist +build +.env +.env.local +.git +.gitignore +*.md +!README.md +data +uploads +plugins-installed +plugins/*.zip +trash +.claude +.vscode +.idea +coverage diff --git a/.env.example b/.env.example index 292a813..a5f8cae 100644 --- a/.env.example +++ b/.env.example @@ -1,24 +1,23 @@ -# Database -DB_PASSWORD=change-me-to-a-random-string -DATABASE_URL=postgresql://echoboard:change-me-to-a-random-string@db:5432/echoboard +# Database (POSTGRES_PASSWORD is shared between the app and the postgres container) +POSTGRES_PASSWORD=change-me-to-a-strong-random-string -# Encryption (generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))") +# Encryption keys (generate each with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))") APP_MASTER_KEY= APP_BLIND_INDEX_KEY= -# Auth secrets (generate each the same way as above) +# Auth secrets (generate each the same way) TOKEN_SECRET= JWT_SECRET= -# ALTCHA spam protection +# ALTCHA spam protection (generate the same way) ALTCHA_HMAC_KEY= -# WebAuthn / Passkey +# WebAuthn / Passkey (set these to your actual domain) WEBAUTHN_RP_NAME=Echoboard -WEBAUTHN_RP_ID=localhost -WEBAUTHN_ORIGIN=http://localhost:3000 +WEBAUTHN_RP_ID=example.com +WEBAUTHN_ORIGIN=https://example.com -# Web Push (generate with: npx web-push generate-vapid-keys) +# Web push notifications (generate with: npx web-push generate-vapid-keys) VAPID_PUBLIC_KEY= VAPID_PRIVATE_KEY= VAPID_CONTACT=mailto:admin@example.com diff --git a/Dockerfile b/Dockerfile index ddd214d..a6cfd0e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,8 @@ COPY --from=builder /app/node_modules node_modules/ COPY --from=builder /app/echoboard.plugins.ts ./ RUN addgroup -g 1001 echoboard && adduser -u 1001 -G echoboard -D echoboard && \ - mkdir -p /app/packages/api/uploads && chown -R echoboard:echoboard /app + mkdir -p /app/packages/api/uploads /app/packages/api/plugins-installed && \ + chown -R echoboard:echoboard /app ENV NODE_ENV=production EXPOSE 3000 diff --git a/README.md b/README.md index e3343b0..78f2afe 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,122 @@ # Echoboard -A self-hosted feedback board where users submit feature requests and bug reports without creating an account. Anonymous by default, with optional passkey registration for persistence across devices. +Self-hosted feedback board. Users submit feature requests and bug reports without creating an account - anonymous by default, with optional passkey registration for persistence across devices. No email required for anything. -## Quick start +## Self-hosting with Docker ```bash -git clone +git clone https://git.lashman.live/lashman/echoboard.git cd echoboard cp .env.example .env - -# Generate secrets -node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" -# Paste output into APP_MASTER_KEY, APP_BLIND_INDEX_KEY, TOKEN_SECRET, JWT_SECRET, ALTCHA_HMAC_KEY - -# Generate VAPID keys for push notifications -npx web-push generate-vapid-keys -# Paste into VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY - -# Set WEBAUTHN_RP_ID and WEBAUTHN_ORIGIN to your domain - -docker compose up -d -docker compose exec app npx echoboard create-admin --email you@example.com ``` +Edit `.env` and fill in: + +1. Set `POSTGRES_PASSWORD` to something random +2. Generate five secrets (one for each of `APP_MASTER_KEY`, `APP_BLIND_INDEX_KEY`, `TOKEN_SECRET`, `JWT_SECRET`, `ALTCHA_HMAC_KEY`): + ```bash + node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" + ``` +3. Generate VAPID keys for push notifications: + ```bash + npx web-push generate-vapid-keys + ``` +4. Set `WEBAUTHN_RP_ID` to your domain (e.g. `feedback.example.com`) +5. Set `WEBAUTHN_ORIGIN` to your full URL (e.g. `https://feedback.example.com`) + +Then start it: + +```bash +docker compose up -d +``` + +Create the initial admin account: + +```bash +docker compose exec app npx tsx packages/api/src/cli/create-admin.ts +``` + +The app is at `http://localhost:3000` (or whatever port you set). Put a reverse proxy in front for HTTPS. + +## What's included + +- Boards for organizing feedback by project or topic +- Feature requests and bug reports with voting and vote budgets +- Comments with markdown, @mentions, reactions, and file attachments +- Status tracking with custom statuses per board +- Roadmap and changelog pages (with scheduled publishing) +- Full-text search with similar post detection +- Push notifications and board-level subscriptions +- RSS feeds per board +- Post view counts +- Admin dashboard with post management, merge, bulk actions, edit rollback +- Team system with invites - super admin, admin, and moderator roles +- Granular locking - lock post edits, comment edits, threads, or voting independently +- Recovery codes for cookie-based users who can't use passkeys +- Plugin system - upload zip plugins through the admin dashboard, no restart needed +- Embed widget for external sites +- Dark and light themes +- i18n ready +- ALTCHA proof-of-work spam protection (no captchas) + +## Reverse proxy + +Echoboard needs HTTPS for passkeys to work. Example nginx config: + +```nginx +server { + listen 443 ssl; + server_name feedback.example.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://127.0.0.1:3000; + 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; + client_max_body_size 50m; + } +} +``` + +## Updating + +```bash +git pull +docker compose up -d --build +``` + +Migrations run automatically on startup. + +## Data + +All persistent data lives next to the compose file: + +- `./data/postgres/` - database +- `./uploads/` - avatars and attachments +- `./plugins-installed/` - uploaded plugins + +## Plugins + +Plugins are zip files uploaded through the admin dashboard. A plugin contains a `manifest.json` and a JS entry point. Plugins can add API routes, respond to events (post created, status changed, etc.), and store their own data. They run with full server access so only install plugins you trust. + +A Gitea sync plugin is included in `plugins/gitea-sync/` as an example. + ## Development ```bash npm install cp .env.example .env -# Fill in .env with dev values (WEBAUTHN_RP_ID=localhost, WEBAUTHN_ORIGIN=http://localhost:3000) +# set WEBAUTHN_RP_ID=localhost, WEBAUTHN_ORIGIN=http://localhost:5173 +# point DATABASE_URL at a local postgres npm run dev ``` -This starts the API on port 3000 and the Vite dev server on port 5173 (with API proxy). - -## Architecture - -- **packages/api** - Fastify + Prisma backend -- **packages/web** - React + Vite frontend -- **plugins/** - Optional integrations (Gitea, GitHub, etc.) - -## Identity model - -Two tiers of user identity: - -1. **Anonymous cookie** (default) - zero friction, browser-generated token, single device only -2. **Passkey** (optional upgrade) - username + WebAuthn biometric, works across devices, no email needed - -## Plugin system - -Plugins live in `plugins/` and are registered in `echoboard.plugins.ts`. Each plugin is self-contained with its own routes, database tables, and UI components. Removing a plugin leaves zero trace in the core app. +API on port 3001, Vite dev server on port 5173. ## License diff --git a/docker-compose.yml b/docker-compose.yml index bb52829..6741ffe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - "${PORT:-3000}:${PORT:-3000}" volumes: - ./uploads:/app/packages/api/uploads + - ./plugins-installed:/app/packages/api/plugins-installed db: image: postgres:16-alpine