InstallationLifecycle

Connecting, verifying, describing and disconnecting an installation on the platform — the eight methods, the value objects they exchange, and how the Connect dialog, the Verify button and the deep links drive them.

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

The core owns the installation row and its encrypted credentials; the connector owns every call to the platform. Connecting is a two-step conversation (begin(), then complete()) so a paste-a-token connector can finish in one round trip while an OAuth connector can send the creator away and take them back.

Methods

MethodCalled whenReturns
begin(InstallationRequest)The creator submits the install form (dashboard, POST …/installation for first-party clients).InstallationDraft
complete(request, draft, credentials)The draft is complete: at once for a one-step connector, after the return leg for OAuth.InstallationSummary
verify(installation, credentials)The Verify button, POST …/installation/verify, the connector doctor, the scheduled health probe.InstallationHealth
disconnect(installation, credentials)Disconnect (dashboard, DELETE …/installation, MCP), and the first step of an uninstall.void
describe(installation, credentials)The core refreshes the installation's name, handle and picture (after connect, on the doctor).InstallationSummary
startLink(installation, ?payload)A plan's deep link, a handshake link, the portal's button, GET …/distribution/start-link.?string
publicUrl(installation)"Open in …" links, the distribution endpoint's public_url.?string
platformInstallation()Creator handshakes and sign-up on a platform-scope connector, recovery facets that act as Subscriby.?InstallationRef

begin() and complete()

InstallationRequest carries the scope (InstallationScope::Project or ::Platform), the acting creator (CreatorRef, with their locale), the project (?ProjectRef), the install form's answers as fields (name to value, every field the creator submitted so far), existing (the InstallationRef being reconnected, or null for a first connection), returnUrl (the core's URL the platform must send the creator back to after a step taken there) and state (what you parked in the previous draft, when the creator is answering a further round; empty on the first). The same answers arrive in the CredentialBag handed to complete(): the core puts every install field in the bag, not only the secrets.

begin() validates what the creator gave and says what happens next through an InstallationDraft:

  • state: anything you need to carry into complete(), or into the next begin() (a verified bot id, the platform's name for the installation, which round you are on).
  • fields: more fields to ask for, when the first answers were not enough. The dashboard grows the form by them and calls begin() again with every answer so far in fields and your state in the request; keep answering with fields until the draft is complete.
  • continueUrl: where to send the creator for the platform's own step (an OAuth authorisation page). Build it with $request->returnUrl as the redirect target. Null means the draft is complete.

The core follows the draft. For a complete draft it calls complete() in the same request with the bag, records the installation row from the summary and stores the bag encrypted on it. For a draft with fields it parks the draft on the pending installation, asks the creator, and calls begin() again with the merged answers and your state. For a draft with a continueUrl it parks the draft with a one-time token (a quarter of an hour), sends the creator to the URL and waits; when the platform brings them back to returnUrl, the core calls complete() with the parked request, the draft carrying returned (the return leg's query string, code and state for OAuth) and a bag of the answers so far. Exchange the code there and put the credential you obtain in the summary's meta, which is how it reaches the stored bag (below). A refusal on the return leg (InstallationRefused::byPlatform() for returned['error']) is shown to the creator on the Connectors tab, and a token that is reused or has expired is refused by the core before you are called. InstallationDraft::withReturned() is how the core hands you the return leg; you never build it yourself.

Proving the round trip without a platform

The fake connector's FakeInstallationLifecycle drives every path: a token starting steps- asks one more field (region) before completing, a token starting oauth- returns a continueUrl on the fake host and completes from returned['code'], minting oauth-<code> into meta, and returned['error'] becomes a platform refusal. The core's own tests connect through all three.

complete() does the platform work (register the webhook, set the command menu, fetch the bot's profile) and answers an InstallationSummary: externalId (the platform's id for the installation, unique per connector), displayName, handle, avatarUrl, meta and storageRef, your own row's id when you keep one. The core records the installation row from the summary through Core\Installations::record(), so you never write the row yourself. The bag it stores is the one it handed you merged with meta (InstallationSummary::credentialsFor()): a secret you mint in complete(), a webhook signing secret say, goes into meta and comes back in every CredentialBag your ports receive, and Core\Installations::credentials() reads it before any port is called, which is how an inbound gateway verifies a signature. Scalars are stored as strings and anything else as JSON; a minted key wins over a field of the same name.

Throw Subscriby\Connector\Exceptions\InstallationRefused when the platform refuses the credentials or another installation already holds them; the core shows its reason to the creator on the form.

verify()

Ask the platform whether the installation still works and answer an InstallationHealth: state (InstallationState: Connected, Degraded, Revoked, Disconnected, Pending), your own reason code, a detail sentence, creatorActionable (can the creator fix it, or is it the platform?) and the failureKind when a call failed. The core writes the verdict to the row, emits connector.status_changed when it is a transition, and dispatches InstallationHealthRecorded to your package listeners.

Use the same platform call here as in RecoverySupport::probeInstallation(), so the two never disagree.

disconnect()

Withdraw the installation from the platform, best effort: unregister the webhook, revoke what can be revoked. The platform may already have revoked the credentials, so a refusal here is logged, not thrown. The core wipes the credentials from the row and sets the state to Disconnected whatever you return; your own tables are untouched, and a reconnect names the same installation through InstallationRequest::$existing.

startLink($installation, $payload) builds the link that starts a conversation with the installation on the platform, carrying a payload: a plan id for a storefront link, a handshake token for a sign-in, null for the home screen. Telegram answers https://t.me/<bot>?start=<payload>. Return null when the platform has no such thing; the core then falls back to the portal.

publicUrl($installation) is where a person finds the installation with nothing to open on: the bot's public page, a server's invite page. Distinct on purpose, because a creator pastes it into a bio and the dashboard shows it as "Open in …".

platformInstallation()

For a connector whose manifest lists the platform scope, the installation Subscriby itself runs on the platform: the bot creators sign in through, are asked in and receive alerts from. The core hands it to startLink() for a creator's handshake links, to RegistersCreators::signupEntry() and to the recovery facets that need the platform's own installation to act. Return null for a connector installed per project only, or while none is configured.

How Telegram does it

A bot connects in one step from its token: getMe says who the bot is, the webhook and the command menu are registered, and the row the legacy schema keeps for the bot is written so everything that still reads it keeps working during the dual-write window. A reconnect names the existing installation and re-points its row at the new token, so the project's members stay bound to the same bot. verify() and the recovery probe share getMe. startLink() is the ?start= deep link; publicUrl() the bot's t.me page; platformInstallation() is Subscriby's platform bot.

What the kit checks

ports.required_bound fails a connector that does not bind the port. No behavioural rule runs against it, because every method needs a live platform; Subscriby's suite exercises the Telegram implementation against a fake Bot API, and yours should do the same against your platform's client.

One external id per installation

InstallationSummary::$externalId is what Core\Installations::findByExternalId() and the registry's uniqueness rely on. Return the platform's stable id for the installation (the bot's numeric id, the app's client id), never the token and never a display name.

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