docs: setup, hosting, API, architecture and roadmap
ci / backend (push) Canceled after 0s
ci / frontend (push) Canceled after 0s

- SETUP: local dev guide (env vars, DB migrate, backend/frontend, troubleshooting)
- HOSTING: bare-metal guide (systemd + Nginx + TLS + backups) and Docker Compose
  guide (deploy, reverse proxy, updates, backup/restore)
- API: REST contract with curl examples and Socket.IO event protocol
- ARCHITECTURE: stack, data model, auth model, realtime design, theming
- ROADMAP: webapp MVP/next steps plus Tauri desktop clients for Windows, macOS
  and Linux with release-binary plan
This commit is contained in:
x3
2026-09-22 13:33:22 +02:00
parent 5252e3ab4f
commit a407a1381a
5 changed files with 413 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
# API Reference
Base URL: `http://localhost:3000/api`. Auth: `Authorization: Bearer <jwt>`.
## Auth
### POST /auth/register
```json
{ "email": "a@example.com", "name": "Ada", "password": "secret123" }
```
-> `201 { user, token }`. Errors: `400` validation, `409` email taken.
### POST /auth/login
```json
{ "email": "a@example.com", "password": "secret123" }
```
-> `200 { user, token }`. Errors: `401` invalid credentials.
### GET /auth/me
-> `200 { user }`.
```bash
curl -X POST localhost:3000/api/auth/register \
-H 'Content-Type: application/json' \
-d '{"email":"a@example.com","name":"Ada","password":"secret123"}'
```
## Workspaces
| Method | Path | Role | Description |
|--------|------|------|-------------|
| GET | `/workspaces` | member | List my workspaces |
| POST | `/workspaces` | any logged-in | Create workspace (caller becomes owner) |
| GET | `/workspaces/:id` | viewer+ | Get workspace |
| PATCH | `/workspaces/:id` | admin+ | Rename |
| DELETE | `/workspaces/:id` | owner | Delete |
| GET | `/workspaces/:id/members` | viewer+ | List members |
| POST | `/workspaces/:id/members` | admin+ | Invite `{email, role}` (role: admin/member/viewer) |
| PATCH | `/workspaces/:id/members/:userId` | admin+ | Change role |
| DELETE | `/workspaces/:id/members/:userId` | admin+ | Remove member |
Roles rank: `viewer < member < admin < owner`. Viewers read only.
## Pages
| Method | Path | Description |
|--------|------|-------------|
| GET | `/pages/by-workspace/:workspaceId` | List pages (flat, client builds tree) |
| POST | `/pages` | Create `{workspace_id, parent_id?, title?}` (member+) |
| GET | `/pages/:id` | Get page + comments |
| PATCH | `/pages/:id` | Update title/icon/cover/content/position/parent/archive (member+) |
| DELETE | `/pages/:id` | Delete incl. children via FK cascade |
| GET | `/pages/search/query?q=&workspace_id=` | Title search |
| POST | `/pages/:id/comments` | Add comment `{body}` |
| DELETE | `/pages/:pageId/comments/:commentId` | Delete (author or admin+) |
Content fields: `content_json` (TipTap JSON), `content_html` (rendered HTML).
## Users
- `GET /users/search?email=` — lookup for invites (auth required).
## Health
- `GET /health` -> `{ok:true, db:'up'}` or `503 {ok:false, db:'down'}`.
## Real-time (Socket.IO)
Connect: `io(url, { auth: { token } })`.
- `page:join { pageId }` — join room; server checks workspace membership.
- `page:edit { pageId, content_json, content_html }` — persist + broadcast to others.
Viewers are ignored server-side.
- `typing { pageId, name }` — broadcast typing indicator.
- Server events: `page:updated { page, from? }`, `page:deleted { id }`,
`comment:added { comment }`, `presence:update { members: [{userId,email,name}] }`,
`error:message { error }`.
+48
View File
@@ -0,0 +1,48 @@
# Architecture
## Stack
- **Frontend:** Vue 3 (Composition API + `<script setup>`), Vite, Vue Router, Pinia,
Axios, Socket.IO client, TipTap editor. Theme via CSS variables (`data-theme`).
- **Backend:** Node.js 20, Express 4, Socket.IO 4, `pg` (node-postgres), JWT, bcryptjs.
- **DB:** PostgreSQL 14+. Single schema file (`backend/src/db/schema.sql`), applied via
`npm run migrate` or `psql`.
## Data model
```text
users (id, email unique, name, password_hash)
└─< workspace_members (workspace_id, user_id, role) >─┐
workspaces (id, name, owner_id -> users) <───────────────┘
└─< pages (id, workspace_id, parent_id self-FK, title, icon, cover,
content_json JSONB, content_html TEXT, position, is_archived)
└─< comments (id, page_id, author_id, body)
```
- Nesting via `parent_id` self-reference (`ON DELETE CASCADE` removes subtrees).
- Owner auto-added as `workspace_members(role=owner)` via trigger.
- `updated_at` maintained by trigger.
## Auth & authorization
- Register/login return a signed JWT (`sub=user.id`). Client stores it in
`localStorage` and sends `Authorization: Bearer`.
- Every workspace/page route resolves the caller's membership role server-side;
viewers are blocked from mutations (`403`).
- Socket.IO handshake verifies JWT; `page:join` and `page:edit` re-check membership
and role against the DB (never trust client role).
## Realtime design (MVP)
- One Socket.IO room per page (`page:<id>`). Edits are debounced client-side (~800ms),
persisted via REST `PATCH /pages/:id`, then broadcast `page:updated`.
- `page:edit` socket events also persist server-side for clients that only use sockets.
- Presence: server keeps `room -> socketId -> user` map, emits `presence:update`.
- No CRDT/OT in MVP — last-write-wins. Concurrent edits converge on the last saved
version; remote updates are ignored while the local user is the sender (`from`).
See ROADMAP for planned Yjs-based conflict-free editing.
## Theming
- `useThemeStore` (`light`/`dark`/`system`), persisted in `localStorage`,
applied as `document.documentElement.dataset.theme`. All colors are CSS variables.
+155
View File
@@ -0,0 +1,155 @@
# Hosting
Two supported ways to host: **A) bare-metal (systemd + Nginx)** and **B) Docker Compose**.
Both need PostgreSQL and a `JWT_SECRET`.
## Option A — Bare-metal (Node.js + Nginx + PostgreSQL)
### 1. Provision
- Server with Ubuntu 22.04+/Debian 12+, Node.js 20, PostgreSQL 14+, Nginx, Certbot.
- DNS `openstack.example.com` pointing at the server.
```bash
sudo apt update && sudo apt install -y nodejs npm postgresql nginx certbot python3-certbot-nginx
```
### 2. Database
```bash
sudo -u postgres psql -c "CREATE USER openstack WITH PASSWORD 'strong-password';"
sudo -u postgres psql -c "CREATE DATABASE openstack OWNER openstack;"
psql "postgres://openstack:strong-password@localhost:5432/openstack" < backend/src/db/schema.sql
```
### 3. Backend (systemd)
```bash
git clone https://git.sysdaemon.xyz/x3/openStack.git /opt/openStack
cd /opt/openStack/backend && npm ci --omit=dev
cat > /opt/openStack/backend/.env <<EOF
DATABASE_URL=postgres://openstack:openstackpw@localhost:5432/openstack
PORT=3000
JWT_SECRET=$(openssl rand -hex 32)
JWT_EXPIRES_IN=7d
CORS_ORIGIN=https://openstack.example.com
EOF
npm run migrate
```
`/etc/systemd/system/openstack-backend.service`:
```ini
[Unit]
Description=openStack Backend
After=network.target postgresql.service
[Service]
WorkingDirectory=/opt/openStack/backend
EnvironmentFile=/opt/openStack/backend/.env
ExecStart=/usr/bin/node src/index.js
Restart=always
User=www-data
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now openstack-backend
curl http://localhost:3000/api/health
```
### 4. Frontend (static build via Nginx)
```bash
cd /opt/openStack/frontend
npm ci && VITE_API_URL=https://openstack.example.com/api VITE_SOCKET_URL=https://openstack.example.com npm run build
sudo mkdir -p /var/www/openstack && sudo cp -r dist/* /var/www/openstack/
```
`/etc/nginx/sites-available/notes`:
```nginx
server {
listen 80;
server_name openstack.example.com;
root /var/www/openstack;
index index.html;
location / { try_files $uri $uri/ /index.html; }
location /api/ { proxy_pass http://127.0.0.1:3000; }
location /socket.io/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
```bash
sudo ln -s /etc/nginx/sites-available/notes /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d openstack.example.com
```
### 5. Backups
```bash
pg_dump "postgres://openstack:...@localhost:5432/openstack" | gzip > backup-$(date +%F).sql.gz
```
---
## Option B — Docker Compose (recommended for VPS / homelab)
Requirements: Docker Engine + Compose plugin.
```bash
git clone https://git.sysdaemon.xyz/x3/openStack.git
cd openStack
cp .env.example .env
# IMPORTANT: set POSTGRES_PASSWORD and JWT_SECRET to strong values
nano .env
docker compose up -d --build
docker compose logs -f backend
curl http://localhost:3000/api/health
```
- Frontend: `http://<host>:8080`
- Backend API: `http://<host>:3000/api/health`
- Data persists in the `pgdata` volume.
### Reverse proxy (TLS) in front of Compose
Point Nginx/Caddy at `localhost:8080` (frontend) and let the frontend's relative
`/api` calls work — the frontend image is built with `VITE_API_URL=/api`, so no
CORS is needed when served behind one domain. Example Caddy:
```caddy
openstack.example.com {
reverse_proxy localhost:8080
}
```
But note: Socket.IO also needs websocket upgrade; the bundled `nginx.conf` serves
only static files. For single-domain production, proxy `/api` + `/socket.io` to the
backend (see Option A Nginx snippet) and serve the built `frontend/dist` statically,
or extend `frontend/nginx.conf` with upstream proxy blocks.
### Updates
```bash
git pull
docker compose up -d --build
```
### Backup / restore (Compose)
```bash
docker compose exec postgres pg_dump -U openstack openstack | gzip > backup.sql.gz
gunzip -c backup.sql.gz | docker compose exec -T postgres psql -U openstack openstack
```
+60
View File
@@ -0,0 +1,60 @@
# Roadmap
## Phase 1 — Web app MVP (this repo, v0.1) ✅
- [x] Auth (register/login/JWT), workspaces, invites, roles
- [x] Nested pages, TipTap rich text, comments, search
- [x] Socket.IO realtime (live updates, presence, typing)
- [x] Light/dark mode
- [x] Bare-metal + Docker Compose hosting docs
- [ ] Hardening: refresh tokens, email verification, password reset
- [ ] Tests: backend integration tests (auth/workspaces/pages), frontend smoke test
- [ ] Uploads: cover images / attachments (S3-compatible or local volume)
## Phase 2 — Collaboration+ (v0.2)
- [ ] Yjs (Hocuspocus) CRDT editing to replace last-write-wins
- [ ] Page sharing links (public read-only), granular page permissions
- [ ] Version history / restore snapshots
- [ ] Notifications (mentions, invites) + activity feed
- [ ] i18n (English + German at minimum)
## Phase 3 — Desktop clients (v0.3): Windows / macOS / Linux binaries
Goal: installable desktop apps published as **GitHub/Gitea Release binaries**
(`.exe`/`.msi`, `.dmg`, `.AppImage`/`.deb`).
**Chosen approach: Tauri v2** (Rust core + existing Vue frontend as webview):
- Small binaries (~10MB), native OS integration, auto-updater support.
- Alternative considered: Electron (larger, heavier) — fallback if native modules block Tauri.
Steps:
1. [ ] Add `src-tauri/` (Tauri v2 init), reuse `frontend/dist` as bundle frontend.
2. [ ] Deep-link + offline cache: bundle API base-URL setting screen; local-first
drafts in IndexedDB with background sync.
3. [ ] CI build matrix (Gitea Actions / GitHub Actions):
`tauri-action` on `windows-latest`, `macos-latest`, `ubuntu-latest`.
4. [ ] Code signing: Windows (SignPath/Azure Trusted Signing), macOS (Developer ID
+ notarization), Linux (GPG `.sig` + checksums).
5. [ ] Auto-update feed (`updater.json` in Releases) + in-app update prompt.
6. [ ] Publish flow: tag `vX.Y.Z` -> CI builds 3 binaries -> attach to
`https://git.sysdaemon.xyz/x3/openStack/releases`.
7. [ ] Distribution extras: winget/Scoop manifests, Homebrew cask, Flathub submission.
Release artifact naming:
```text
openStack_0.3.0_x64-setup.exe (Windows)
openStack_0.3.0_x64.dmg (macOS Intel)
openStack_0.3.0_aarch64.dmg (macOS Apple Silicon)
openStack_0.3.0_amd64.AppImage (Linux)
```
Track desktop work in issues with label `client`. Web app remains the primary target
until v0.3; API stays compatible (desktop = wrapper around the same frontend + API).
## Phase 4 — Mobile / polish (v0.4+, tentative)
- [ ] PWA packaging (installable web app) before native mobile
- [ ] Evaluate Capacitor/Tauri-mobile for Android/iOS
+73
View File
@@ -0,0 +1,73 @@
# Setup (Development)
## Prerequisites
- Node.js 20+
- PostgreSQL 14+ (local or remote)
- npm 10+
## 1. Clone & configure
```bash
git clone https://git.sysdaemon.xyz/x3/openStack.git
cd openStack
cp .env.example .env
```
Edit `.env`:
| Variable | Purpose | Example |
|----------|---------|---------|
| `DATABASE_URL` | Backend Postgres connection | `postgres://openstack:openstackpw@localhost:5432/openstack` |
| `JWT_SECRET` | Signing key for auth tokens (min 32 random chars) | `openssl rand -hex 32` |
| `JWT_EXPIRES_IN` | Token lifetime | `7d` |
| `CORS_ORIGIN` | Allowed frontend origin(s), comma-separated | `http://localhost:5173` |
| `VITE_API_URL` | Frontend -> backend base URL | `http://localhost:3000/api` |
| `VITE_SOCKET_URL` | Frontend -> Socket.IO URL | `http://localhost:3000` |
| `PORT` | Backend listen port | `3000` |
## 2. Database
```bash
createdb openstack
psql "$DATABASE_URL" < backend/src/db/schema.sql
# or: cd backend && npm install && npm run migrate
```
Verify:
```bash
psql "$DATABASE_URL" -c "SELECT * FROM users LIMIT 1;"
```
## 3. Backend
```bash
cd backend
npm install
npm run dev # watch mode on http://localhost:3000
curl http://localhost:3000/api/health # -> {"ok":true,"db":"up"}
```
## 4. Frontend
```bash
cd frontend
npm install
npm run dev # http://localhost:5173
```
Open the frontend, **Register** the first user, create a workspace, invite teammates
by email (they must register with the same email first).
## Troubleshooting
- `DATABASE_URL is not set` — you forgot `.env` or didn't export it. Backend reads
`backend/.env` too if you copy `.env` there, otherwise set env in shell.
- `db: down` on `/api/health` — Postgres unreachable; check `DATABASE_URL`,
firewall, `pg_isready`.
- CORS errors — `CORS_ORIGIN` must exactly match the frontend origin (scheme+host+port).
- Socket.IO `Invalid token` — log out/in again; token may have expired.
- `relation "users" does not exist` — schema not applied; run migrate step.
- Postgres.app on macOS hangs on `psql -h localhost` — use the socket
(`psql -h /tmp`) or set a password and use TCP; see HOSTING notes.