IdentityResolver

Who an inbound event is from, what the platform knows about an account and whether an installation can reach it — resolving, describing, adopting and reaching identities, and the line between what the connector says and what the core decides.

Subscriby\Connector\Contracts\Ports\IdentityResolver. Required of every connector.

The core keeps the identity rows and decides which creator or member an account belongs to. The connector only says which account the platform is talking about, keeps whatever row of its own it needs to talk to that account later, and says whether it can talk to it now.

Methods

MethodCalled whenReturns
resolveInbound(InboundEnvelope)The kit's identity.tolerates_empty_envelope; your own inbound handler, to find the actor of an event before it consults the core.?IdentitySummary
describe(installation, credentials, externalId)Reserved: the core does not call it yet. Your own connector uses it where a fresh name or picture matters more than a cached one.IdentitySummary
adopt(installation, IdentitySummary)The core learns of an account outside an inbound event: a sign-in widget vouched for it, a handshake completed with it.IdentityRecord
deliveryTarget(installation, credentials, IdentityRef)Before the core sends to a member who holds accounts on several connectors, to pass over one the installation cannot reach; the kit's identity.answers_reachability.?Recipient

resolveInbound()

Given a decoded event, answer the account that acted as an IdentitySummary (externalId, displayName, username, avatarUrl, meta), or null for an event with no actor (a channel post, a system notice). Never throw on an event you do not recognise: the kit hands you an envelope with an empty payload and expects null or a summary.

The core does not call this method itself yet; the connector's inbound handler does, and then asks the Core API who holds the account (Identities::findByExternalId(), findCreator(), findMember()). The contract is on the port so the day the core dispatches envelopes itself, nothing in your connector changes.

describe()

Ask the platform about an account by its external id and answer what it says now. The contract exists so the core can refresh a name or a picture without knowing the platform's call; today no core surface invokes it, so implement it and use it from your own connector where a fresh summary matters.

adopt()

A sign-in widget hands the core an account the connector has never seen an update from; the core calls adopt() with the platform installation so the account can be written to later. Keep your own row for it under the installation given (Telegram: the private chat row under the platform bot) and answer the IdentityRecord the core files: connector, externalId, installationId (null for a platform-wide id), displayName, username, avatarUrl, storageRef naming your row, meta, lastSeenAt. Calling adopt() twice for the same account returns the same row.

deliveryTarget()

Whether the installation can write to the account right now, and how to address it. Reachability is the platform's to know: Telegram writes only to a person who has opened the bot in question, Discord only to a member whose direct messages are open, a forum to anyone with an inbox. The core asks before it sends a member anything, so an account it cannot reach is passed over for one it can (the member's next connected account, then email) rather than attempted and failed. Answer a Recipient for the account (with a threadId when the platform needs one) or null when the installation cannot reach it. A connector that cannot tell in advance answers a Recipient and lets the send report the account as unreachable through its FailureClassifier.

Platform-wide or per installation?

An IdentityRecord carries an installationId when the platform gives an account a different id per installation (a per-app user id) and null when one id names the person everywhere. The core's uniqueness is (connector, installationId, externalId), so get this right on day one: Telegram's user id is the same whichever bot the person talks to, so its records carry no installation and the core files them as platform-wide accounts.

How Telegram does it

A Telegram user id is the same for every bot, so the summary carries no installation; a channel post has no actor and resolves to null; an account the sign-in widget vouched for is adopted as a private chat row under the bot given, because that row is what every later message to the person is written against. Reachability is that row: a bot can write to a person only where a private chat row exists under that very bot, because Telegram refuses a bot the person never opened, so deliveryTarget() answers null when the installation's bot holds no row for the account.

What the kit checks

identity.tolerates_empty_envelope: resolveInbound() given an InboundEnvelope with the connector's key, no installation, kind unknown and an empty payload returns null or an IdentitySummary, nothing else, and does not throw.

identity.answers_reachability: deliveryTarget() given an installation and an account the connector has never seen returns null or a Recipient, nothing else, and does not throw.

Identity is not membership

Resolving an account says nothing about whether the person may be in a place. That is the access ledger's business, read through Core\Grants and asserted through your AccessController. Keep the resolver free of authorisation.

How is this guide?

On this page

Subscriby is a product designed by you — for you.

No boardroom full of executives deciding what we ships next. Our roadmap always shaped by you with your feedback.

Share feedback or a request