Winch Labs

Security

Multi-tenancy

Isolation is the shape of the schema, not a feature layered on top. Invariant 2, made concrete.

What holds

Guarantee How
Every table carries org_id, every query filters on it Schema-level, asserted per subsystem rather than once globally
Receipt chains never cross tenants One chain per organisation
Metrics carry no tenant-identifying labels No metric is keyed by repo, PR, organisation, SHA or rule id
Repository listings are filtered by installation binding A tenant sees only what their installation covers
A sealed secret is bound to its own organisation row The envelope’s additional authenticated data includes the org id, so a ciphertext moved to another row will not open

Why the metrics rule matters

A /metrics endpoint labelled by repository or organisation is a tenant-enumeration channel: anyone who can scrape it learns your customer list, and possibly their repository names. So no metric carries those labels — which costs some observability granularity and is worth it.

It also means /metrics is gated with the rest of the API surface once auth is on. A ServiceMonitor scraping it unauthenticated will break at that point; that is the control working.

Why it is asserted per subsystem

Tests such as TestServerGateBudgetIsOrgScoped, TestFixLinkIsOrgScoped and TestRCDHistory_OrgScoped each assert scoping for their own area, rather than one global test asserting it everywhere.

A single global assertion is exactly the kind that quietly stops covering the next table somebody adds. Per-subsystem tests fail in the subsystem that broke.

The webhook path deserves a note

Session-less paths — the webhook above all — pass the installation’s organisation explicitly rather than letting a context lookup fall back to a default. Without that, a tenant’s muted rules would silently stop applying to their own pull requests: the finding would appear, correctly scoped in every other respect, and the tenant’s own decision to hide it would be ignored.

That is the kind of bug that is invisible until a customer asks why a rule they muted is still commenting.

What we do not publish

The schema itself, and the signed-off list of places where isolation is deliberately enforced by something other than a WHERE org_id clause. That list exists, it is reviewed, and it is a precise map of where to look — which is why it stays internal. Ask under NDA and we will walk you through it.