A self-hostable, organization-scoped knowledge base — spaces, hierarchical pages, comments, and fine-grained role-based access, built on an end-to-end type-safe TypeScript stack.
Wiki is a multi-tenant wiki for teams. Every piece of content lives inside an organization; content is grouped into spaces, and spaces hold a tree of pages. Access is governed by Better Auth's organization plugin with dynamic access control, so permissions are enforced server-side and mirrored in the UI.
The codebase is a pnpm + Turborepo monorepo with a strict boundary between the HTTP surface (apps/server), the business logic and data contracts (packages/api), and the clients that consume them (apps/web, apps/tui). A single oRPC router definition drives both the type-safe RPC surface and a generated OpenAPI document.
- Organizations & members — multi-tenant by default; every query is org-scoped.
- Spaces — group related pages with
public/private/restrictedvisibility. - Pages — draft → published → archived lifecycle, hierarchical nesting with fractional (LexoRank) ordering, per-space auto-deduplicated slugs, and backlink tracking surfaced in the page rail.
- Comments — create, resolve, and moderate; author-or-permission authorization.
- Tags — space-scoped labels, created and applied inline from the page header.
- External links — a curated, reorderable list of references outside the wiki per page; URLs are normalized and scheme-allowlisted to
http(s)on the server, so a stored link can never become ajavascript:href. - Attachments — files stored in any S3-compatible object store (a RustFS service ships with the compose stack); uploads and downloads are proxied by the API so the bucket never faces the internet.
- Portable exports — Space admins can download Markdown, HTML, or JSON ZIP archives with attachments, metadata, tags, and hierarchy; the format is documented.
- Email — invitations, password reset and address verification over SMTP; optional, with delivery disabled when no SMTP host is configured.
- Favorites & subscriptions — personal pins and per-page watch lists.
- Full-text search — PostgreSQL
tsvectorgenerated columns with GIN indexes. - Activity feed / audit log — every mutation records an audit row, including destructive deletes.
- Role-based access control — static and dynamic roles via Better Auth; see docs/permissions.md.
- Guided onboarding — create an organization and optionally seed sample content.
- Type-safe API — one oRPC definition powers both
/rpcand a generated, versioned OpenAPI spec at/v1.
| Layer | Technology |
|---|---|
| Web client | React 19, TanStack Start & Router, Tailwind CSS 4 |
| Terminal client | OpenTUI + React (preview) |
| HTTP server | Hono |
| API / RPC | oRPC with OpenAPI generation |
| Auth | Better Auth (organization + dynamic access control) |
| Database / ORM | PostgreSQL + Drizzle ORM |
| Validation | Zod |
| Tooling | pnpm, Turborepo, Vitest, Oxlint / Oxfmt |
┌─────────────┐ ┌─────────────┐
Browser ───▶ │ apps/web │ │ apps/tui │ ◀─── Terminal
│ TanStack St │ │ OpenTUI │
└──────┬──────┘ └──────┬──────┘
│ type-safe oRPC client │
└───────────┬────────────┘
▼
┌───────────────┐
│ apps/server │ Hono + oRPC handler
│ /rpc /api-… │ (+ generated OpenAPI)
└───────┬───────┘
▼
┌───────────────┐
│ packages/api │ routers · schemas · access control
└───────┬───────┘
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ packages/db │ │ packages/auth│ │ packages/env │
│ Drizzle + PG │ │ Better Auth │ │ typed t3-env │
└──────────────┘ └──────────────┘ └──────────────┘
Reads are gated on space visibility (packages/api/src/lib/access.ts); mutations are gated on organization role (assertOrgPermission). Every write runs in a transaction and appends an audit row.
The only requirement is Docker (with the Compose plugin). Clone the repo, download the guided installer from GitHub Releases, run it:
git clone https://github.com/lucabmn/Wiki.git && cd Wiki
curl -fsSLo installer "https://github.com/lucabmn/Wiki/releases/latest/download/nilovon-wiki-installer-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')"
chmod +x installer && ./installerThe installer is a self-contained terminal wizard: it generates strong
secrets, writes the .env files, builds and starts the stack (database
migrations run automatically inside it), and waits for the health checks.
Open http://localhost:3001 and register; onboarding creates the first
organization.
Public deployment with automatic HTTPS: enter https:// URLs in the
installer form (e.g. https://wiki.example.com, API https://api.example.com,
Collab wss://collab.example.com) plus a Let's Encrypt email — the installer
then brings the stack up behind the bundled Caddy TLS proxy.
The same wizard also handles reconfiguration ("Konfigurieren") and updates
("Updaten"). From a checkout with the dev toolchain you can run it as
pnpm dev:tui instead of downloading the binary. See DEPLOY.md
for details, manual installation, and backups.
- Node.js LTS
- pnpm 10 — pinned via
packageManager; runcorepack enableto pick it up automatically - Docker — for the local PostgreSQL database
- Bun — runs the
serverandtuidev scripts
# 1. Install dependencies
pnpm install
# 2. Start PostgreSQL (detached)
pnpm db:start
# 3. Configure environment
cp apps/server/.env.example apps/server/.env
cp apps/collab/.env.example apps/collab/.env
cp apps/web/.env.example apps/web/.env
# → set BETTER_AUTH_SECRET in the server AND collab files (same value).
# Generate one with: openssl rand -base64 48
# Placeholder values are rejected at startup.
# 4. Apply the schema to your database
pnpm db:push
# 5. Run everything in dev mode
pnpm dev- Web app: http://localhost:3001
- API server: http://localhost:3000 (OpenAPI reference at
/v1)
nilovon-wiki/
├── apps/
│ ├── web/ # React + TanStack Start frontend (:3001)
│ ├── server/ # Hono + oRPC HTTP server (:3000)
│ ├── collab/ # Hocuspocus real-time collaboration server (:1234)
│ └── tui/ # OpenTUI terminal installer (install/configure/update)
├── packages/
│ ├── api/ # oRPC routers, zod schemas, access control (business logic)
│ ├── auth/ # Better Auth config, statement & roles (RBAC source of truth)
│ ├── db/ # Drizzle schema & SQL migrations
│ ├── ui/ # Shared shadcn/ui primitives and styles
│ ├── env/ # Typed environment-variable schemas (t3-env)
│ └── config/ # Shared TypeScript configuration
├── docs/ # Additional documentation
└── docker-compose.yml
Note:
apps/tuiis the guided terminal installer — shipped as a self-contained binary on GitHub Releases (built viapnpm --filter tui compile), or run from the checkout withpnpm dev:tui.
Run from the repository root.
| Script | Description |
|---|---|
pnpm dev |
Start all apps in development mode |
pnpm dev:web |
Start the web app only |
pnpm dev:server |
Start the server only |
pnpm dev:tui |
Start the terminal UI only |
pnpm build |
Build all apps and packages |
pnpm test |
Run the test suite (Vitest, via Turborepo) |
pnpm check-types |
Type-check every package |
pnpm check |
Lint and format with Oxlint + Oxfmt |
pnpm db:start |
Start PostgreSQL via Docker Compose (detached) |
pnpm db:push |
Push the schema directly to the database (dev shortcut) |
pnpm db:generate |
Generate SQL migrations from schema changes |
pnpm db:migrate |
Apply pending database migrations |
pnpm db:studio |
Open Drizzle Studio |
pnpm docker:up |
Build and start the full Docker Compose stack |
pnpm docker:down |
Stop the Docker Compose stack |
The recommended path is the installer (see Self-hosting above and DEPLOY.md). The stack is a docker-compose file with four services plus a one-shot migrate service that applies the versioned database migrations automatically before the apps start.
Cookie behavior: over HTTPS auth cookies are issued
SameSite=None; Secure(supports web and API on different subdomains). Over plain HTTP they fall back toSameSite=Lax, so localhost and LAN pilots work without TLS — production deployments should still use the HTTPS overlay.
pnpm docker:up # build + start (web :3001, server :3000, collab :1234, postgres 127.0.0.1:5432)
pnpm docker:logs # tail logs
pnpm docker:down # stopDocker installs are configured entirely through the root .env (written by the installer; annotated template in .env.example):
| Variable | Required | Description |
|---|---|---|
POSTGRES_PASSWORD |
yes | Database password (generated by the installer) |
BETTER_AUTH_SECRET |
yes | Auth signing secret, min 32 chars — placeholders are rejected |
BETTER_AUTH_URL / CORS_ORIGIN |
production | Public API URL / web origin (derived from domains in the overlay) |
VITE_SERVER_URL / VITE_COLLAB_URL |
production | URLs baked into the web bundle at build time |
WEB_DOMAIN, API_DOMAIN, COLLAB_DOMAIN, ACME_EMAIL |
production | Domains + Let's Encrypt email for the Caddy overlay |
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM |
no | Mail delivery; without SMTP_HOST mails are not sent |
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY |
yes | Generated credentials for bundled RustFS attachment storage |
S3_ENDPOINT, S3_BUCKET, S3_REGION, S3_FORCE_PATH_STYLE |
no | Override the bundled S3-compatible storage configuration |
BACKUP_KEEP_DAYS |
no | Retention for the opt-in backup compose profile (default 14) |
For local development (pnpm dev), each app reads its own .env file instead:
apps/server/.env
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
yes | PostgreSQL connection string |
BETTER_AUTH_SECRET |
yes | Auth signing secret (min 32 chars, no placeholders) |
BETTER_AUTH_URL |
yes | Public URL of the server |
CORS_ORIGIN |
yes | Allowed origin of the web app |
NODE_ENV |
no | development (default) / production / test |
APP_NAME |
no | Display name used by auth flows (white-labeling) |
RATE_LIMIT_MAX / RATE_LIMIT_AUTH_MAX |
no | Per-IP requests/minute for the API / auth routes |
RATE_LIMIT_SCIM_MAX |
no | Per-IP requests/minute for SCIM (default 1200) |
SMTP_* |
no | SMTP delivery; unset SMTP_HOST disables delivery |
S3_* |
no | S3-compatible attachment storage |
ATTACHMENT_MAX_MB |
no | Per-file upload ceiling (default 25) |
apps/collab/.env
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
yes | Same database as the server |
BETTER_AUTH_SECRET |
yes | Same secret as the server (verifies collab tokens) |
COLLAB_PORT |
no | WebSocket port (default 1234) |
apps/web/.env
| Variable | Required | Description |
|---|---|---|
VITE_SERVER_URL |
yes | URL the browser uses to reach the API |
VITE_COLLAB_URL |
yes | WebSocket URL of the collab service (ws(s)://) |
See the .env.example files for annotated placeholders.
- Permissions (RBAC) — how authorization works, and how to use it on the backend and frontend.
Contributions are welcome. See CONTRIBUTING.md for the development workflow, and please open an issue before starting large changes. Report security vulnerabilities privately per SECURITY.md.
GPL-3.0 © Nilovon