Getting Started

The layout of a connector package, the service provider that wires it in, and the ports every connector binds.

A connector is an ordinary Composer package that depends on subscriby/connector-sdk. Three things make it a connector: a connector.json at its root, a service provider that extends the SDK's, and a Connector class that binds the ports the package implements.

Start from an empty Composer package and pull the SDK in:

composer require subscriby/connector-sdk

The SDK is published from github.com/envigoinnovations/subscriby-connector-sdk, a read-only mirror of Subscriby's monorepo; its CHANGELOG.md lists what each version added.

Package layout

my-connector/
├── connector.json            the manifest — what the connector is and can do
├── composer.json             requires subscriby/connector-sdk, declares the provider
├── config/                   optional: connector-<key>.php and any client config
├── database/migrations/      optional: the connector's own tables, prefixed <key>_
├── lang/                     optional: <locale>.json translations for the connector's strings
├── resources/views/          optional: slot views, published under connector-<key>::
├── routes/
│   ├── inbound.php           optional: the platform's webhook routes
│   └── web.php               optional: browser routes (a sign-in callback, a health probe)
├── src/
│   ├── MyConnectorServiceProvider.php
│   ├── MyConnector.php
│   └── Ports/…               one class per port
└── tests/

The provider file must sit at src/<Provider>.php: the SDK finds the package root as its grandparent, which is how it locates the manifest and everything else.

composer.json

{
  "name": "acme/subscriby-connector-example",
  "require": {
    "php": "^8.4",
    "illuminate/contracts": "^13.0",
    "illuminate/support": "^13.0",
    "subscriby/connector-sdk": "^1.0"
  },
  "autoload": {
    "psr-4": { "Acme\\Connectors\\Example\\": "src/" }
  },
  "extra": {
    "laravel": {
      "providers": ["Acme\\Connectors\\Example\\ExampleConnectorServiceProvider"]
    }
  }
}

The sdk constraint inside connector.json ("sdk": "^1.0") is checked as well: the registry refuses a package built against an SDK the application does not run.

The service provider

Extend Subscriby\Connector\ConnectorServiceProvider and return your connector from connector(). The base class does the rest when the application boots:

  • reads and validates connector.json, listing every problem at once with its dotted path;
  • registers the connector with the application's registry under that manifest;
  • loads database/migrations, resources/views (as the connector-<key> namespace), lang/*.json, routes/inbound.php (behind the core's connector.inbound middleware, which authenticates each call through your InboundGateway, drops a replayed event and refuses a paused connector), routes/web.php (behind web) and the console commands you return from packageCommands();
  • binds the listeners you return from packageListeners() to the SDK's events (Subscriby\Connector\Events\*);
  • hands the seeders you return from packageSeeders() to the registry, and the application's database seeder runs them after its currencies and countries and before its demo projects, so a connector ships its own demo rows (its bots, its chats, its native currency);
  • merges every config/*.php the package ships under the file's own name, so config/connector-example.php becomes config('connector-example.…').
<?php

declare(strict_types=1);

namespace Acme\Connectors\Example;

use Subscriby\Connector\ConnectorServiceProvider;
use Subscriby\Connector\Contracts\Connector;

final class ExampleConnectorServiceProvider extends ConnectorServiceProvider
{
    protected function connector(): Connector
    {
        return $this->app->make(ExampleConnector::class);
    }
}

The Connector class

Subscriby\Connector\Contracts\Connector has one method. It receives a ConnectorRegistrar and binds each port the package implements by its interface name:

final class ExampleConnector implements Connector
{
    public function __construct(
        private readonly Ports\ExampleInstallationLifecycle $lifecycle,
        private readonly Ports\ExampleIdentityResolver $identities,
        // …
    ) {}

    public function register(ConnectorRegistrar $registrar): void
    {
        $registrar->port(InstallationLifecycle::class, $this->lifecycle);
        $registrar->port(IdentityResolver::class, $this->identities);
        $registrar->port(InboundGateway::class, $this->gateway);
        $registrar->port(FailureClassifier::class, $this->failures);
        $registrar->port(TextRenderer::class, $this->renderer);
        $registrar->port(SettingsSchema::class, $this->settings);
        $registrar->port(UiSlots::class, $this->slots);
        $registrar->port(Messenger::class, $this->messenger);
        $registrar->port(AccessController::class, $this->access);
    }
}

What the connector is never lives in this class: the manifest describes it, and the conformance kit fails a package whose registered manifest differs from its file.

Required and optional ports

Every connector binds these seven, whatever the platform:

PortWhy it is required
InstallationLifecycleConnecting, verifying, disconnecting and describing an installation.
IdentityResolverWho an inbound event is from; what the platform knows about an account.
InboundGatewayAuthenticating and decoding what the platform sends.
FailureClassifierReading why the platform refused a call, in the core's failure vocabulary.
TextRendererTurning the core's canonical HTML into what the platform accepts.
SettingsSchemaThe install and settings forms as data (usually bound for you from the manifest's fields).
UiSlotsThe connector's UI contributions, even when it fills none.

Every other port is bound when the manifest declares the matching capability, and the registry refuses a manifest that declares a capability without its port or binds a port without its capability. Ports has one page per port.

SettingsSchema comes from the manifest

A connector whose install and settings forms are plain fields declares them in connector.json (install.fields, install.settings_fields) and binds nothing: the core wires the SDK's ManifestSettingsSchema over those fields, translated through the package's language files. Write your own SettingsSchema only when the fields depend on the installation.

Reading and writing the core's rows

A connector never queries the application's database. It records and reads installations, identities, spaces, grants and support messages through the Core API, the contracts under Subscriby\Connector\Core\* that the core binds in its container and hands to any class your package builds. Everything they take and return is an SDK value object; anything a connector keeps of its own (a bot row, a token, a cached chat title) goes into its own tables under the data rules, with a storage_ref string on the core row pointing at it. Core API has one page per contract.

The worked example

The SDK ships a complete connector to copy from: the fake connector under Subscriby\Connector\Testing, with its own connector.json beside it. It deliberately violates every assumption a Telegram-shaped core would make (280-character messages, no files, membership grants instead of invite links, a resource kind the creator fulfils by hand, no early admission, three admin commands), so a core path that still assumes one platform fails against it. Its port fakes keep what they were asked in memory and offer assertions, which makes them the doubles to write your own connector's tests against too.

Next

Write the manifest and validate it against the published schema.
Implement the seven required ports, then the ones your capabilities need (Ports).
Keep your migrations inside the data rules.
Run the conformance kit until it is green, with the fakes covering the core paths your connector relies on.
Fill in the listing and publish: version, translate, submit.

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