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.
ColumnTypePurpose
plugin_idTEXT"discord", "slack"
project_idINTEGER FKWhich project this connection belongs to
external_user_idTEXTProvider's user/team ID
access_tokenTEXTOAuth access token
refresh_tokenTEXTFor token refresh (nullable)
token_expires_atINTEGERExpiry timestamp (nullable)
plugin_resources
External resources claimed by projects. A Discord server or Slack workspace can only belong to one project at a time.
ColumnTypePurpose
plugin_idTEXT"discord", "slack"
resource_typeTEXT"server" (Discord), "workspace" (Slack)
project_idINTEGER FKOwning project
external_resource_idTEXTGuild ID, team ID
display_nameTEXTHuman-readable name
metadataTEXT JSONExtra 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.
ColumnTypePurpose
codeTEXT UNIQUE32-byte random hex
user_idINTEGERWho initiated the flow
project_idINTEGERTarget project
plugin_idTEXTTarget plugin
expires_atINTEGERCreated + 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 commands
  • discord.onEvent() messages, reactions
  • discord.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, mentions
  • slack.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.