Skip to content

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.

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.

  1. 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=Angiru
    BOOTSTRAP_PROJECT_SLUG=angiru
    BOOTSTRAP_PROJECT_NAME=Angiru
    BOOTSTRAP_INGEST_KEY=pk_live_cambiame

    Once the console ships, this becomes POST /v1/projects from a real user session instead (see Console API) — the bootstrap variables are a bridge, not the permanent path, and the plan is to delete BootstrapService once that screen exists.

  2. 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.

  3. What happens after report() is Angirú’s concern, not Espejo’s

    Section titled “What happens after report() is Angirú’s concern, not Espejo’s”

    Espejo’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.

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_id relation — never a resolved list of ids that could go stale.
  • A session or a scope miss returns the same 404 as one that doesn’t exist, in the console API and in the MCP tools alike — never a 403 that 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.