A framework for building composable, type-safe plugin systems. It combines Effect for resource lifecycle management, Module Federation for remote loading, and oRPC for type-safe contracts.
bun add every-pluginCreate a runtime and use your first plugin:
import { createPluginRuntime } from "every-plugin/runtime";
const runtime = createPluginRuntime({
registry: {
"data-source": {
remoteUrl: "https://cdn.example.com/plugins/source/remoteEntry.js",
version: "1.0.0"
}
},
secrets: {API_KEY: "secret-value" }
});
const { createClient } = await runtime.usePlugin("data-source", {
secrets: { apiKey: "{{API_KEY}}" },
variables: { timeout: 30000 }
});
const client = createClient();
const result = await client.search({ query: "typescript", limit: 20 });
console.log(`Found ${result.items.length} items`);
await runtime.shutdown();Plugins define their interface using oRPC procedures. The runtime ensures type safety from contract definition through to client calls:
export default createPlugin({
initialize: () => { /* setup resources, return context */ }
contract: oc.router({
getData: oc.procedure
.input(z.object({ id: z.string() }))
.output(DataSchema),
streamItems: oc.procedure
.input(QuerySchema)
.output(eventIterator(ItemSchema))
}),
createRouter: (context, builder) => {
// builder is pre-configured: implement(contract).$context<TContext>()
}
});
const { createClient } = await runtime.usePlugin("plugin-id", config);
const client = createClient();
const data = await client.getData({ id: "123" });The runtime handles plugin loading (Module Federation or local imports), secret injection, initialization, and cleanup. Resources are managed automatically through Effect:
const runtime = createPluginRuntime({
registry: { /* plugin definitions */ },
secrets: { /* secret values */ }
});
const result = await runtime.usePlugin("plugin-id", config);
await runtime.shutdown();usePlugin() returns an EveryPlugin with three ways to work with plugins:
const { createClient, router, metadata } = await runtime.usePlugin(...);
// 1. Client - Direct typed procedure calls
const client = createClient();
const data = await client.getData({ id: "123" });
// 2. Router - Mount as HTTP endpoints
const handler = new OpenAPIHandler(router);
// 3. Streaming - Process continuous data
const stream = await client.streamItems({ query: "typescript" });
for await (const item of stream) {
console.log(item);
}Two deployment patterns with identical APIs:
// Production - Remote plugins via Module Federation
const runtime = createPluginRuntime({
registry: {
"plugin-id": {
remoteUrl: "https://cdn.example.com/remoteEntry.js",
version: "1.0.0"
}
}
});
// Development/Testing - Local plugins
const runtime = createLocalPluginRuntime(
{ registry: {...} },
{ "plugin-id": PluginImplementation }
);
const { createClient } = await runtime.usePlugin("plugin-id", config);
const client = createClient();Secrets are defined centrally and injected at runtime using template syntax:
const runtime = createPluginRuntime({
registry: { /* plugins */ },
secrets: {
API_KEY: process.env.API_KEY,
DATABASE_URL: process.env.DATABASE_URL
}
});
const { createClient } = await runtime.usePlugin("plugin-id", {
secrets: {
apiKey: "{{API_KEY}}",
dbUrl: "{{DATABASE_URL}}"
},
variables: {
timeout: 30000
}
});
const client = createClient();Plugins aren't limited to simple API wrappers. With Effect's resource management, they can:
Run Background Tasks - Continuously poll APIs, process queues, or generate events:
initialize: (config) => Effect.gen(function* () {
const queue = yield* Queue.bounded(1000);
yield* Effect.forkScoped(
Effect.gen(function* () {
while (true) {
const event = yield* fetchFromExternalAPI();
yield* Queue.offer(queue, event);
yield* Effect.sleep("1 second");
}
})
);
return { queue };
})Stream Data Continuously - Process infinite streams with backpressure:
streamEvents: handler(async function* () {
while (true) {
const event = await Effect.runPromise(Queue.take(context.queue));
yield event;
}
})Compose into Pipelines - Chain plugins together for complex workflows:
const { client: source } = await runtime.usePlugin("data-source", config);
const { client: processor } = await runtime.usePlugin("transformer", config);
const { client: distributor } = await runtime.usePlugin("webhook", config);
const rawData = await source.fetch({ query: "typescript" });
const transformed = await processor.transform({ items: rawData.items });
await distributor.send({ items: transformed.items });Mount as HTTP APIs - Expose plugin procedures via OpenAPI or RPC:
const { router } = await runtime.usePlugin("plugin-id", config);
const handler = new OpenAPIHandler(router);
server.use('/api', handler.handle);This flexibility means plugins can be:
- Simple API clients for basic integrations
- Background processors for continuous data ingestion
- Stream transformers for real-time data pipelines
- HTTP services exposed via OpenAPI
- Job workers in queue systems like BullMQ
All with the same type-safe contract interface, and easy to use with simple async/await.
Execute a plugin once with full type safety:
import { createPluginRuntime } from "every-plugin/runtime";
const runtime = createPluginRuntime({
registry: {
"social-feed": {
remoteUrl: "https://cdn.example.com/plugins/social/remoteEntry.js",
version: "1.0.0"
}
},
secrets: {
SOCIAL_API_KEY: "your-api-key"
}
});
const { createClient } = await runtime.usePlugin("social-feed", {
secrets: { apiKey: "{{SOCIAL_API_KEY}}" },
variables: { timeout: 30000 }
});
const client = createClient();
const posts = await client.search({ query: "typescript", limit: 10 });
console.log(`Found ${posts.items.length} posts`);
await runtime.shutdown();For continuous data processing with async iterators:
const { createClient } = await runtime.usePlugin("social-feed", {
secrets: { apiKey: "{{SOCIAL_API_KEY}}" },
variables: { timeout: 30000 }
});
const client = createClient();
const stream = await client.streamItems({ query: "typescript" });
for await (const item of stream) {
console.log("Received item:", item);
if (item.id === "target-id") break;
}Handle errors gracefully with try-catch:
try {
const { createClient } = await runtime.usePlugin("social-feed", config);
const client = createClient();
const result = await client.search({ query: "typescript" });
console.log(result);
} catch (error) {
console.error("Plugin failed:", error);
}Perfect for BullMQ workers or similar job processing systems:
import { Job } from "bullmq";
import { createPluginRuntime } from "every-plugin/runtime";
const runtime = createPluginRuntime({
registry: pluginRegistry,
secrets: await loadSecrets(),
});
const processJob = async (job: Job) => {
const { pluginId, config, input } = job.data;
const { createClient } = await runtime.usePlugin(pluginId, config);
const client = createClient();
return await client.process(input);
};
const worker = new Worker("my-queue", processJob);
process.on("SIGTERM", async () => {
await worker.close();
await runtime.shutdown();
});Chain multiple plugins for complex workflows:
const { createClient: createSourceClient } = await runtime.usePlugin("data-source", {
secrets: { apiKey: "{{SOURCE_API_KEY}}" }
});
const source = createSourceClient();
const { createClient: createProcessorClient } = await runtime.usePlugin("transformer", {
variables: { format: "json" }
});
const processor = createProcessorClient();
const { createClient: createDistributorClient } = await runtime.usePlugin("webhook", {
secrets: { webhookUrl: "{{WEBHOOK_URL}}" }
});
const distributor = createDistributorClient();
const rawData = await source.fetch({ query: "typescript" });
const processed = await processor.transform({ items: rawData.items });
await distributor.send({ items: processed.items });import { createPluginRuntime } from "every-plugin/runtime";
import { OpenAPIHandler } from "orpc/openapi";
import express from "express";
const runtime = createPluginRuntime({
registry: pluginRegistry,
secrets: await loadSecrets()
});
const app = express();
const { router } = await runtime.usePlugin("data-api", config);
const handler = new OpenAPIHandler(router);
app.use('/api', handler.handle);
app.listen(3000);Creates a runtime for plugin execution.
Parameters:
config.registry: Plugin registry mapping with remote URLsconfig.secrets: Secret values for template injection (optional)config.logger: Custom logger implementation (optional)
Returns: Runtime instance with usePlugin() and shutdown() methods
Creates a runtime with local plugin implementations for testing/development.
Parameters:
config: Same ascreatePluginRuntimeplugins: Map of plugin IDs to plugin implementations
Returns: Runtime instance with same API as createPluginRuntime
Load, initialize, and return a plugin interface.
Parameters:
pluginId: ID from the registryconfig.secrets: Secret templates to injectconfig.variables: Configuration variables
Returns: Promise resolving to { client, router, metadata }
client: Typed client for direct procedure callsrouter: oRPC router for HTTP mountingmetadata: Plugin metadata
Cleanup all plugins and release resources.
Returns: Promise that resolves when shutdown is complete
- Source: Fetch data from external APIs with oRPC contracts
- Transformer: Process and transform data between formats
- Distributor: Send data to external systems
All plugin types use the same oRPC contract interface for type safety.
# Install dependencies
bun install
# Build the package
bun run build
# Run tests
bun testMIT
