Knowtation Hub
REST API + OAuth (Google/GitHub) + JWT for the Knowtation vault and proposals. Phase 11. Run self-hosted (Node or Docker) or use the same API contract on ICP.
Run locally (from repo root)
- Install hub dependencies:
cd hub && npm install && cd .. - Set env:
KNOWTATION_VAULT_PATH(required),HUB_JWT_SECRET(required in production), optional OAuth and port (see below). - Start:
npm run hubornode hub/server.mjs
Default port: 3333. Open http://localhost:3333/ in a browser for the Rich Hub UI (served by the same process). Health: GET http://localhost:3333/health. API base: http://localhost:3333/api/v1 (all routes require Authorization: Bearer <jwt> except health, static UI, and auth).
Environment
| Variable | Required | Description |
|---|---|---|
KNOWTATION_VAULT_PATH |
Yes | Absolute path to vault directory (or set in config/local.yaml). |
HUB_JWT_SECRET |
Yes (prod) | Secret for signing JWTs. Use a long random string in production. |
HUB_PORT |
No | Port (default 3333). |
HUB_BASE_URL |
No | Base URL for OAuth callbacks (default http://localhost:3333). |
HUB_UI_ORIGIN |
No | Redirect after login (defaults to Hub base URL, same tab). |
GOOGLE_CLIENT_ID |
For Google login | From Google Cloud Console. |
GOOGLE_CLIENT_SECRET |
For Google login | From Google Cloud Console. |
GITHUB_CLIENT_ID |
For GitHub login | From GitHub OAuth App. |
GITHUB_CLIENT_SECRET |
For GitHub login | From GitHub OAuth App. |
KNOWTATION_HUB_PROPOSAL_ENRICH |
No | Set to 1 to enable POST /api/v1/proposals/:id/enrich (LLM summary + suggested labels; needs OpenAI or Ollama chat). |
Log in (OAuth) — required for the Hub UI
There is no separate “Sign up”. Identity is Google or GitHub: the first time you sign in, that account is tied to your session; there is no email/password form on Knowtation.
Until OAuth is configured, Log in will not work. Add at least one provider to .env, restart the Hub, then use Continue with Google or Continue with GitHub.
- Google Cloud Console → APIs & Services → Credentials → Create credentials → OAuth client ID (configure consent screen if asked). Application type: Web application.
- Authorized redirect URIs must match exactly what the server sends (scheme, host, port, path). There are two stacks:
- This server (
npm run hub, port 3333 by default):
http://localhost:3333/api/v1/auth/callback/google - Gateway (hosted OAuth; see hub/gateway/README.md): path is
/auth/callback/google, not/api/v1/.... Example local gateway on port 3340:
http://localhost:3340/auth/callback/google
If you run the gateway on port 3333, setHUB_BASE_URL=http://localhost:3333and addhttp://localhost:3333/auth/callback/googleto Google.
Production gateway:https://YOUR-GATEWAY-HOST/auth/callback/google. You can add multiple redirect URIs to one OAuth client (local full Hub + local gateway + production).
- This server (
- Copy Client ID and Client secret into
.env:
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=... - Restart
npm run huband openhttp://localhost:3333/→ Continue with Google.
Error 400: redirect_uri_mismatch: The URI Google receives is not in the list above. Compare with what your process uses: full Hub → /api/v1/auth/callback/google; gateway → /auth/callback/google. Port must match HUB_BASE_URL (gateway) or HUB_PORT (full Hub).
GitHub
- GitHub → Settings → Developer settings → OAuth Apps → New OAuth App.
Authorization callback URL(s) — add both (or the one you use):http://localhost:3333/api/v1/auth/callback/github(login)http://localhost:3333/api/v1/auth/callback/github-connect(Connect GitHub in Settings, for vault push)
- Create the app; copy Client ID and generate a Client secret into
.env:
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=... - Restart the Hub → Continue with GitHub.
If you see GitHub’s “Be careful! … redirect_uri is not associated with this application”: the Authorization callback URL in your GitHub OAuth App must match exactly what this server uses — fornpm run hubthat ishttp://localhost:3333/api/v1/auth/callback/github(note/api/v1/; not/auth/callback/github, which is the gateway path). Add both callback URLs from step 1 if anything is missing.
If you use Connect GitHub: the same OAuth App must also listhttp://localhost:3333/api/v1/auth/callback/github-connect.
Production: setHUB_BASE_URL=https://your-domain.comand addhttps://your-domain.com/api/v1/auth/callback/githubandhttps://your-domain.com/api/v1/auth/callback/github-connectto the GitHub app.
Hosted product (gateway + bridge): the Connect GitHub callback lives on the bridge (/auth/callback/github-connecton the bridge origin), not on the full Hub server. Copy-paste table: docs/CONNECT-GITHUB-AND-STORAGE-CHECK.md §0.
Docker
From repo root:
docker build -f hub/Dockerfile -t knowtation-hub .
docker run -p 3333:3333 \
-e KNOWTATION_VAULT_PATH=/data/vault \
-e HUB_JWT_SECRET=your-secret \
-e GOOGLE_CLIENT_ID=... \
-e GOOGLE_CLIENT_SECRET=... \
-v /path/to/vault:/data/vault \
-v /path/to/data:/app/data \
knowtation-hub
Proposals are stored under data_dir (e.g. /app/data/hub_proposals.json). Mount a volume for persistence.
Roles (Phase 13)
First-run: no roles file (works for everyone)
When there is no data/hub_roles.json file (or the file has no entries), everyone who logs in receives role admin. No manual setup or hardcoded IDs. You see the Team tab in Settings and have full access. This is the default so every new install works without intervention.
When you want to restrict who can do what, use Settings → Team and add the first role (e.g. yourself as admin, or a teammate as viewer). That creates the roles file. From then on, only users listed in the file get the role you set; anyone not listed gets member (editor). To avoid locking yourself out, add your own User ID as admin in the Team tab before adding others with more restricted roles.
Why roles?
Roles let you control who can do what on a shared vault without giving everyone full access. For example: give teammates viewer (read-only), editor (can add notes and create proposals but not approve them or change Setup), or admin (full access). That way you can share the Hub URL and still decide who can change backup settings or approve agent proposals.
How assignment works (user ID, not email or handle)
Role assignment uses a user ID of the form provider:id — for example github:12345678 or google:109876543210987654321. This is the OAuth subject ID from Google or GitHub, not the person’s email or GitHub username. So you do not put [email protected] or alice-github in the file; you put the ID the Hub uses internally (e.g. github:12345).
How to get someone’s user ID
How to assign a role: A backup repo is not required for roles. Two options:
Option A — From the Hub (recommended)
If you are an admin: open Settings → Team (only admins see it). Have the other person copy Your user ID from their Settings and send it to you. Paste it in the Team tab, choose Role (viewer / editor / admin), click Add / update role.
Option B — Edit the file
On the server, create or edit data/hub_roles.json (see format below); restart the Hub or save Setup once to reload.
The roles file
No file or empty file: Everyone is admin (see "First-run" above). File with entries: Only listed users get the assigned role; others get member (editor). To restrict access, create or edit data/hub_roles.json (or use Settings → Team). Format:
{
"roles": {
"github:12345": "admin",
"google:67890": "editor",
"github:11111": "viewer"
}
}
- Viewer — Can only read (notes, search, proposals). Cannot write, delete, propose, approve, or change Setup.
- Editor — Can read, write and delete notes, and create proposals; cannot change Setup or approve/discard proposals.
- Admin — Full access: everything above plus Setup and approve/discard.
The Hub UI shows Your role and Your user ID in Settings so users know their role and can share their ID with an admin. A future invite flow (Phase 13) may allow assigning by email or invite link so you don’t have to manage the JSON file by hand.
Role changes vs JWT: The JWT still stores whatever role you had at login. For API checks and GET /api/v1/settings, the Hub uses data/hub_roles.json (current file) as the source of truth, so Team changes (e.g. promoting someone to evaluator or toggling May approve) apply on the next request without logging out.
Note provenance (self-hosted Hub)
On POST /api/v1/notes, POST /api/v1/import (second pass on each imported path), POST /api/v1/capture, and proposal approve, the Hub merges server-controlled YAML frontmatter so clients cannot forge identity:
| Key | When |
|---|---|
knowtation_editor |
OAuth sub (provider:id) for the acting user, when known |
knowtation_edited_at |
ISO-8601 UTC timestamp of the write |
author_kind |
human | webhook | agent | import |
knowtation_proposed_by |
Set on approve from the stored proposal |
knowtation_approved_by |
Set on approve to the admin’s sub |
Webhook capture does not set knowtation_editor (no logged-in user). New proposals store proposed_by in hub_proposals.json.
Hosted (canister): note writes through the gateway/canister do not use this Node Hub path. To get the same fields in hosted backups, mirror this merge where hosted notes are persisted (Motoko / bridge) using the authenticated principal or JWT sub.
Optional: signed Git commits (single Hub identity)
Signed commits prove a private key possessed by the backup process signed the commit — they do not identify which OAuth user edited a note (use frontmatter for that). To get GitHub “Verified” on backup commits:
- Create a dedicated GPG or SSH signing key for the machine that runs
npm run hub. - Register the public key with GitHub; use a verified email for
git config user.emailin the vault (or global config for that user). - In the vault directory (or globally):
git config commit.gpgsign true,git config user.signingkey <key-id>(GPG) or use SSH signing withgpg.format sshand a public key path. - Ensure non-interactive signing (
gpg-agent/ssh-agent) solib/vault-git-sync.mjscommits from the Hub do not hang.
All backup commits will show one signer (the Hub), not per-editor identities.
API contract
See docs/HUB-API.md for routes, request/response shapes, and auth (JWT, OAuth, ICP).