Skip to main content

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)

This is the gating step

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

VariableWhereWhy
WIKI_BASE_URLeach developer's MCP registration (claude mcp add … -e WIKI_BASE_URL=…) / the host's own configRegisters 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_URLthe PT / coordinator runtimeThe 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_URLthe 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_SECRETboth the PT runtime and the wiki runtime — same valueAuthenticates 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_SALTthe wiki runtimeKeys 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:

MisconfigurationWhat happens
Unset on PTemitKbUsageRead returns early after one console warning per process lifetime — recording is off for every read on the deployment
Unset on the wikiEvery emit gets a 503 KB_READS_EMITTER_NOT_CONFIGURED
Set to different valuesEvery 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_usage report 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 login once (OAuth; no token to distribute) and, for Claude Code, registers the server user-scoped with PT_BASE_URL and WIKI_BASE_URL via claude mcp add (Claude Desktop, Codex, and other runtimes use their own config). query_knowledge_base is always registered and reads the wiki via PT's server-side WIKI_ZONE_URL, so omitting your local WIKI_BASE_URL does not stop it working — what goes missing are the direct wiki tools (get_page, search_pages, …). A static PT_AUTH_TOKEN (CI/headless) or a PT_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-kb skill — so agents query the KB before implementing and cite what they used.
  • .claude/agents/pt-coordinator.md — copied in, with their initiative ID.

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 (and events.deliveries grows — 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 classified ephemeral, 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 index is 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-curator subagent.

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

ModelTrade-off
(a) Additional tenant, shared instanceFastest. The team is a tenant with its own KB. Works today — modulo the RLS prerequisite above.
(b) Separate Constellation deploymentHeavier, full infra isolation. SaaS / Dedicated-Cloud / On-Prem tiers; each deployment gets its own KB. See Deployment.