Skip to content

Security

We attacked our own product from a second company, and published what happened.

Almost every business tool of this size will tell you that one customer cannot see another's data. Very few have tried to do it on purpose, from a real second account, against every surface at once. We did that before any client's data went in, and this page is the account of it, including the part where we found something.

Nine surfaces, each attacked on its own vector.

A second organization was created beside ours in production shaped infrastructure and used to attack every way data leaves the system. Each surface got the attack that surface is actually vulnerable to rather than a generic one, which is the difference between a red team pass and a checklist.

The attack ledgerTelo's own data
  • CRM role pages, their polling, and the view-as closer surfaceAttempted. A second organization's records carrying the first organization's closer and setter identifiers, plus colliding emails and bookingsResult. No foreign records reached the page
  • Every reader of the deal tableAttempted. A compiler-level enumeration looking for any read that narrows on assignment without also narrowing on the organizationResult. Every reader ruled, and a gate now refuses new ones
  • Threads and the blocks on themAttempted. Foreign anchors and foreign contact, creative, deal and call identifiers substituted into block referencesResult. A concealed absence or a stated gap, never foreign data
  • The cached read models behind the fast surfacesAttempted. A cache key collision across the organization axis and the visibility axis, and a persisted projection served to the wrong readerResult. The key carries both axes, so neither collision is expressible
  • Sigil, the assistantAttempted. Foreign blocks, objects, sessions, ledgers and feedback requested directly, and prompts written to elicit them indirectlyResult. Refused or answered with a gap, with no foreign facts and no fragments
  • Inbound webhooks and connectorsAttempted. Deliveries carrying provider identifiers shared between organizations, and deliveries claiming a tenant that cannot be resolvedResult. Only a trusted connection resolves a delivery, and an unresolved one fails closed
  • Operators, scheduled work, and the post-connection ledgerAttempted. The full connection and heartbeat lifecycle driven across two organizationsResult. Clean, with one test fixture corrected to match the schema
  • Exports, and the shape of our error responsesAttempted. An export requested without an organization, an export requested against an untrusted endpoint, and an object the viewer is not allowed to know existsResult. The export refuses; an object you cannot see returns the same not-found as an object that does not exist, and an actor without permission gets a forbidden
  • Every path previously classified as an accepted exceptionAttempted. The whole classified set re-read rather than trusted from its last reviewResult. No findings

Our own record, from the pass completed on 3 September 2026. Per suite test counts are not published here: they move weekly, and a number nobody is keeping true is worse on this page than on any other.

The first run stopped early, because it worked.

Partway through, the pass found a real one. Our sales dashboards narrowed their reads by which salesperson a deal was assigned to, and assumed that identifier was unique enough to be a boundary. It is not. Given a second company whose records happened to reference the first company's salesperson identifier, those dashboards would have shown payment, pipeline and booking facts across the line.

The pass stopped there rather than carrying on to collect a clean sounding report around it. The defect was filed as urgent, fixed at the cause with a test that fails without the fix, and the whole enumeration was then run again from the beginning. The table above is that second run. A partial pass with a finding in the middle of it is not evidence about the surfaces it never reached.

The permanent consequence is the part worth reading. The fixture that proves it, a deliberate collision between two organizations sharing a salesperson identifier, now lives in the required test suite rather than in the report. And a new gate refuses any future read of the deal table that narrows on assignment without also narrowing on the organization, so the class is closed rather than the instance.

Every read narrows twice, and confusing the two is how this goes wrong.

The tenant axis asks whose company a record belongs to. The visibility axis asks whether this particular person is allowed to see it. Every engineer is trained to remember the first, which is exactly why the second is where the defect lives.

Keeping them separate also protects the ordinary case. A setter should see their own pipeline and not the whole company's, and that is a visibility decision made inside one tenant rather than a security boundary. Collapsing the two produces a product that is either leaky or unusable, and usually both in different places.

What each axis answersSample data
  • TenantWhose organization does this row belong to. Enforced in the schema, checked by a gate that reads the database definition rather than the code around it, and never satisfied by an identifier that happens to be unique in practice.
  • VisibilityMay this actor see this row, given their role and what is assigned to them. A separate question with a separate answer, so a narrower view for a salesperson can never be the only thing standing between two companies.

Isolation you have to remember is isolation you will eventually forget.

Every table that holds business data carries the organization it belongs to. That is not a convention the team follows, it is a check that fails the build: a new table without that column does not merge. The exceptions are a short published list of infrastructure and shared reference tables, each carrying a written reason in the same file, and nothing is grandfathered in for being old.

A second gate reads the queries themselves, using the TypeScript compiler rather than a search over the text, and holds a ratchet on every read that touches an organization scoped table. Existing debt cannot grow and moving a line to a new file cannot hide it.

The reason for putting this in the build rather than in a document is plain enough. A rule that lives only in the reader's head is followed by whoever happened to read it, and the deal table failure above happened on a codebase where the rule had been written down twice.

Your ad account is connected with your own grant, not a shared one.

Every provider you connect is authorized per organization, through your own authorization flow, producing credentials that belong to your workspace. There is no pooled account through which several customers reach the same provider, which is the arrangement that turns one mistake into everybody's mistake.

To be exact about it, because this is the sort of sentence worth being exact about: the application registered with each provider is ours, as it is for every tool of this kind. The authorization is yours, the token that comes back is yours, and it is stored against your organization and reachable only through it.

Those credentials are stored encrypted with a key generated for that connection alone, itself wrapped by a versioned key held outside the database. Rotating a key is therefore a real operation rather than a promise, and reading the table without the wrapping key yields nothing.

How a connection is heldSample data
  • One grant per workspaceYou authorize the provider yourself. The token that comes back is scoped to your organization and stored against it, and no other workspace reaches your provider through it.
  • Encrypted per connectionAES-256-GCM, with a data key minted for that single connection and stored only in wrapped form. The wrapping key is versioned and lives outside the database.
  • Disconnection is a state, not a deletionA connection that needs reauthorizing says so rather than failing quietly, because a silently dead connection reads exactly like a business with no activity.

The gates run on every change, and the two company fixture is permanent.

These two sentences are different promises and are worth separating, because a vendor who blurs them is telling you less than it sounds.

  • The structural gates carry no condition.Organization scoping in the schema, the page level authorization check, the visibility axis on deal reads, the query level tenant ratchet and identity conformance all run on every proposed change and again on every entry to the merge queue. There is no path to production that skips them, including ours.
  • The two organization suite runs when a change reaches it.The adversarial fixture is a real Postgres test that runs against the suites a change actually touches, rather than on every unrelated commit. It is registered in the suite manifest, so deleting it fails a separate check instead of quietly reducing what is proven.
  • A finding is fixed at the cause.The deal table failure was closed by changing what the reads do and adding a gate that refuses the shape, not by adding the affected files to an exception list. An exception list is a record of the places a rule is not enforced.

We hold no certifications, and we are not going to imply otherwise.

  • No compliance certifications.Signet has not completed SOC 2, ISO 27001 or any comparable audit, and there is no badge on this page. If your procurement process requires one, we are not a fit today and saying so now is cheaper for both of us than saying it in week six.
  • No claim that a system this size is perfect.The pass described above found something on its first run, which is the honest expectation for any adversarial search worth running. The commitment is that we look, we fix at the cause, and we tell you if something affecting your data goes wrong.
  • No claim about surfaces that do not exist yet.Signet is building a connection that lets an assistant read your workspace directly. When it ships, the same two organization fixture runs through it and this page will say so. Until then it says nothing about it.

How your data is handled, who controls what, and who processes it on your instruction is in the privacy policy. What stands behind each number the product shows you is on receipts.

Ask a vendor what their isolation pass found.

It is one chain rather than a set of tools you wire together yourself. You get all of it, and the tiers differ by scale rather than by which capabilities are switched on.