Tenant integration
Espejo is multi-tenant by design (see Core concepts): one tenant per customer account, one project per thing they record, one public key per project embedded in that product’s pages. Integrating a new tenant is the same shape regardless of who it is — this page walks through it using Angirú, the agricultural/livestock management platform, which is tenant #1.
Why Angirú specifically
Section titled “Why Angirú specifically”Espejo’s own README says it plainly: the project started as Angirú’s
in-house recorder — a Chrome extension that worked but required an install,
had no concept of tenant, and only saw network traffic half the time
(chrome.webRequest can’t read bodies without the DevTools protocol, which
shows the intrusive “being debugged” banner). Espejo generalized that into a
<script>-based, multi-tenant recorder; the extension itself survives only
for what a page script structurally can’t do — following several tabs and
capturing things outside the browser.
That history is also why the storage adapter’s S3 client is shaped
specifically for R2 (region: 'auto', forcePathStyle: true) — it’s the
same shape already proven against R2 by Angirú’s original recorder.
The mechanics of onboarding a tenant
Section titled “The mechanics of onboarding a tenant”-
A tenant and a project exist in Postgres
Section titled “A tenant and a project exist in Postgres”Right now, before the console exists, that’s
BootstrapService(apps/api/src/bootstrap.service.ts): on every boot it idempotently ensures one tenant/project/key from environment variables. Angirú’s is configured exactly this way in.env.example:Terminal window BOOTSTRAP_TENANT=AngiruBOOTSTRAP_PROJECT_SLUG=angiruBOOTSTRAP_PROJECT_NAME=AngiruBOOTSTRAP_INGEST_KEY=pk_live_cambiameOnce the console ships, this becomes
POST /v1/projectsfrom a real user session instead (see Console API) — the bootstrap variables are a bridge, not the permanent path, and the plan is to deleteBootstrapServiceonce that screen exists. -
The product embeds the SDK
Section titled “The product embeds the SDK”Angirú’s frontend drops in the script tag with its project’s public key, same as any integrator:
<script src="https://dbuger.dnh.ar/sdk/espejo.js" data-key="pk_live_..."></script>and wires its own “Report a problem” UI to call
espejo.report(description)(see Quickstart), which uploads whatever is in the ring buffer at that moment and returns the recording’s URL. -
What happens after
Section titled “What happens after report() is Angirú’s concern, not Espejo’s”report()is Angirú’s concern, not Espejo’sEspejo’s job ends at handing back a session URL. What a specific tenant does with that URL — attach it to a support ticket, open a GitHub issue, page someone — is product logic that lives in that tenant’s own codebase, never in Espejo. Angirú, for instance, turns a report into a GitHub issue on its own side; that flow (and its docs) live in Angirú’s repository, not here — this repo only produces the recording and the link to it.
Isolation between tenants
Section titled “Isolation between tenants”Nothing about onboarding a second tenant is special-cased for Angirú. The guarantees that keep tenants apart are structural, described in full in Core concepts and MCP server:
- Every query that touches a session filters through the
project.tenant_idrelation — never a resolved list of ids that could go stale. - A session or a scope miss returns the same
404as one that doesn’t exist, in the console API and in the MCP tools alike — never a403that confirms the id is real. - The admin key (
x-espejo-admin-key) sees every tenant, which is exactly why it’s refused wherever access is granted on a person’s behalf: creating a project, or anything under/v1/mcp/*. - An MCP connection’s project scope is fixed once, at consent time, from
user.tenant.id— never from a request parameter.