definePlatform is the entry point for building your own provider. It takes a name and a definition object, and returns a callable that:
- exposes a
.config()method for registering the provider onSpectrum() - accepts a Spectrum instance, space, or message for narrowing
- carries any
staticproperties you declare (like iMessage’stapbacks)
Shape
Field reference
| Field | Required | Description |
|---|---|---|
config | Yes | A Zod schema that validates the object passed to platform.config(). If every field is optional, platform.config() can be called with no arguments. |
user.resolve | Yes | Resolves a user from a string ID. Returns at minimum { id: string }. |
user.schema | No | Optional Zod schema for extra user properties. |
space.create | Yes | Creates a conversation from participants. Receives an array of users plus optional params. |
space.get | No | Hydrates a space from a known platform space ID. When omitted, the framework builds { id } and validates it against space.schema. Providers whose schema requires more fields must implement this. |
space.schema | No | Optional Zod schema for the resolved space. |
space.params | No | Zod schema for additional space parameters — surfaces as the second arg to platform(app).space.create() and platform(app).space.get(). |
space.actions | No | A map of content-builder factories that become sugar methods on the resolved space. Each space.<name>(...args) delegates to space.send(factory(...args)). Names that collide with built-in Space methods (send, edit, unsend, startTyping, stopTyping, responding, getMessage, rename, avatar) are skipped at runtime with a warning. |
lifecycle.createClient | Yes | Creates the platform client. Receives config, projectId, projectSecret (both may be undefined), and store. |
lifecycle.destroyClient | No | Tears down the client on shutdown. Omit if no cleanup is needed. |
messages | Yes | Async generator that yields incoming messages. |
send | Yes | Dispatches a content item to a space. All content types — text, markdown, attachments, reactions, replies, edits, unsends, typing indicators — flow through this single action. Return a ProviderMessageRecord for content that produces a message (including reactions — the record is the unsend handle), or undefined for fire-and-forget signals (typing, edits, unsends). |
actions.getMessage | No | Fetches a message by ID from a space. Receives (ctx, space, messageId) where ctx is { client, config, store }. Powers space.getMessage(id). When omitted, space.getMessage() throws UnsupportedError. |
actions.[custom] | No | Platform-specific methods projected onto the platform instance. Each receives (ctx, ...args) where ctx is { client, config, store }; the public signature drops ctx. Names that collide with reserved instance keys (user, space, messages, plus any event names) are skipped at runtime with a warning. |
events.[custom] | No | Additional async generators for platform-specific events — exposed on app.[eventName]. |
message.schema | No | Zod schema for extra properties on incoming messages. |
static | No | Constants attached to the platform object (e.g. tapback names). |
Message direction
Records yielded frommessages are wrapped as inbound, and records returned from send are wrapped as outbound. When a provider knows the actual direction of a record, it can set direction on the raw record and Spectrum will use it instead of the wrapping context:
direction, nested targets default to the outer record’s direction.
Event producers
Every event generator receives{ client, config, store } and returns an AsyncIterable. The signature is .
The core messages stream lives at the top level of the definition. Optional custom event streams (presence, read receipts, etc.) live inside events:
app.presence) and the narrowed platform instance (myPlatform(app).presence).
Message extras
Declare amessage.schema to add extra typed fields to every incoming message. The extractor surfaces them through a narrowed message:
Instance actions
Methods declared inactions are projected onto the platform instance returned by myPlatform(app). The framework injects ctx = { client, config, store } as the first argument, so callers only pass the trailing args.
Two tiers share the actions slot:
- Platform-wise actions (
getMessage) — framework-recognized names. Always present on every platform instance. When omitted, the framework wires a default that throwsUnsupportedError. - Platform-specific actions — free-form keys for platform ergonomics (e.g. iMessage’s
getAttachment). Only present when declared.
Fusor-backed providers
When your platform receives inbound messages through webhooks (rather than a persistent connection), usefusor(...) as the client in lifecycle.createClient. A Fusor client handles webhook signature verification and delivers parsed payloads to your messages handler:
definePlatform replaces the top-level messages async generator with a per-webhook-delivery handler that receives { payload, config, respond }. Call respond() to set the HTTP response sent back to the webhook caller.
Registering your platform
Exported platforms work like the built-ins — register with.config() and use narrowing for the typed surface: