Installations
The installation rows as a connector may read and write them — finding by id, platform id or storage ref, listing a project's or the platform's, recording one, recording a health verdict, forgetting one.
Subscriby\Connector\Core\Installations. An installation is a connector set up on a project (the project's bot, the project's server) or, for a platform-scope connector, Subscriby's own shared presence on the platform. The core owns the row, its encrypted credentials, its state and its health; a connector reads it here and writes what it learns.
Reads
| Method | Returns | Null when |
|---|---|---|
find(string $id) | ?InstallationRef | No installation has that UUID. |
findByExternalId(string $connector, string $externalId) | ?InstallationRef | The platform id is unknown to the core. |
findByStorageRef(string $connector, string $storageRef) | ?InstallationRef | Nothing points at that row of yours. |
listForProject(ProjectRef $project, ?string $connector = null) | list<InstallationRef> | Never null; live and standby, one connector's or every connector's. |
listPlatform(string $connector) | list<InstallationRef> | Never null; the connector's platform-scope installations. |
credentials(InstallationRef $installation) | CredentialBag | Throws ResourceNotFoundException for an unknown ref; empty for a disconnected installation. |
findByStorageRef() is the read your inbound handler makes most: a webhook arrives for a bot, your table knows the bot's row, and the ref tells you which core installation (and so which project) it belongs to.
credentials()
$bag = $installations->credentials($installation);
$secret = $bag->get('webhook_secret');The stored bag of an installation the connector can already name: what the creator pasted into the install form plus what the connector put in its InstallationSummary::$meta when it connected (InstallationSummary::credentialsFor() merges the two before the row is written). It exists for the one moment a connector needs a secret before the core has handed it anything: an inbound gateway verifying a signature. It widens nothing a port call would not have given, because every port receives the same bag for the same installation.
record()
$ref = $installations->record(new InstallationRecord(
connector: 'example',
scope: InstallationScope::Project,
externalId: $bot->id,
displayName: $bot->name,
projectId: $project->id,
role: InstallationRole::Live, // or Standby
state: InstallationState::Connected,
stateReason: null,
handle: $bot->username,
avatarUrl: $bot->avatarUrl,
storageRef: (string) $row->id,
credentials: null, // only when the connector holds secrets the core should store
settings: [],
connectedAt: now(),
verifiedAt: now(),
));Idempotent on the connector key and the platform id: a second record of the same bot or app updates the row rather than adding one. When a connector's InstallationLifecycle::complete() answers, the core records the installation from the summary itself, so a connector calls record() directly only when it learns of an installation outside that flow.
recordState()
$installations->recordState($ref, InstallationState::Revoked, reason: 'token_revoked');Your health probes call this instead of re-describing the whole installation, so a probe that only learnt "the token was revoked" writes exactly that. reason is your own code; the core shows a generic sentence for it and prefers the words your RecoverySupport::healthReasonText() gives.
forget()
Drop an installation whose row on the connector's side is gone. It exists for the first-party connector, whose own tables led the neutral rows for a while; disconnect and uninstall keep the row, so once the neutral rows lead this call has no caller. A connector built on the SDK from day one never calls it.
What the ref carries
InstallationRef: id, connector, scope, projectId, externalId, storageRef, handle. Enough to act on the platform (with the CredentialBag the core hands your port) and to find your own row. Credentials, settings and health are never on the ref: the core hands credentials to ports per call (or through credentials() when a gateway asks first), and health is written, not read, from a connector.
One row per bot, whatever happens to it
Disconnect wipes credentials and sets Disconnected; uninstall stamps the row and keeps it; a reconnect names the same row through InstallationRequest::$existing. The platform id is the identity of the row for its whole life, which is why record() keys on it.
How is this guide?
Core API
The contracts a connector calls into the core — how they are bound, the records and refs that cross them, the idempotency every write keeps, the exceptions, and what the nine contracts cover.
Identities
The identity rows and who holds them — finding an account, recording it, linking it to a creator or a member, the handshake that proves possession, and the two lookups that turn an account into a person.