Setting up a new tenant's knowledge base
How to stand up an isolated agent-native knowledge base (the
knowledge-base wiki space) for a new team — one that agents read index-first
and that compounds from their work, fully isolated from other tenants by
Row-Level Security.
For what the KB is and how it operates, read the agent-native knowledge base concept page first; for the read path agents use, see KB retrieval.
The model: a team that needs its own KB = its own tenant
- Tenant = the RLS isolation boundary. A team that needs an isolated KB gets its own tenant.
- Organisation = a sub-entity within a tenant (ADR-005). Orgs in one tenant share the tenant's RLS scope, so they share one KB.
- Therefore: isolated KB ⇒ separate tenant, not just a separate org.
Prerequisite — verify RLS is actually enforced (do this first)
The entire multi-tenant isolation rests on the app connecting to Postgres as a
non-BYPASSRLS role. If the runtime connects as a superuser / BYPASSRLS
role, all RLS is inert — one tenant could read another's data, and per-tenant
KB isolation is illusory.
Before onboarding any team whose KB must be isolated, confirm the runtime connects as a non-bypass role. If it does not, fix that first. Everything below assumes RLS is genuinely enforced.
Setup steps
1. Provision the tenant + org (Directory)
Create the tenant (types: AGENCY / SUPPLIER / PROGRAMME_OFFICE /
PLATFORM_OPERATOR), at least one organisation, and an admin user. This is the
RLS boundary every later step inherits.
2. Create the knowledge-base space (in the new tenant)
Use exactly the slug knowledge-base. That well-known slug is what wires the
coordinator read path and consult file-back to the space. Mark it agent-owned and
give it a description (mirror the platform KB space). One KB space per tenant.
3. Give agents tenant-scoped access
Access is tenant-scoped either way: the credential carries tenant_id, so
every MCP / pt CLI call RLS-scopes to this tenant — a credential for Team A
can never read Team B's KB.
Developers and interactive agents — pt login (no key to mint):
export PT_BASE_URL=https://constellation.planetb2b.com/projects
npx pt login
Then, for Claude Code, register the MCP server once per machine from the
main checkout (claude mcp add writes Claude Code's config only):
claude mcp add constellation -s user \
-e PT_BASE_URL=https://constellation.planetb2b.com/projects \
-e WIKI_BASE_URL=https://constellation.planetb2b.com/wiki \
-- npx -y tsx "$(pwd)/tools/mcp-server/src/index.ts"
Claude Desktop (claude_desktop_config.json), Codex, Cursor, Aider, and hosted
(Streamable HTTP) clients each have their own registration — see the
MCP server setup for the canonical per-runtime detail.
CI / headless only — where a browser consent is impossible — mint a
tenant-scoped API key (Settings → API Keys, in the tenant): shown once, ~90-day
default. Note it takes precedence over the pt login store, so never leave
one set on a developer machine. See the
MCP server setup.
4. Wire the environment
| Variable | Where | Why |
|---|---|---|
WIKI_BASE_URL | each developer's MCP registration (claude mcp add … -e WIKI_BASE_URL=…) / the host's own config | Registers the direct wiki MCP tools (search_pages, get_page, ingest, lint, …). Does not gate query_knowledge_base — that tool is always registered against the PT client and reads the wiki through PT's WIKI_ZONE_URL. |
WIKI_ZONE_URL | the PT / coordinator runtime | The backend read path for query_knowledge_base and the coordinator KB reader; the wiki-spaces proxy fails closed if it is unset (returns WIKI_BACKEND_NOT_CONFIGURED, never a silent empty). |
CRON_SECRET + DISPATCHER_DATABASE_URL | the wiki runtime (for file-back) | The outbox dispatch-events cron must run for consult file-back to deliver. Use a dispatcher-role DSN. |
KB_READS_EMITTER_SECRET | both the PT runtime and the wiki runtime — same value | Authenticates KB usage-read recording (wiki.kb_reads). Two-sided — see the callout below. Without it the KB works normally but records nothing. |
KB_READS_HASH_SALT | the wiki runtime | Keys the HMAC question hash. Also gates recording: while it is unset, above-UNCLASSIFIED reads are not recorded at all (only UNCLASSIFIED ones are), so recording can be half-dead even with the emitter secret correct. |
KB_READS_EMITTER_SECRET is two-sided — one side is the same as neither (INF-295)KB usage recording spans two separate deployments: project-tracker sends the
secret in the x-kb-reads-emitter header, and the wiki validates it. They must
carry the same value, and all three failure modes are equally silent:
| Misconfiguration | What happens |
|---|---|
| Unset on PT | emitKbUsageRead returns early after one console warning per process lifetime — recording is off for every read on the deployment |
| Unset on the wiki | Every emit gets a 503 KB_READS_EMITTER_NOT_CONFIGURED |
| Set to different values | Every emit gets a 403 — and the PT side swallows it |
The emit is fire-and-forget and post-response (deliberately — a KB read must
never fail or slow because telemetry failed), so none of these reach a
caller. In every case no new rows reach wiki.kb_reads, and what
kb_usage shows depends only on what was recorded before recording stopped:
- Never configured — the report is all-zero, indistinguishable from "no traffic". That is exactly how the 2026-07-20 incident went unnoticed from release until a human traced the code.
- Configured, then broken (secret removed, rotated on one side only, or the
two sides drifted apart) — rows already inside the 1–90 day report window keep
being counted, so the report stays non-zero and looks healthy while
recording is completely dead. It goes quiet only gradually, as the last good
rows age out of the window. A non-zero
kb_usagereport is therefore not evidence that recording is currently working — the counts may be entirely historical.
After setting it, redeploy both projects with a genuine full build — a plain Vercel redeploy can miss newly-added env vars (PLT-361). Then verify that the count moves — a moving count is the only check that distinguishes live recording from stale rows:
Poll for the increase — do not read the count once. The usage emit is
scheduled through Next.js after(), so query-kb returns to you before the
wiki write happens. Reading kb-usage immediately races the emitter and can show
an unchanged count on a perfectly healthy deployment — a false alarm that sends
you looking for a config bug that isn't there.
npx pt kb-usage --days 1 # note the count
npx pt query-kb "verification probe"
# then poll, up to ~30s, until it rises
for i in $(seq 1 10); do
sleep 3
npx pt kb-usage --days 1
done
(The MCP equivalents are kb_usage(days: 1) and query_knowledge_base(...).)
Treat it as failed only if the count never rises across the whole poll window.
Run the probe as an UNCLASSIFIED caller, or set KB_READS_HASH_SALT on the
wiki first — above-UNCLASSIFIED reads are not recorded while that salt is unset,
so the count would stay flat for a reason unrelated to the emitter secret.
None of this is detected automatically yet. Making the unset-secret cases detectable — a wiki-side lint finding, a PT-side check, and a recording-health banner on the report — is rolling out under INF-295 and all three are still pending; until they ship, the two-sided secret has to be checked by hand. The mismatch case stays undetectable even after they land, because neither side can see the other's value; closing it needs a wiki-side rejected-emit counter, tracked as a follow-up.
5. Set the coordinator profile + initiative (PT)
The coordinator is initiative-scoped. Create the team's initiative and its
coordinator_profile (the conventions the coordinator reasons with). The
platform coordinator service is generic — per-team behaviour lives in this
profile.
6. The per-repo convention layer (in the team's repo)
The platform is generic; convention lives in two places — the per-initiative
coordinator_profile (step 5) and the team's repo:
- MCP access — each developer runs
npx pt loginonce (OAuth; no token to distribute) and, for Claude Code, registers the server user-scoped withPT_BASE_URLandWIKI_BASE_URLviaclaude mcp add(Claude Desktop, Codex, and other runtimes use their own config).query_knowledge_baseis always registered and reads the wiki via PT's server-sideWIKI_ZONE_URL, so omitting your localWIKI_BASE_URLdoes not stop it working — what goes missing are the direct wiki tools (get_page,search_pages, …). A staticPT_AUTH_TOKEN(CI/headless) or aPT_AUTH_COOKIE(CI/headless, or local dev against a logged-in browser / mock-auth session) both take precedence over the OAuth store, so a leftover one silently shadows the login. - The
consult-the-kbskill — so agents query the KB before implementing and cite what they used. .claude/agents/pt-coordinator.md— copied in, with their initiative ID.
7. Seed the KB (recommended)
A fresh KB is empty. Seed the team's durable canonical knowledge (their
constitution / ADRs / conventions) via ingest_source (human-approved, with
approvedBy), in curated waves — not a raw dump. ADRs and locked decisions
are the best seed material; volatile documents create curation churn. See the
knowledge-base concept page
for what makes a good ingestion candidate.
Verify it works
- Read path: a live consult on the team's initiative returns citations referencing seeded KB pages.
- File-back loop: a successful UNCLASSIFIED consult files back a
synthesis-consult-*page (andevents.deliveriesgrows — proving the dispatcher delivers). Choose the test question with care, because file-back is gated: ask something durable ("why is X built this way", "what is the convention for Y") that the answer can cite a seeded page for. A planning or status question ("what should I work on next") is classifiedephemeral, and an uncited answer fails the quality gate — neither writes a page, and neither means the setup is broken. A near-duplicate of an existing synthesis is skipped as well. (Cycle-close retrospectives are the one exception — they file back uncited; see Knowledge base.) - Index health: the space
indexis a small hub-and-spoke, under the coordinator reader cap. - Isolation: a token for this tenant cannot see another tenant's KB (the RLS check from the prerequisite, now proven end to end).
Keep it healthy (curation cadence)
- Daily mechanical lint — automated cron: orphan / broken-crossref / provenance-drift.
- Per-release LLM judgement pass + nomination-queue processing: stale-claim / contradiction findings + ingesting newly-nominated pages.
- See Wiki lint & curation and the
wiki-curatorsubagent.
What is not shared across tenants (yet)
Each tenant's KB is fully isolated. Common knowledge — a shared constitution, platform ADRs, cross-team conventions — is duplicated per tenant today; there is no shared/federated KB. A cross-tenant / platform shared KB is a future enhancement, not a current capability.
Deployment models
| Model | Trade-off |
|---|---|
| (a) Additional tenant, shared instance | Fastest. The team is a tenant with its own KB. Works today — modulo the RLS prerequisite above. |
| (b) Separate Constellation deployment | Heavier, full infra isolation. SaaS / Dedicated-Cloud / On-Prem tiers; each deployment gets its own KB. See Deployment. |