Version

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

MethodReturnsNull when
find(string $id)?InstallationRefNo installation has that UUID.
findByExternalId(string $connector, string $externalId)?InstallationRefThe platform id is unknown to the core.
findByStorageRef(string $connector, string $storageRef)?InstallationRefNothing 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)CredentialBagThrows ResourceNotFoundException for an unknown ref; empty for a disconnected installation.
settings(InstallationRef $installation)array<string, mixed>Never null; the creator's settings by declared name, empty before any were saved and for a ref the core does not know.

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.

settings()

$settings = $installations->settings($installation);
$welcome = (bool) ($settings['welcome_dm'] ?? true);

The values of the fields your SettingsSchema declares, by name, as the core stores them: written by the core's own settings action from the dashboard, PATCH …/installation/settings and the apps, after the rules your fields carry have run. A field the creator never saved is absent, so apply your own default. The core also tells your package when they change: after every save it dispatches Subscriby\Connector\Events\InstallationSettingsChanged (the ref, the names of the fields that changed, every setting as now stored, and when) to the listeners your packageListeners() returns, so a connector that must act on a choice at once (the forum channel a creator picked becomes the relay space) never polls.

updateSettings()

$installations->updateSettings($installation, ['announcement_channel' => $channel['id']]);

The one settings write a connector may make: some of the fields your SettingsSchema declares, for a value the connector learnt on the platform (the channel an installation action just created) that the creator would otherwise pick by hand. It runs through the same action the dashboard's settings dialog does, for whoever is acting, so a field you never declared and a value that fails its rule are refused as InstallationRefused (invalid) exactly as a creator's would be, a platform installation has no settings to write (project_unknown), and nobody acting who may change the project's connectors is not_permitted. The fields you do not name keep their values, the answer is every setting as now stored, and InstallationSettingsChanged is dispatched afterwards as for any save.

updateSettingsAs()

$installations->updateSettingsAs($creator, $installation, ['support_forum_channel' => $channelId]);

The same write for a creator surface with no actor bound (a gateway job answering a slash command), run as the creator the CreatorRef names, so the project's permission to change its connectors is the core's check: a teammate without it is not_permitted, a project the creator may not manage is project_unknown, and a ref that names nobody is creator_unknown.

runAction()

$result = $installations->runAction($creator, $installation, 'setup_members_area', ['name' => 'Members']);

One of your own installation actions, run as the creator the CreatorRef names from your creator surface: the same path the dashboard's Connectors tab, the REST API and the apps take, so the project's permission to change its connectors, the fields' rules and the installation's state are checked by the core before your port hears of it. The answer is the port's InstallationActionResult; the refusals are InstallationRefused with creator_unknown, installation_unknown, project_unknown, not_connected, action_unknown, invalid or not_permitted.

detailsForProject()

foreach ($installations->detailsForProject($creator, $project) as $installation) {
    // $installation->connectorName, ->displayName, ->state, ->stateReason, ->connectedAt, ->healthCheckedAt
}

A project's installations as the creator reads them, every connector's and live or standby alike, each an InstallationDetails with the connector named as the directory names it, what the installation is called on its platform, how the core last found it and why, when it connected and when the doctor last looked. Run as the creator the CreatorRef names, so a project they may not open is project_unknown.

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?

Last updated on

Version

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

For AI agents: llms.txt · llms-full.txt