Is your feature request related to a problem? Please describe.
Today there is no simple, safe and reliable way for a company (or a single power user) to extend OpenCloud with small pieces of custom logic. Typical things people want to do are:
- "When somebody adds a task/checkbox to a markdown document, create a ticket in our issue tracker."
- "When a list item is added to
TODO.md in a shared space, notify a Teams/Slack/Matrix channel."
- "When a file lands in
/Incoming/Invoices, call our ERP API and move the file."
- "Expose a small tool/command inside OpenCloud that talks to an internal service."
Right now every one of these requires either a full external integration (webhooks + a separately hosted service), deep changes to the Go backend, or a frontend-only web extension that cannot react to server-side events reliably. There is no first-class, sandboxed, scriptable "plugin" concept that is:
- Easy – a small
index.ts in a folder, not a new microservice.
- Secure – plugin code runs isolated, with an explicit, narrow API surface and no access to internal services.
- Reliable – lifecycle, cleanup, retries, timeouts, background jobs and error handling are handled by the runtime, not by every plugin author re-inventing them.
- Small & composable – a plugin (or a whole plugin host) should be able to run as a tiny, stateless-looking unit whose durable state lives in object/SQLite storage, so it can be evicted, moved, scaled and recomposed without losing anything.
Every company wants to write a few of these small plugins quickly, keep them in their repo, and know they won't take down or compromise the server.
Describe the solution you'd like
OpenCloud should adopt (or closely mirror) the OpenCode v2 plugin architecture – https://opencode.ai/v2/docs (open source) – and in particular its Effect-TS variant: https://opencode.ai/v2/docs/build/plugins/effect/
Concretely:
1. Plugin packaging & discovery, same as OpenCode
- Load plugins automatically from a well-known directory (e.g.
.opencloud/plugins/<name>/index.ts per space/instance) and from published packages via config:
- Options are passed to the plugin via
ctx.options, exactly like OpenCode.
2. Effect-native plugin definition
A plugin is a single default export; its lifetime is a Scope, so registrations, background fibers and finalizers are cleaned up together on reload/unload:
import { Plugin } from "@opencloud/plugin/effect"
import { Effect, Stream, Schedule } from "effect"
export default Plugin.define({
id: "acme.tasks",
effect: (ctx) =>
Effect.gen(function* () {
// React to server-side events (files, markdown edits, shares, spaces...)
yield* ctx.event.subscribe().pipe(
Stream.filter((e) => e.type === "markdown.task.added"),
Stream.runForEach((e) => createTicket(e)),
Effect.forkScoped,
)
// Background job, automatically interrupted on unload
yield* Effect.repeat(syncTracker(), { schedule: Schedule.spaced("5 minutes") })
.pipe(Effect.forkScoped)
yield* Effect.addFinalizer(() => Effect.logInfo("acme.tasks unloaded"))
}),
})
3. A narrow, typed context – no access to private core services
Mirror OpenCode's Context with OpenCloud domains, e.g.:
| Domain |
Purpose |
ctx.event |
Stream of OpenCloud events (file created/updated/deleted, markdown task/list item added, share created, space changes, user events) |
ctx.file / ctx.space |
read/write files, metadata, tags, move/copy, list spaces |
ctx.markdown |
structured access to markdown docs (tasks, list items, headings, frontmatter) |
ctx.share / ctx.user / ctx.group |
read & manage shares, resolve users/groups |
ctx.notification |
send notifications (in-app, mail) |
ctx.command |
register commands/actions that show up in the web UI (add-only, like OpenCode's CommandEditor) |
ctx.tool |
register typed tools (Effect Schema input/output) usable by OpenCloud automations and AI features, incl. codemode: true |
ctx.integration |
OAuth/key connections to external systems (Jira, GitHub, ERP…), with authorize/refresh as Effects |
ctx.storage |
durable JSON KV scoped to the plugin ID, with prefix scan – backed by pluggable storage (see §6) |
ctx.hook |
intercept operations before/after (file.write.before, share.create.before, upload.after…) with the ability to Effect.fail to veto |
ctx.options / ctx.app |
plugin options and instance info |
Hooks and transforms should follow OpenCode's semantics: run in plugin order, later hooks see earlier changes, registrations return a Registration with dispose, and everything is released when the plugin scope closes.
4. Secure execution
- Run plugins in an isolated JS/TS runtime (Bun/Node worker, workerd, or a sidecar "plugin host" process) that only talks to OpenCloud through the typed plugin API – never directly to the database, storage backend or internal Go services.
- Support OpenCode's code mode pattern (
codemode: true): tool/plugin code executed through a restricted DSL/sandbox with an explicit allow-list of capabilities, so admins can permit "small scripts" without granting full runtime access.
- Per-plugin permissions declared up front (which events, which spaces, which outbound hosts), enforced by the host.
5. Reliability via Effect-TS
Using Effect gives every plugin, for free:
- typed errors instead of thrown exceptions,
- structured concurrency (
forkScoped) and guaranteed cleanup (addFinalizer),
- built-in retries, timeouts and schedules,
- backpressured
Streams for events,
- a runtime that can supervise, restart and hot-reload plugins without restarting OpenCloud.
6. Runtime profiles with durable state on object/SQLite storage – small and composable
OpenCode ships different host profiles for the same plugin API. Besides the local Bun/Node profile, @opencode-ai/sdk/workerd runs the whole host inside a Cloudflare Durable Object: it uses the object's SQLite storage, persists durable events so the host can be evicted and recovered, and swaps out unavailable local filesystem/process services for storage-backed equivalents:
import { OpenCodeWorkerd } from "@opencode-ai/sdk/workerd"
import myPlugin from "./my-plugin"
export class OpenCodeDO {
private readonly opencode: Promise<OpenCodeWorkerd.Interface>
constructor(state: DurableObjectState) {
// One host per DO instance, not per request; blockConcurrencyWhile
// keeps events out until ready and resets the object if init fails.
this.opencode = state.blockConcurrencyWhile(() =>
OpenCodeWorkerd.create({
storage: state.storage, // durable state lives here
config: { default_agent: "build" },
plugins: [myPlugin],
}),
)
}
async fetch() {
const opencode = await this.opencode
return Response.json(await opencode.health.get())
}
}
OpenCloud should offer the same shape:
- Storage-backed host state.
ctx.storage, plugin registrations, pending hooks and the durable event log are written to a pluggable storage adapter – Durable Object SQLite, an S3/object-storage bucket, Postgres, or OpenCloud's own decomposedfs/posixfs – instead of local disk or in-memory state.
- Eviction-safe by design. Because the event stream is persisted, a plugin host can be evicted, restarted, moved to another node or scaled to zero and pick up exactly where it left off. No lost "task added" events during redeploys.
- Tiny units. A host + a handful of plugins becomes a very small, stateless-looking unit (a Worker/DO, a container, a sidecar) whose entire state is external. That makes it cheap to run one host per space, per team or per tenant, which is a natural isolation and billing boundary for OpenCloud.
- Composable. Hosts are addressable objects; they can be composed – one per space forwarding to a per-tenant host, a central "integrations" host that others call via
ctx.rpc – without any shared mutable state, exactly like Durable Objects compose.
- Same plugin, any profile. The plugin code (
Plugin.define({...})) does not change between the local profile, a Kubernetes sidecar and a Durable Object; only the host profile and storage adapter differ. Plugin authors never think about persistence or eviction.
- Same lifecycle guarantee.
blockConcurrencyWhile-style startup gating should be built into the host: no event is delivered until all plugins have loaded, and a failed initialization resets the unit cleanly instead of leaving a half-loaded host serving traffic.
7. Why this matters for OpenCloud specifically
The killer use case is the markdown document as a lightweight workflow surface: a team keeps a tasks.md/board.md in a space, and a 30-line plugin turns "someone ticked a checkbox / added a list item" into an API call, a process trigger, a notification, or a file operation. Combined with §6, that 30-line plugin can run as a tiny per-space unit with its state in object storage – no server to babysit, nothing lost on restart. This is exactly what OpenCode's architecture already makes trivial for agentic workflows, and OpenCloud could be the first file platform to offer it natively.
Reference implementations to look at: https://github.com/azatakmyradov/opencode-plugins/tree/main
Describe alternatives you've considered
- Webhooks + external service (n8n, Zapier, custom microservice): works, but every small automation becomes its own deployment with auth, retries, hosting and state to manage. No sandboxing inside OpenCloud, no UI integration, no shared storage/config, and delivery is fire-and-forget – events are lost if the receiver is down.
- Frontend-only web extensions: good for UI, but cannot reliably react to server-side events (uploads via WebDAV/sync clients, other users' edits, scheduled jobs).
- Native Go plugins / forking services: high barrier to entry, must be compiled against a specific version, no isolation – a bug crashes the server.
- WASM plugin system (e.g. Extism): strong sandbox, but poor DX for the target audience (people who want to write a few lines of TypeScript quickly), and no established ecosystem/patterns for lifecycle, hooks, streams or durable state.
- MCP servers only: great for AI tool exposure, but MCP has no concept of event subscriptions, hooks, transforms, plugin lifecycle or persistence. It should be one domain the plugin API can manage (as
ctx.mcp in OpenCode), not the whole story.
- Stateful long-running plugin daemon with local disk: simpler at first, but not evictable, not horizontally scalable, and turns every plugin host into a pet. Storage-backed hosts (§6) avoid this.
- Inventing a bespoke OpenCloud plugin API from scratch: possible, but OpenCode v2 already solved the hard parts (scoped lifecycle, ordered hooks/transforms, typed tools, integrations/OAuth, storage, secure code mode, multiple host profiles incl. Durable Objects) and is open source. Reusing the design keeps plugins portable and lets the two communities share patterns and even packages.
Additional context
Suggested minimal first milestone:
- Plugin host (Bun/Node sidecar) that loads
.opencloud/plugins/* and configured packages, with Plugin.define + Scope lifecycle and startup gating (no events before all plugins are loaded).
ctx.event.subscribe() bridged from OpenCloud's existing event bus (NATS events for file/share/space changes) plus new markdown-level events (task.added, task.completed, list.item.added), with a persisted event log so hosts can be restarted without loss.
ctx.file, ctx.storage, ctx.notification, ctx.options – with ctx.storage behind a storage-adapter interface (SQLite/Postgres/object storage first).
ctx.hook for file.write.before/after and share.create.before (vetoable).
ctx.command / ctx.tool so plugins can surface actions in the web UI.
- A second host profile (workerd/Durable Object or a minimal container) proving the same plugin runs unchanged with all state in external storage – one host per space.
Everything else (integrations/OAuth, MCP management, code mode, transforms, host-to-host RPC) can follow the OpenCode API shape incrementally.
Is your feature request related to a problem? Please describe.
Today there is no simple, safe and reliable way for a company (or a single power user) to extend OpenCloud with small pieces of custom logic. Typical things people want to do are:
TODO.mdin a shared space, notify a Teams/Slack/Matrix channel."/Incoming/Invoices, call our ERP API and move the file."Right now every one of these requires either a full external integration (webhooks + a separately hosted service), deep changes to the Go backend, or a frontend-only web extension that cannot react to server-side events reliably. There is no first-class, sandboxed, scriptable "plugin" concept that is:
index.tsin a folder, not a new microservice.Every company wants to write a few of these small plugins quickly, keep them in their repo, and know they won't take down or compromise the server.
Describe the solution you'd like
OpenCloud should adopt (or closely mirror) the OpenCode v2 plugin architecture – https://opencode.ai/v2/docs (open source) – and in particular its Effect-TS variant: https://opencode.ai/v2/docs/build/plugins/effect/
Concretely:
1. Plugin packaging & discovery, same as OpenCode
.opencloud/plugins/<name>/index.tsper space/instance) and from published packages via config:ctx.options, exactly like OpenCode.2. Effect-native plugin definition
A plugin is a single default export; its lifetime is a
Scope, so registrations, background fibers and finalizers are cleaned up together on reload/unload:3. A narrow, typed context – no access to private core services
Mirror OpenCode's
Contextwith OpenCloud domains, e.g.:ctx.eventStreamof OpenCloud events (file created/updated/deleted, markdown task/list item added, share created, space changes, user events)ctx.file/ctx.spacectx.markdownctx.share/ctx.user/ctx.groupctx.notificationctx.commandCommandEditor)ctx.toolcodemode: truectx.integrationauthorize/refreshas Effectsctx.storagescan– backed by pluggable storage (see §6)ctx.hookfile.write.before,share.create.before,upload.after…) with the ability toEffect.failto vetoctx.options/ctx.appHooks and transforms should follow OpenCode's semantics: run in plugin order, later hooks see earlier changes, registrations return a
Registrationwithdispose, and everything is released when the plugin scope closes.4. Secure execution
codemode: true): tool/plugin code executed through a restricted DSL/sandbox with an explicit allow-list of capabilities, so admins can permit "small scripts" without granting full runtime access.5. Reliability via Effect-TS
Using Effect gives every plugin, for free:
forkScoped) and guaranteed cleanup (addFinalizer),Streams for events,6. Runtime profiles with durable state on object/SQLite storage – small and composable
OpenCode ships different host profiles for the same plugin API. Besides the local Bun/Node profile,
@opencode-ai/sdk/workerdruns the whole host inside a Cloudflare Durable Object: it uses the object's SQLite storage, persists durable events so the host can be evicted and recovered, and swaps out unavailable local filesystem/process services for storage-backed equivalents:OpenCloud should offer the same shape:
ctx.storage, plugin registrations, pending hooks and the durable event log are written to a pluggable storage adapter – Durable Object SQLite, an S3/object-storage bucket, Postgres, or OpenCloud's own decomposedfs/posixfs – instead of local disk or in-memory state.ctx.rpc– without any shared mutable state, exactly like Durable Objects compose.Plugin.define({...})) does not change between the local profile, a Kubernetes sidecar and a Durable Object; only the host profile and storage adapter differ. Plugin authors never think about persistence or eviction.blockConcurrencyWhile-style startup gating should be built into the host: no event is delivered until all plugins have loaded, and a failed initialization resets the unit cleanly instead of leaving a half-loaded host serving traffic.7. Why this matters for OpenCloud specifically
The killer use case is the markdown document as a lightweight workflow surface: a team keeps a
tasks.md/board.mdin a space, and a 30-line plugin turns "someone ticked a checkbox / added a list item" into an API call, a process trigger, a notification, or a file operation. Combined with §6, that 30-line plugin can run as a tiny per-space unit with its state in object storage – no server to babysit, nothing lost on restart. This is exactly what OpenCode's architecture already makes trivial for agentic workflows, and OpenCloud could be the first file platform to offer it natively.Reference implementations to look at: https://github.com/azatakmyradov/opencode-plugins/tree/main
Describe alternatives you've considered
ctx.mcpin OpenCode), not the whole story.Additional context
@opencode-ai/sdk/workerd(see https://opencode.ai/v2/docs, "Cloudflare")Suggested minimal first milestone:
.opencloud/plugins/*and configured packages, withPlugin.define+Scopelifecycle and startup gating (no events before all plugins are loaded).ctx.event.subscribe()bridged from OpenCloud's existing event bus (NATS events for file/share/space changes) plus new markdown-level events (task.added,task.completed,list.item.added), with a persisted event log so hosts can be restarted without loss.ctx.file,ctx.storage,ctx.notification,ctx.options– withctx.storagebehind a storage-adapter interface (SQLite/Postgres/object storage first).ctx.hookforfile.write.before/afterandshare.create.before(vetoable).ctx.command/ctx.toolso plugins can surface actions in the web UI.Everything else (integrations/OAuth, MCP management, code mode, transforms, host-to-host RPC) can follow the OpenCode API shape incrementally.