2. The manifest
Write connector.json for Agora — every block with the decision behind it, and a first test that validates the file exactly as the application will at boot.
Everything Agora is goes into connector.json. Here is the whole file; the sections below say why each block reads as it does.
{
"$schema": "https://docs.subscriby.net/connectors/connector.schema.json",
"key": "agora",
"name": "Agora",
"version": "0.1.0",
"sdk": "^1.0",
"vendor": "Acme",
"install": {
"mode": "paste_credential",
"scopes": ["project"],
"fields": [
{
"name": "intro",
"type": "instructions",
"label": "Create an API key on your Agora forum, then paste it here.",
"steps": [
"Sign in to your forum as an administrator and open Settings › API.",
"Create a key with the Boards and Messages permissions.",
"Copy the key, paste it below with your forum's address, then click \":button\" in :app."
],
"help": "The key acts as a bot user on your forum. Members will receive messages from it and be added to boards by it."
},
{
"name": "forum_url",
"type": "text",
"label": "Forum address",
"help": "The address members open, such as https://forum.example.com.",
"required": true,
"rules": ["string", "url", "starts_with:https://"]
},
{
"name": "api_key",
"type": "secret",
"label": "API key",
"help": "Keep it secret: anyone who has it can post as your forum's bot.",
"required": true,
"rules": ["string", "regex:/^agk_[A-Za-z0-9]{32}$/"]
}
],
"settings_fields": []
},
"resource_kinds": [
{
"kind": "board",
"label": "Board",
"portal_label": "Private board",
"icon": "rectangle-stack",
"grant_mode": "membership",
"supports_early_admission_hold": false,
"mirrorable": false
}
],
"capabilities": ["messaging", "access_control", "portal_login", "recovery_probes"],
"messaging": {
"max_length": 10000,
"buttons_per_row": 1,
"max_buttons": 3,
"callback_data_bytes": 64,
"supports_underline": true,
"supports_spoiler": false,
"supports_files": false
},
"pacing": {
"min_interval_microseconds": 1000000,
"burst": 5,
"per_recipient_interval_microseconds": 2000000
},
"management_commands": [],
"relay_modes": [],
"recovery": { "probes": true },
"listing": {
"category": "community",
"tagline": "Sell access to private boards on your Agora forum, joined and left automatically.",
"overview": "Connect your Agora forum with an API key and Subscriby runs your membership on it: members buy on the portal, are added to your private boards the moment they pay, and are removed when their access ends. Every confirmation and reminder arrives as a private message from your forum's bot user.\n\nMembers sign in to the portal with their forum account, and Subscriby's health checks tell you when the forum, a board or the bot user stops answering.",
"screenshots": [],
"links": {
"documentation": "https://docs.acme.test/subscriby-agora",
"support": "mailto:[email protected]",
"privacy": "https://agora.example/privacy",
"terms": "https://agora.example/terms",
"homepage": "https://agora.example"
},
"added_at": "2026-10-01",
"changelog_url": "https://github.com/acme/subscriby-connector-agora/blob/main/CHANGELOG.md",
"sign_in_required": false,
"portal_cta": { "label": "Open the forum" },
"marketing": {
"audience": "forum communities",
"place": "board",
"places": "private boards",
"installation": "forum bot",
"identity": "forum account"
}
}
}The decisions
Identity. version starts at 0.1.0 because nothing works yet; it reaches 1.0.0 when the declared capabilities do. sdk is ^1.0. vendor is our name; the Official badge is not ours to claim.
install. Agora hands out API keys, so the mode is paste_credential. Only the project scope: every creator runs their own forum, and there is no shared Agora for Subscriby to own. Three fields: an instructions walkthrough with :app and :button placeholders, a text field for the forum address with a url rule, and a secret for the key with a regex in Agora's key format. The kit's settings.install_fields wants a paste-a-credential connector to declare a secret, and we do. No settings fields yet.
resource_kinds. One kind, board. Its grant_mode is membership: Agora can add a user to a board directly, so there is nothing for a member to tap and no invite link to mint. Members will read "Private board" on the portal. No early admission (Agora has no waiting room) and no mirroring (a board is a conversation, not a feed).
capabilities. messaging because every core message reaches a member as a private message; access_control because we gate boards; portal_login because a member who bought through the forum has no password; recovery_probes because Agora lets us ask whether the forum, a board and an account still answer. Not broadcasts: at sixty requests a minute a broadcast to two thousand members would take half an hour, and we would rather the core told the creator "not on this connector" than queue it. Not management_surface: a forum has no conversation to render an admin wizard in. Each declared capability obliges a port; chapters 5 to 7 bind them.
messaging. Agora private messages are long (10 000 characters of BBCode) and have no buttons. buttons_per_row: 1 and max_buttons: 3 tell the core it may still attach up to three actions; chapter 5 turns them into links and reply keywords. supports_files: false makes the core refuse sendFile() itself.
pacing. Sixty requests a minute is one a second: min_interval_microseconds: 1000000, a burst of five, and two seconds between two messages to the same member. The core registers a connector:agora limiter at one send a second from this.
relay_modes, management_commands, recovery. Empty, empty, and probes only. The recovery block and the recovery_probes capability say the same thing in the two places the core reads them.
listing. Category community. The tagline is seventy-nine characters. The privacy and terms links are Agora's, not ours. sign_in_required: false because a creator pastes a key rather than signing in. The portal button reads "Open the forum" and opens startLink(). The marketing words are lower case and read mid-sentence: "your board", "a forum bot".
The first test
The manifest is data, so the first test in the package loads it exactly as the application will:
<?php
declare(strict_types=1);
use Subscriby\Connector\Enums\Capability;
use Subscriby\Connector\Manifest\ManifestFile;
it('declares a manifest the SDK accepts', function (): void {
$manifest = ManifestFile::load(__DIR__.'/../connector.json');
expect($manifest->key)->toBe('agora')
->and($manifest->capabilities)->toContain(Capability::AccessControl)
->and($manifest->resourceKinds)->toHaveCount(1)
->and(mb_strlen($manifest->listing->tagline))->toBeLessThanOrEqual(80);
});ManifestFile::load() throws InvalidManifest listing every problem with its dotted path, so a broken file fails this test with the same message the application would boot with.
Keep the schema on
Point your editor at the $schema URL. The eighty-character tagline and a few other limits are the schema's alone; the PHP loader checks structure, types, patterns and unknown keys.
How is this guide?
1. The package
Start the Agora connector — composer.json, the service provider, a Connector class that binds nothing yet, configuration, and the HTTP client every port will share.
3. Installation
Implement InstallationLifecycle for Agora — validate the key with /me, register the webhook, keep the forum row, verify, disconnect, and build the links a member opens.