Skip to content

Multi-project on one shared backend — project scoping

mokata's shared backends (a team Postgres DSN for memory, semantic vectors, portable sessions, and the team audit log) are owned + namespaced so they never collide with another app's tables. Stage 71a adds the missing layer: they are also scoped per project, so one shared database can safely host many projects with no cross-project bleed.

Local backends were always clean

Local SQLite (.mokata/…) and the committed vault (.mokata/vault/) are per-repo already. This page is only about the shared Postgres backends.

The project key

Every project has a stable, deterministic project key:

  • Configured: settings.project.id if you set one (mokata config set settings.project.id <id>).
  • Otherwise derived: the git remote URL — normalized so an ssh clone and an https clone of the same repo agree — else the repo's path, hashed to a short token (p_…).

It is stable across sessions (same repo → same key) and machine-path-free. Two clones of the same repo on different machines resolve to the same key (via the shared remote), so they share one project's data; two unrelated repos never do.

Set an explicit, human-friendly key when a team wants everyone to agree regardless of clone URL:

mokata config set settings.project.id acme-web    # human-gated config write

How every shared row is scoped

Each shared table carries a project column; every write, read, list, and delete filters by the current project key. So a recall, a session list, or an audit --team returns only your project's rows even when several projects share one DSN. A session tag like auth no longer collides across projects — each project keeps its own bundle.

Reviewing memory / sessions / audit

Review defaults to the current project. Three flags open it up:

Flag Effect
(none) the current project only (the default)
--all span every project on the shared backend
--project <id> a specific project
--list-projects print the projects present on the shared backend, then exit
mokata memory --list-projects          # which projects are on this shared DB?
mokata memory --project acme-web       # review another project's brain
mokata audit --team --all              # who-did-what across every project
mokata session list --all              # every project's portable sessions

Outside a project (a bare shell, no .mokata/) pointed at a shared DSN, mokata never silently dumps every project. It asks you to choose a scope — --all, --project <id>, or --list-projects to see what's there first.

One DSN per project vs one shared DB

Both work:

  • One shared DB (recommended now): point every project at the same DSN; project scoping keeps them isolated. Simplest to operate — one database for the whole team.
  • One DSN per project: give each project its own database/schema. Also fine (the scoping is simply a no-op when only one project is present). This was the interim operating model before Stage 71a.

Migration — pre-existing shared tables

Tables created before Stage 71a gain the project column when the shared schema is provisioned or upgraded — an idempotent ADD COLUMN IF NOT EXISTS run by mokata team init, which owns all DDL. (Runtime connections run no DDL at all; they only verify the schema, so a least-privilege DML-only role is enough — see team setup. If the column isn't there yet, re-run mokata team init.)

Old rows have no project key and read back as a legacy bucket:

  • Scoped reads (the default) do not show legacy rows — no crash, no surprise.
  • --all surfaces them, and --list-projects shows the legacy bucket, so nothing is hidden silently.

To fold legacy rows into a real project, do a one-time backfill against your database, e.g.:

UPDATE mokata_memory        SET project = 'acme-web' WHERE project IS NULL;
UPDATE mokata_memory_vectors SET project = 'acme-web' WHERE project IS NULL;
UPDATE mokata_session_bundle SET project = 'acme-web' WHERE project IS NULL;

(Use the key mokata config get settings.project.id reports, or your chosen id.) After the backfill those rows scope to that project like any other. Everything stays human-gated + secret-scanned on write, and the DSN secret is never stored — only the env-var name.