Multiple vaults and scoped access
(Also split vault: splitting personal vs shared content, or multiple vaults in one place.)
This doc answers: What if I invite a teammate but don’t want them to see all my personal notes? How do we differentiate shared vs private content? Do we support multiple vaults, or filters/gates?
Current behavior (as implemented)
Phase 15: Multi-vault and scoped access (self-hosted)
- Config: Self-hosted Hub supports multiple vaults via
data/hub_vaults.yaml(id, path, label per vault; at least one with iddefault). If the file is absent, a single vaultdefaultfromvault_pathis used (backward compatible). - Vault access:
data/hub_vault_access.jsonmaps user IDs to allowed vault IDs. Users not listed get["default"]only. - Scope (Option B):
data/hub_scope.jsonrestricts a user to specific projects and folders within a vault. Omitted or empty = full vault. Projects are inferred from the note’s path (e.g. undervault/projects/Launch/) or from frontmatter (project: Launch); both are used for filters and scope. To put a note’s file in a project folder, use the Hub + New note → “New note” tab and set Path to e.g.projects/Launch/note.md; Quick capture always writes toinbox/and can set project/tags in frontmatter only. Full detail: How to use in the Hub, Step 6 — “Notes: project, path, and tags”. - API: All vault-scoped requests accept
X-Vault-Id(or queryvault_id). The server resolves vault, checks access, applies scope for list/search/facets. Proposals are keyed byvault_id. - Hub UI: Vault switcher in the header (when multiple vaults are allowed); Settings → Vaults (admin): vault list, vault access, scope (JSON edit). Settings → Backup → Danger zone includes Delete vault for non-
defaultvaults (admins on self-hosted; hosted: same rules as creating a cloud vault — workspace owner when set). API:DELETE /api/v1/vaults/:vaultId(see HUB-API.md §3.3.2). - Roles: Unchanged: viewer / editor / admin control actions. Vault access and scope control which vault(s) and which projects/folders a user sees.
Hosted (canister + gateway + bridge) — Phase 15.1 (repo vs production)
Self-hosted multi-vault (above) is fully implemented. Hosted partitions notes by (userId, vault_id) on the canister. Team vault allowlists and scope are enforced via the bridge + gateway: operators set POST /api/v1/workspace owner_user_id so invited teammates use the owner’s canister partition; vault-access and scope JSON match self-hosted semantics. See TEAMS-AND-COLLABORATION.md and HUB-API.md (workspace and vault routes).
Repository behavior (current code):
| Layer | Role of X-Vault-Id (default vault id: default when omitted) |
|---|---|
| Hub UI | Sends X-Vault-Id on API calls from the vault switcher / stored selection (web/hub/hub.js). |
| Gateway (hub/gateway/server.mjs) | Proxies to the canister with x-user-id from JWT and x-vault-id from the client; GET /api/v1/settings loads vault_list / allowed_vault_ids from canister GET /api/v1/vaults. Facets use the same vault header. |
| Canister (hub/icp/src/hub/main.mo) | vaultIdFromRequest; notes and export are getVault(uid, vault_id); GET /api/v1/vaults lists vault ids persisted for that user. Mutations run in http_request_update and call saveStable — a new vault id appears in the vault list only after a write (e.g. POST /api/v1/notes) for that id, not from a cold GET alone. Proposals carry vault_id and list/filter by active vault. |
| Bridge (hub/bridge/server.mjs) | Index/search storage and GitHub backup export use X-Vault-Id when calling the canister; vectors are keyed by (uid, vault_id). |
Production: Treat per-vault isolation as live only after the ICP canister and gateway are redeployed from this repo and you run smoke checks from hub/gateway/README.md (health, auth, multi-vault headers).
Migration: V0→V1 stable-memory migration and reserved billing fields are in hub/icp/src/hub/Migration.mo. npm run canister:verify-migration is a static source check; it does not call the network.
Hosted multi-vault — Phase 15.1 checklist (status)
Order was: operational hosted baseline → canister partition → verify → polish.
| # | Work item | Status in repo | Notes |
|---|---|---|---|
| 1 | Bridge + gateway BRIDGE_URL, smoke: login, note CRUD, index/search |
Done (ops) | hub/gateway/README.md — deploy + smoke |
| 2 | Canister: X-Vault-Id, partition (uid, vault_id) → path → note |
Done | hub/icp/src/hub/main.mo |
| 3 | Canister: export / list / get / post scoped to vault_id |
Done | Same |
| 4 | Proposals: vault_id + filter by vault |
Done | Same |
| 5 | Bridge vault/sync + export scoped by X-Vault-Id |
Done | hub/bridge/server.mjs |
| 6 | Vault list source of truth on hosted | Done (canister-derived) | Vault access / scope for teammates: bridge + gateway — TEAMS-AND-COLLABORATION.md |
| 7 | Gateway GET /api/v1/settings vault_list / allowed_vault_ids |
Done | Fetches canister /api/v1/vaults |
| 8 | Tests + migration static verify | Ongoing | npm test; npm run canister:verify-migration |
| 9 | Hub Settings → Vaults → Create vault (hosted) | Done | PR #47 — bootstrap note + X-Vault-Id; refreshes switcher and panel |
| 10 | Hub busy state on slow POSTs (save, sync, vaults, team, …) | Done | PR #48 — label + disabled + spinner (.btn-busy) |
Optional product polish (not required for data parity): Second-vault bootstrap is largely addressed by row 9 (Create vault in Settings); agents/CLI can still target a new vault id via X-Vault-Id as before.
Next hosted parity: Track gaps in PARITY-MATRIX-HOSTED.md. Team vault access + scope ship in repo via bridge + gateway (operators set POST /api/v1/workspace owner_user_id for shared partition). MCP — see AGENT-INTEGRATION.md §2.
How to configure multi-vault (Phase 15)
All files below live in the Hub data directory, usually data/ (relative to the project root, or the path set in config/local.yaml under data_dir). You can edit them with any text editor, or use Settings → Vaults in the Hub (admin) to edit vault access and scope as JSON.
1. Find your user ID
- Open the Hub → Settings → Backup tab.
- Under Your user ID you’ll see a value like
google:104164334692309763642orgithub:12345678. That is your user ID (format:provider:id). Copy it; you’ll use it in the JSON files.
2. Vault access — who can see which vaults
File: data/hub_vault_access.json
- Purpose: Maps each user ID to the list of vault IDs they are allowed to use. Users not listed get access only to the vault
default. - Format: A single JSON object. Keys = user IDs (string). Values = arrays of vault IDs (strings).
Example (you and one teammate, both with access to default):
{
"google:104164334692309763642": ["default"],
"github:98765432": ["default"]
}
Example (you have two vaults, teammate only the first):
{
"google:104164334692309763642": ["default", "work"],
"github:98765432": ["default"]
}
- Editing: Create or edit
data/hub_vault_access.jsonwith the structure above. Use your real user ID from Settings → Backup. Save the file. The Hub reads it on each request; no restart needed. - Optional: You can also edit this via Settings → Vaults → Vault access (JSON textarea) → Save vault access.
3. Vault list — which vaults exist (only for multiple vaults)
File: data/hub_vaults.yaml
- Purpose: Defines the set of vaults (id, path, label). If this file is absent, the Hub uses a single vault with id
defaultand the path fromvault_path(orhub_setup.yaml). Create this file only when you want more than one vault. - Format: YAML with a
vaultsarray. At least one entry must haveid: default. Paths can be absolute or relative to the project root.
Example (two vaults):
vaults:
- id: default
path: ./vault
label: Personal
- id: work
path: /Users/me/team-vault
label: Team
- Editing: Create or edit
data/hub_vaults.yaml. Ensure each path exists and is a directory. After changing this file you must restart the Hub (or add/edit vaults via Settings → Vaults → Vault list and Save, which reloads config without restart).
4. Scope — limit a user to certain projects/folders (optional)
File: data/hub_scope.json
- Purpose: Restricts a user to specific projects and/or folders within a vault. If a user has no entry (or an empty one) for a vault, they see the full vault.
- Format: A JSON object: keys = user IDs; values = objects whose keys are vault IDs and whose values are
{ "projects": ["p1", "p2"], "folders": ["folder/path"] }.
Example (user sees only project team-project and folder inbox in vault default):
{
"github:98765432": {
"default": {
"projects": ["team-project"],
"folders": ["inbox"]
}
}
}
- Editing: Create or edit
data/hub_scope.json. Save the file. The Hub reads it on each request; no restart needed. Or use Settings → Vaults → Scope (JSON textarea) → Save scope.
Quick reference
| File | Purpose | Restart after edit? |
|---|---|---|
data/hub_vault_access.json |
User → allowed vault IDs | No |
data/hub_vaults.yaml |
List of vaults (id, path, label) | Yes (or use Settings → Vaults to save) |
data/hub_scope.json |
Per-user per-vault projects/folders limit | No |
For single-vault setups you don’t need to create any of these: everyone gets vault default automatically. Add hub_vault_access.json only if you want to explicitly list users (e.g. with ["default"]) or when you introduce a second vault and need to assign who sees which.
User identity (Google vs GitHub) and multiple users
User ID format: Every logged-in user has a user ID of the form provider:id — for example google:104164334692309763642 (signed in with Google) or github:12345678 (signed in with GitHub). The Hub does not merge identities: if you sign in with Google in one session and with GitHub in another, you have two user IDs and the Hub treats them as two separate users. You see your current user ID in Settings → Backup (“Your user ID”).
Multiple users on the same Hub: You can have many users (many Gmail accounts, many GitHub accounts, or a mix) using the same Hub instance. Each person signs in with their chosen OAuth provider; that gives them one user ID. An admin adds each user ID to data/hub_roles.json (viewer/editor/admin) and, for multi-vault, to data/hub_vault_access.json (which vault IDs they can use). So the same Hub URL can serve a large number of users, each with their own role and vault access (and optional scope). The only limit is how many users you configure and how many distinct OAuth identities sign in.
Backup per user: “Connect GitHub” and “Back up now” are tied to the currently logged-in user. Each user can connect their own GitHub account (one token per user). When they click Back up now, the Hub pushes the currently selected vault’s folder to that vault’s Git remote. So:
- Different users → different repos: Typical case. User A connects GitHub and backs up vault “default” to repo A; user B connects their GitHub and backs up their vault to repo B.
- Same repo, different branches: Possible if you configure each vault folder’s Git remote to point to the same repo with a branch (e.g.
origin mainvsorigin user-branch). The Hub does not manage branches in the UI; you’d set that up in the vault folder’s git config. - Same Hub, many vaults: Each vault is a folder; each folder can be its own Git repo with its own remote. So one user can have access to vaults A and B and, when they switch vault and click Back up now, the Hub pushes that vault’s folder to that folder’s remote.
How to differentiate shared vs private today
Option 1 — Multiple vaults (separate Hub instances)
Supported today: Run more than one Knowtation setup, each with its own vault and (optionally) its own Hub.
| Setup | Vault path | Who uses it |
|---|---|---|
| Personal | e.g. ~/my-vault |
Only you (one user or your own Hub URL). |
| Team / shared | e.g. ~/team-vault or a shared drive |
Shared Hub; invite teammates to this Hub only. |
- How: Two separate deployments (or two
npm run hubprocesses with differentKNOWTATION_VAULT_PATHand ports). Point the “team” Hub at the shared vault; keep the “personal” Hub (or local-only CLI) on your personal vault. No code changes required. - Limitation: Teammates use one URL (team Hub) and see the whole team vault; you use another (personal) for private notes. There is no single login that shows “only my shared projects” — it’s two different apps/vaults.
Option 2 — One vault, organize by project/folder (no isolation)
Supported today: Put shared work under vault/projects/team-project/ and personal under vault/projects/personal/ or vault/inbox/. Use project and folder filters in the Hub UI so that you usually browse by project. This does not hide anything from teammates. Anyone with access can still open “All projects” or search the whole vault. Filters are for convenience, not access control.
Option 3 — Scoped access (not implemented)
Not built: Per-user or per-role visibility rules (e.g. “viewer X can only see project A and B”) or gates (e.g. notes under vault/private/ are hidden from non-admins). Would require:
- A way to define scope (e.g. which projects/folders a role or user can see).
- Every notes list and search (Hub + API) filtering results by that scope.
- Possibly per-note or per-folder “visibility” metadata.
This is a future design; see “What would be needed” below.
Are we set up for multiple vaults?
Today
- Multiple vaults = multiple deployments. The codebase is built for one vault per process. To use “multiple vaults” you run multiple Hub instances (and/or multiple CLI environments), each with its own
vault_path. The app does not support switching vaults in the UI or selecting a vault per request. - No vault selector in the Hub. The Hub has no concept of “choose vault” or “workspace”; it always uses the single vault from config /
hub_setup.yaml.
If we add multi-vault later
Possible directions (for a future phase):
Hub: multiple vaults per instance
Config orhub_setuplists several vaults (e.g. by path or id). Each user or role is assigned to one vault (or a list). API and UI scope all operations to “the vault(s) this user can see.” Requires: vault list in config/setup, user→vault or role→vault mapping, and scoping every read path (list, search, get-note) by that mapping.Hub: one vault, scoped visibility
Single vault; per-user or per-role allowlists for projects or folders. List/search/get-note filter out notes outside the user’s scope. Requires: store scope (e.g. inhub_roles.jsonor a new file), and apply scope in API and Hub UI for notes and search.CLI / MCP
Today the CLI and MCP use a singleKNOWTATION_VAULT_PATH. Multi-vault could mean:--vault <id>flag, or multiple MCP “resources” (one per vault), or separate CLI configs per vault. Agents would then target a vault explicitly.
None of this is implemented; the codebase is not currently set up for (1) or (2) beyond “run another Hub instance.”
What to document for users
- How to use / FAQ: State clearly that inviting a teammate gives them access to the entire vault for that Hub; there are no in-app filters or gates that hide some notes. If they need to keep personal and shared separate, use two vaults and two Hub instances (or one shared Hub + local-only personal vault).
- Settings / Team: When we describe “invite” and “roles,” we should mention that roles control actions (viewer/editor/admin), not which part of the vault is visible. Optionally add a short “Sharing and multiple vaults” link to this doc or a How to use section.
- Roadmap: Treat “scoped access” or “multi-vault in one Hub” as a future phase; capture in TEAMS-AND-COLLABORATION or this doc as a known gap and design option.
Summary
| Question | Answer |
|---|---|
| How is shared vs private differentiated today? | It isn’t within one Hub. Everyone with access sees the same vault. You differentiate by using separate vaults (and separate Hub instances) for personal vs team. |
| What are the filters and gates? | Filters (project, tag, folder) are for narrowing the list; they are not access gates. There are no gates that hide notes from certain users. |
| Do users have to create multiple vaults? | If they want true separation (e.g. don’t expose personal docs to teammates), yes — use multiple vaults and multiple Hub instances. |
| Are we set up for multiple vaults? | Yes in the sense that you can run multiple instances (each with its own vault path). No in the sense that a single Hub cannot serve or switch between multiple vaults; that would require new design and implementation. |
See TEAMS-AND-COLLABORATION.md for roles and invite flow; SPEC.md for single vault root; ARCHITECTURE.md for “one vault, many projects” and filters.
Implementation status (Phase 15)
Done (self-hosted):
- Config/data:
lib/hub-vaults.mjs,hub/hub_vault_access.mjs,hub/hub_scope.mjs;lib/config.mjsloads vault list and exposesvaultListandresolveVaultPath(vaultId). Single-vault default whenhub_vaults.yamlis absent. - Backend: Hub server middleware resolves
req.vault_id, checks vault access, setsreq.vaultPathandreq.scope. List, search, facets, proposals, index, export, import, sync are vault-scoped. Admin routes: GET/POST/api/v1/vaults,/api/v1/vault-access,/api/v1/scope. GET/api/v1/settingsreturnsvault_listandallowed_vault_ids. - Hub UI: Vault switcher (header); Settings → Vaults tab (admin): vault list JSON, vault access JSON, scope JSON with Save; Settings → Backup danger zone — delete non-default vault (PR #66).
- Bridge: Index and search keyed by (uid, vault_id); vectors dir and Blob key include vault_id; canister export request sends X-Vault-Id; delete-vault orchestration for hosted (PR #66).
- Gateway: Forwards
x-vault-idto canister; proxiesDELETE /api/v1/vaults/:vaultIdto bridge when configured (PR #66).
Follow-up (hosted canister): Canister storage keyed by (uid, vault_id) and migration of existing data to vault_id default — see hub/icp/src/hub/Migration.mo and redeploy procedures with hub/gateway/README.md.