Write a provider plugin
A provider plugin implements one social network. It cannot add commands, change the CLI, or alter how Core stores state: the plugin contract is the whole extension surface, and the five official providers use exactly the same contract as a third-party one.
Package shape
A provider is an ESM package whose root default export is the plugin object. Declare it with defineProvider from @syndroo/provider-sdk and export the result:
import { defineProvider } from "@syndroo/provider-sdk";
export default defineProvider({
manifest: {
id: "example",
name: "Example",
version: "0.7.0-rc.1",
apiVersion: 1,
declaredCapabilities: ["text"],
egress: { fixedOrigins: ["https://api.example.com"] },
schemas: {
connectOptions: { type: "object", additionalProperties: false, properties: {} },
credentialInput: {
type: "object",
additionalProperties: false,
required: ["token"],
properties: { token: { type: "string", minLength: 1 } }
},
content: {
type: "object",
additionalProperties: false,
properties: { text: { type: "string" } }
},
publishOptions: { type: "object", additionalProperties: false, properties: {} }
}
},
connect: async (input, context) => {
return { status: "done", identity: { /* account */ }, credentials: { /* stored */ } };
},
freeze: (input) => {
return { payload: { text: input.content.text } };
},
publish: async (input, context) => {
return { status: "succeeded" };
}
});The default export is the plugin object itself, not a factory. One package declares one provider with a single defineProvider call; the id is also the registry key and the package-name suffix.
The manifest
| Field | Rule |
|---|---|
id | Lowercase provider id, at most 64 characters, matching the package suffix. |
name | Display name only, at most 128 characters. |
version | Must equal the package's own version. |
apiVersion | The integer 1. A mismatch is refused instead of guessed at. |
declaredCapabilities | A list drawn from text and article, with no repeats and at most eight entries. |
schemas | The four schemas connectOptions, credentialInput, content and publishOptions. Each is JSON Schema, at most 64 KiB and at most 32 levels deep, with local references only. |
egress | The outbound origins the plugin may use; see below. |
The declaration is bounded before anything reads it: the validator inspects own data properties, never runs a getter, never compiles or evaluates a schema, and never imports, touches the filesystem, reads the clock or opens a socket while the module loads.
The egress declaration
egress.fixedOrigins is required and lists the canonical origins the provider intends to reach. The runtime builds the outbound policy from this list, so a request to an origin the plugin did not declare is refused before it leaves the process.
- An origin is
https://plus a host: no port, no path, no query, no user info. - An IP literal or a host without a dot is refused.
- The list holds at most 20 entries and duplicates are removed.
- A provider with no fixed origin (a federated network, where the user names the host) declares an empty list together with
federated: true.
The official declarations are the worked examples: https://bsky.social for Bluesky, https://dev.to for DEV.to, the linkedin.com pair for LinkedIn, the threads.net pair for Threads, and an empty federated list for Mastodon.
The three operations
connect runs one step of a connection. It either finishes with an identity and the credentials to store, or returns an action for the caller: ask for credential fields, open_url for browser authorization, or wait_for_callback. A plugin never drives user interface and never decides how a session is stored.
freeze is a pure function of the frozen content, the target options and what the same run already learned. It compiles the provider-native payload, and it must be deterministic: the same input produces byte-identical output. No secret may appear in a frozen payload, a preview, a write outcome or a thrown error.
publish sends the frozen payload through the injected transport and reports one of three outcomes: succeeded, failed with disposition not_applied, or unknown. A plugin never retries on its own and never reclassifies an earlier unknown write.
Testing your plugin
@syndroo/provider-sdk/testing exports the shared contract tests, and the production entry point deliberately does not, so a runtime dependency graph never reaches test scaffolding. The official providers assert the same contract with it.
Not included
- No CLI command, middleware, state adapter or execution rule can be contributed.
- No automatic discovery: the registry is explicit and configuration-driven.
- No marketplace. This site documents the contract and the official providers; a third-party provider documents its own platform.