docs: setup, hosting, API, architecture and roadmap
- 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:
+77
@@ -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 }`.
|
||||
@@ -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
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user