Plugin System
generic framework for third-party integrations — discord, slack, email, and beyond
Overview
Plugins connect Fling projects to external services like Discord, Slack, and email providers.
Each plugin runs as a standalone Cloudflare Worker — completely
isolated from the API worker — with rate-limited proxy routes and optional OAuth,
webhook handling, and resource management. Some plugins (like email-send) are auto-available
with no install step; others require OAuth setup.
01
Install
fling plugin install→
02
Register
onCommand, onEvent
→
03
Deploy
metadata extraction
→
04
Live
webhooks + proxy
Architecture
Plugin workers are standalone — they do not communicate with the API worker at runtime.
The CLI talks to both the API and plugin workers directly. User workers reach plugins
via Cloudflare service bindings.
CLI
fling plugin install
External Service
Discord / Slack API
OAuth, status
webhooks
Plugin Worker
fling-discord / fling-slack / fling-email-send
dispatch event
proxy call (HMAC)
User Worker
Handlers: onCommand, onEvent
service binding
Binding
DISCORD_PLUGIN
Binding
SLACK_PLUGIN
Binding
EMAIL_PLUGIN
Secret Isolation
Plugin secrets (bot tokens, client secrets) live only in the plugin worker.
The API worker and user workers never see them — keeping the blast radius small.
Shared Database
Plugin workers share the platform D1 database with the API worker for OAuth connections
and resource claims. This avoids data duplication while keeping workers independent.
Install Flow
Plugin installation uses OAuth with short-lived session codes to avoid leaking long-lived
API tokens in browser URLs. The CLI orchestrates the flow between the API and plugin worker.
Step 1
fling plugin install discord
POST /oauth/session
API Worker
Generate session code (2 min TTL)
open browser
Plugin Worker
Validate code, build OAuth state
redirect to provider
External
OAuth consent → authorize
callback with code
Plugin Worker
Store tokens, claim resources
CLI polls until connected
Done
Plugin connected, resources claimed
Why session codes? — Passing a long-lived API token in a browser URL
exposes it to browser history, server logs, Referer headers, and extensions. Session codes
are 32 random bytes, bound to a single plugin+project, expire in 2 minutes, and are consumed
atomically on first use. Even if leaked, they're already spent.
Webhook Flow
When an external event happens (a Discord slash command, a Slack message), the service sends
a webhook to the plugin worker. The plugin verifies the signature, looks up which project
owns the resource, and dispatches to the user's worker.
Event
User runs /ping in Discord
POST /webhook (signed)
Verify
Check signature (Ed25519 / HMAC)
lookup resource owner
Resolve
guild_id → project → worker name
dispatch to /__plugin/discord
User Worker
Run onCommand("ping") handler
discord.reply() via proxy
Result
"Pong!" appears in Discord
Internal Paths
Plugin dispatch uses
/__plugin/discord and /__plugin/slack
paths. These are blocked from external HTTP access but available to
service-binding dispatch from plugin workers.
Opaque Routing
Plugin code receives an opaque
ResourceOwnerRef for dispatch —
it never sees internal user or project IDs. The framework handles all
database lookups and worker name resolution.
Proxy Authentication
When user code calls
discord.reply(), slack.sendMessage(),
or email.send(), the runtime authenticates the request with HMAC to prove
which project is calling. This prevents one project from impersonating another.
User Code
discord.sendMessage(...)
sign with WORKER_SECRET
Identity Token
{projectId}.{timestamp}.{signature}
X-Fling-Identity header
Plugin Worker
Validate HMAC, check timestamp
rate limit (3/60s)
Proxy
Forward to external API
Why per-project secrets? — Each project has a unique
worker_secret
generated at creation time and stored in both the user worker's environment bindings and the platform
database. The plugin worker looks up the secret by project ID, so a stolen token from project A
cannot authenticate as project B.
Database Schema
Three tables in the shared D1 database support the plugin system.
All are generic — a
plugin_id column distinguishes Discord from Slack.
Note: email-send is stateless and uses none of these tables.
plugin_oauth_connections
One OAuth connection per plugin per project. Stores access/refresh tokens from the provider.
| Column | Type | Purpose |
|---|---|---|
| plugin_id | TEXT | "discord", "slack" |
| project_id | INTEGER FK | Which project this connection belongs to |
| external_user_id | TEXT | Provider's user/team ID |
| access_token | TEXT | OAuth access token |
| refresh_token | TEXT | For token refresh (nullable) |
| token_expires_at | INTEGER | Expiry timestamp (nullable) |
plugin_resources
External resources claimed by projects. A Discord server or Slack workspace
can only belong to one project at a time.
| Column | Type | Purpose |
|---|---|---|
| plugin_id | TEXT | "discord", "slack" |
| resource_type | TEXT | "server" (Discord), "workspace" (Slack) |
| project_id | INTEGER FK | Owning project |
| external_resource_id | TEXT | Guild ID, team ID |
| display_name | TEXT | Human-readable name |
| metadata | TEXT JSON | Extra data (e.g., Slack bot token) |
oauth_session_codes
Short-lived, one-time-use codes that bridge the CLI and browser for OAuth.
Consumed atomically on first use.
| Column | Type | Purpose |
|---|---|---|
| code | TEXT UNIQUE | 32-byte random hex |
| user_id | INTEGER | Who initiated the flow |
| project_id | INTEGER | Target project |
| plugin_id | TEXT | Target plugin |
| expires_at | INTEGER | Created + 2 minutes |
Discord, Slack & Email
Three plugins ship today. Discord and Slack require OAuth installation and handle bidirectional
communication (webhooks in, proxy calls out). Email-send is the simplest — auto-available,
proxy-only, no OAuth or webhooks needed.
Discord
Slash Commands
Shared bot, per-guild command registration.
Requires
fling plugin install.
- Signature: Ed25519 on request body
- Resource: server (guild)
- Bot token: shared, in plugin env
- Commands: per-guild via Discord API
discord.onCommand()slash commandsdiscord.onEvent()messages, reactionsdiscord.reply()discord.followup()discord.sendMessage()discord.editMessage()
Slack
@mention Events
Per-workspace bot tokens, Events API delivery.
Requires
fling plugin install.
- Signature: HMAC-SHA256 with signing secret
- Resource: workspace (team)
- Bot token: per-workspace, in DB
- Events: via Slack Events API
slack.onEvent()messages, mentionsslack.sendMessage()slack.editMessage()slack.addReaction()slack.threadReply()
Email-Send
Transactional Email
Auto-available, no install needed. Just import and call
email.send() to send via Resend.
- OAuth: none (auto-available)
- Resources: none
- Webhooks: none
- Provider: Resend REST API
- From: {slug}@flingit.run
email.send({ to, subject, html })- Limits: 50 recipients, 1MB body
- Rate: 3 sends / 60s per project
The generic framework — adding a new plugin means implementing a
PluginDefinition with proxy routes and optional OAuth config, webhook handler,
and resource management. The framework provides createPluginWorker(plugin) which
assembles the complete worker automatically. Email-send shows the minimal case: just a single
proxy route with validation and rate limiting — no OAuth, no webhooks, no state.
Metadata Extraction
At deploy time, the bundler imports user code in a stub environment
to collect registered commands. Output is sent to the plugin worker's
POST /deploy endpoint.
Rate Limiting
All proxy routes enforce 3 actions per 60 seconds per project
via Cloudflare Durable Objects. Disabled on private deployments.
Plugin-Safe Types
Plugins receive
PluginProject and PluginUser types
that hide internal database IDs. Dispatch uses opaque
ResourceOwnerRef.