Lua/Honeycomb

Identities & Approvals

How Honeycomb decides which real person a handle belongs to — why email is the only identity that grants access, how usernames link automatically, and the 'is this email you?' approval that stops a namesake from inheriting someone's private memory.

Access in Honeycomb is granted to people, not to raw handles. A single human shows up as many identifiers — an email address, a Slack ID, a GitHub login, a display name — and Honeycomb resolves those to one canonical person so a grant made to [email protected] also works when Priya appears by her Slack handle. This page explains how that resolution stays safe: which handles can confer access, which only improve graph quality, and the confirmation step that keeps one person's private Spaces from ever landing in a namesake's view.

At a glance

  • Email is the only identity that grants access. A person's read view is resolved from a verified email. Usernames, Slack IDs, and display names help build the knowledge graph but never, on their own, unlock anyone's private Spaces.
  • Usernames link automatically; emails need proof. Linking scotty595 to "Scott" is low-risk — it can't grant access — so it happens automatically. Attaching a new email to an existing person is access-bearing, so it isn't done silently.
  • A namesake can't inherit your memory. When ingest wants to attach an email to a pre-existing person by a weak signal (a matching name), Honeycomb does not link it. It records a proposal that only the real owner of that identity can confirm.
  • Approvals go to the owner, not the claimant. The "is this email you?" prompt appears for the already-verified person whose identity is the target — so someone inviting a look-alike can't approve their own link.
  • One address, one owner. An email can be the access-bearing identity for at most one person, and only the first confirmation wins.

Why identity is an access-control decision

Every grant — every share, every membership, every private thread — is keyed to a canonical person, not to a handle. When someone reads, Honeycomb resolves their verified email to that canonical person and returns exactly the Spaces that person can see (Spaces & permissions).

That makes identity resolution part of the security boundary. If the wrong handle resolved to a person who holds grants, the holder of that handle would inherit those grants' access. So Honeycomb draws a hard line by handle type:

HandleExampleLinks people in the graph?Can unlock private Spaces?
Email[email protected]YesYes — the access-bearing identity
Login / usernamescotty595, a GitHub loginYesNo
Slack / platform IDU04F…YesNo
Display name"Scott"Yes (for graph quality)No

Usernames, IDs, and names are used freely to stitch the graph together — to merge five fragments of the same person into one node — because getting one wrong only affects graph tidiness, never access. Email is different: an email is what a read is resolved from, so attaching one is the moment access can change hands.

How an email becomes an access-bearing identity

An email is registered as a person's access-bearing identity only when it genuinely is their identity — never as a side effect of a name match:

  • A person is created from that email — the person is the address (a brand-new contact who emailed in). No ambiguity, no approval needed.
  • The same address already belongs to that person, seen again from another source. Re-attesting a known email is safe.
  • The person's owner explicitly confirms it — the approval flow below.

Any other path — a new email that lines up with an existing person only because a name or a look-alike local-part matched — is treated as unproven. Honeycomb records it as a pending proposal and grants no access until it's confirmed.

Note: This is why merging a username to a person is instant but attaching a second email isn't. The username can't grant access; the email can.

The "is this email you?" approval

When ingest encounters an email that would attach to a pre-existing person by a weak signal, it creates a pending identity link instead of wiring it up. A pending link is inert: it confers no access, appears in no one else's view, and never resolves a read — it's just a question waiting for an answer.

The prompt — "Is [email protected] you?" — is shown to the person who already owns the target identity, in their Lua inbox. That direction is the whole safeguard: an attacker who invites a look-alike account can't see or approve the link, because they aren't the owner of the identity it targets.

  • Confirm promotes the email to that person's access-bearing identity. From then on it resolves their view like any verified address.
  • Reject tombstones the proposal. The same email→person link is never raised again by ingestion, so a rejected suggestion won't nag on every re-sync.

Guarantees

  • Owner-only. A proposal is visible and actionable only by the person whose identity it targets. No one else — not an admin acting broadly, not the account that introduced the email — can approve it.
  • First confirmation wins. If two people could plausibly claim the same address, the first to confirm owns it; a later attempt to move it is refused, so an address can never be re-homed off the person who confirmed it.
  • Atomic. Confirmation promotes the email and closes the proposal in one step — there's no in-between state where an email is live but the request still looks open.

API

Identity approvals are surfaced in the Lua desktop inbox, and are also available directly. All three are scoped to the acting user — the engine only ever returns or acts on proposals whose target identity is the caller:

EndpointMethodWhat it does
/api/v1/identity/pending-linksGETList email→identity proposals awaiting my confirmation
/api/v1/identity/pending-links/:id/confirmPOSTConfirm — the email becomes my access-bearing identity
/api/v1/identity/pending-links/:id/rejectPOSTReject — tombstone the proposal so ingest never re-raises it
curl -s "$HONEYCOMB_URL/api/v1/identity/pending-links" \
  -H "Authorization: Bearer hck_..." \
  -H "x-acting-user: [email protected]"
{
  "links": [
    {
      "id": "pil_7c21",
      "email": "[email protected]",
      "proposedPersonName": "Stefan Kruger",
      "via": "display_name",
      "source": "gmail",
      "createdAt": "2026-07-10T14:00:00.000Z"
    }
  ]
}

Acting as a verified user

Reads are scoped to the acting user's identity (see acting on behalf of users). Honeycomb accepts that identity two ways, in order of trust:

  1. A verified assertion. Platforms integrated with Lua pass a short-lived, cryptographically signed identity assertion. It's verified against the issuer's keys on every request and always takes precedence — it can't be forged or replayed.
  2. The x-acting-user header, honored only on a key that carries the act-as-user capability. It's the migration path; the verified assertion is the destination.

Workspaces can require the verified path: once assertions are confirmed flowing, a workspace can be switched so the plain header is ignored for it entirely, leaving only the cryptographic identity to resolve a read. There's no downgrade to the spoofable header. Until a workspace opts in, both paths work and a divergence between them is logged as a signal to watch.

Note: With no resolvable acting user — no assertion, and no header on a capable key — a read returns org-wide content only. It never falls back to full-workspace visibility.

Where to next