How extensions work
Understand distributions, runtime registration, and Kurrier's trusted extension architecture.
You do not need to understand this page to create a feature. If you just want to add one, start with Creating an extension.
The short version
Feature packages expose runtime-specific extension lists. The active distribution selects those lists, and each Kurrier runtime registers only what it needs.
Feature implementation
↓
server.ts / worker.ts / web.ts
↓
package runtime extension lists
↓
active distribution adapters
↓
runtime registration
↓
kurrierServer / kurrierWeb / Nitro worker
↓
hooks, workers, schedulers, pages and navigationCore owns infrastructure and lifecycle. Extensions contribute feature behavior.
Why runtime entry points are separate
Kurrier's Next.js application and Nitro worker are separate Node runtimes with separate in-memory registries and different dependency requirements.
A worker must not load React dashboard components just because a feature also has web UI. Likewise, web registration should not unnecessarily load worker-only handlers or background infrastructure.
For that reason, extension packages can expose separate entry points:
extension/
├── server.ts
├── worker.ts
└── web.tsA feature only needs the entry points it actually uses.
Package extension lists
A package groups its features into runtime-specific collections.
Conceptually:
export const serverExtensions = [
singleWorkspaceServerExtension,
];
export const workerExtensions = [
someWorkerExtension,
];
export const webExtensions = [
someWebExtension,
];For Kurrier's OSS features, these live under:
packages/oss/src/extensions/Distribution adapters
The distribution layer decides which extension lists belong to a product.
The OSS distribution adapter exposes the server, worker, and web extensions shipped with Kurrier.
The generic registration layer then selects the active distribution.
Distribution selection uses explicit static imports and a distribution switch. This keeps runtime composition obvious and ensures bundlers can see every supported implementation.
Conceptually:
switch (distribution) {
case "oss":
registerOssExtensions();
return;
}The same principle applies at the server, worker, and web registration boundaries.
Feature code should not import another distribution's implementation.
Registration is runtime-local
Web and worker processes do not share an extension registry.
Each runtime registers the appropriate extension collection for itself.
kurrierServer exposes stable server-side extension facilities such as hooks and background contribution discovery.
Application code can run hooks through:
await kurrierServer.hooks.run("workspace.beforeCreate", {
userId,
});kurrierWeb exposes registered UI contributions in structured areas such as:
kurrierWeb.pages.dashboard();
kurrierWeb.pages.auth();
kurrierWeb.navigation.dashboard();The Nitro worker consumes registered worker and scheduler contributions and owns the BullMQ runtime objects that execute them.
Application code consumes these extension points rather than importing individual distribution features.
Worker ownership
Worker extensions are intentionally declarative.
An extension describes a worker with values such as:
type ExtensionWorker = {
queue: string;
concurrency?: number;
handler: (job: Job) => Promise<unknown>;
};A scheduler is described with values such as:
type ExtensionScheduler = {
queue: string;
id: string;
jobName: string;
every: number;
data?: unknown;
};The exact current types in packages/extensions/src/ are authoritative.
The extension does not instantiate BullMQ infrastructure.
Kurrier's Nitro worker owns:
Workercreation- scheduler creation/upsert
- Redis-backed runtime lifecycle
- concurrency wiring
- startup
- graceful shutdown
This keeps queue infrastructure in core while allowing an extension to provide the actual feature handler and schedule.
Queue producers
Producing a job and processing a job are separate concerns.
Kurrier provides shared queue infrastructure in @common. Feature code can enqueue work without owning a BullMQ worker:
web action / hook / server code
↓
shared queue
↓
Nitro worker
↓
registered extension worker
↓
feature handlerThis allows the same feature logic to participate cleanly in both the web and worker runtimes.
The extension registry
registerExtension() activates a trusted extension in the current runtime.
For server extensions, registration attaches declared handlers to the hook registry.
For web extensions, registration makes page and navigation contributions available to kurrierWeb.
For worker extensions, registration makes worker and scheduler contributions available to the worker runtime.
Feature implementations should not call registerExtension() themselves. The active distribution owns composition and registration.
Distribution responsibilities
A distribution is more than a list of extensions.
It defines the product composition around Kurrier core, including:
- which extensions are enabled
- public website pages
- metadata and product presentation
- layouts
- static feature configuration
- schema composition for distribution tooling
- runtime product access policy
Static product capabilities belong in distribution config. For example, whether a distribution includes Drive at all can be represented as a static feature flag.
Workspace-specific runtime entitlement is separate.
Runtime access policy is not an extension
Runtime product access is a distribution concern.
Kurrier exposes a distribution access contract for workspace-level capabilities such as:
type WorkspaceAccess = {
canUseWorkspace: boolean;
canCreateProvider: boolean;
canSyncMail: boolean;
canCreateStorageVolume: boolean;
reason: string | null;
};The exact current contract in the source tree is authoritative.
The OSS distribution implements the default access policy. The contract keeps runtime policy separate from feature code.
Shared application code therefore asks the active distribution for access rather than hard-coding product policy into feature code.
This is deliberately separate from extensions:
- extensions contribute features and lifecycle behavior
- distribution access decides whether the product/workspace may perform runtime operations
Application boundaries
Core application code should depend on stable Kurrier facades and distribution contracts rather than individual implementations.
Server lifecycle behavior:
kurrierServer.hooks.run(...)Web contributions:
kurrierWeb.pages.dashboard()
kurrierWeb.pages.auth()
kurrierWeb.navigation.dashboard()Runtime entitlement:
DISTRIBUTION_ACCESS.workspace(workspaceId)Background execution is similarly discovered through the server extension facade and executed by the Nitro worker rather than by importing a distribution feature directly.
Application-specific helper functions may wrap these contracts so normal actions do not need to know how a distribution computes policy or composes extensions.
Trusted extensions
The current extension system is designed for trusted in-process code.
Trusted extensions can use the same application packages and server capabilities as Kurrier itself. Depending on the feature, this can include database access, environment variables, server actions, queues, and other internal resources.
This model is appropriate for:
- Kurrier's own OSS features
- private extensions
- self-hosted extensions whose source is trusted
- reviewed contributions shipped with Kurrier
It is not a sandbox.
Future remote extensions
A future marketplace can use a different security boundary: manifests, explicit permissions, remote APIs, and isolated or hosted UI such as iframe-based surfaces.
Remote marketplace extensions should not be assumed to have arbitrary Kurrier database or Node runtime access.
That future model is separate from, and does not weaken, the trusted extension system documented here.
Extension or distribution?
A useful rule:
An extension is an application feature.
Use it for authenticated pages, navigation, lifecycle behavior, background workers, and recurring jobs.
A distribution defines the product.
Use it for public pages, metadata, product configuration, enabled extensions, and runtime access policy.
Most contributors adding application functionality should start with an extension.
Working source
Use the current source tree as the final authority for the API:
packages/extensions/src/
packages/oss/src/extensions/
packages/distribution/src/
apps/worker/server/plugins/These docs describe the architecture as shipped in the Kurrier OSS repository.