Skip to content

Manifest reference ​

manifest.json is the contract between a plugin and the runtime. It is validated at load against packages/shared/src/manifest.ts; an invalid manifest means the plugin is skipped. Unknown top-level fields are rejected (typo protection), so the table below is the complete allowed set.

Minimal example ​

json
{
  "name": "guestbook",
  "version": "0.1.0",
  "api_version": "^1.0",
  "author": "you",
  "description": "A simple server guestbook.",
  "type": "standalone",
  "backend": { "entry": "backend/index.ts" },
  "frontend": { "entry": "frontend/index.html" },
  "permissions": ["data.sql:self", "broadcast.clients"]
}

Top-level fields ​

FieldTypeRequiredSinceStatusNotes
manifest_version2nov2stableManifest document schema version. Absent means 1. An unknown value fails closed rather than being guessed by shape.
namestring (slug)yesv1stableThe plugin's stable identity and directory name. Lowercase slug, matching ^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$ — must start with a letter, no leading, trailing, or consecutive hyphens. There is no separate slug field.
versionstring (semver)yesv1stableStrict MAJOR.MINOR.PATCH. No pre-release or build metadata.
api_versionstring (caret range)yesv1stableThe runtime→plugin contract range this plugin targets, e.g. "^1.0". Checked against the server's PLUGIN_API_VERSION before install, so an incompatible plugin is refused before anything is stopped or copied.
authorstringyesv1stableDisplay name of the publisher. Non-empty.
descriptionstringyesv1stableOne-line summary shown in the admin Plugins panel and the marketplace. Non-empty.
licensestringnov1stableSPDX identifier or free text.
iconstring (lucide icon name)nov1stableIcon shown in the admin Plugins panel, e.g. "Hash". Max 64 characters. An unknown name renders as the default placeholder.
type"core" | "standalone" | "extension"yesv1stableHow this plugin relates to other plugins. This is a RELATIONSHIP axis and is orthogonal to runtime.kind, which describes what the plugin's own code is. See Plugin type & extends.
extendsstring (slug)iff type: "extension"v1stableSlug of the base plugin. Required when type is "extension", and rejected otherwise.
dependenciesRecord<pluginSlug, semverRange>nov1stablePLUGIN-to-plugin dependencies, resolved at boot. Not npm packages — those live in package.json.
runtime{ kind: "ui-only" | "sandboxed-backend" }iff manifest_version: 2v2stableWhat the plugin's own code is. Exactly two kinds: ui-only serves a static frontend to a sandboxed iframe; sandboxed-backend additionally runs bun --smol run under the seccomp+Landlock jail. Inferred when absent (a backend block means sandboxed-backend, frontend-only means ui-only), so no v1 plugin needs an edit. Required to be explicit when manifest_version is 2. See ui-only example.
backend{ entry: string }one of the twov1stableBackend entry point, relative to the plugin root. At least one of backend or frontend is required.
frontend{ entry: string }one of the twov1stableFrontend entry point (an HTML file). Only BUILT assets ship — a frontend-src/ directory is never packaged.
permissionsstring[]yesv1stableHost-sandbox capabilities the PLUGIN PROCESS requests, as resource.action[:scope]. May be empty — a frontend-only plugin needs none. Blanket wildcard scopes are rejected. Distinct from capabilities, which grants USERS things. See Permissions.
capabilities{ key, label?, defaultTier }[]nov1stableUser-facing capabilities this plugin defines for the Admin/Member tiers. Each is registered as a named permission granted to its default tier and above.
client_capabilitiesstring[]nov1stableClient features the plugin requires. Currently only "client.browser".
runtime_capabilitiesstring[]nov1stableRuntime APIs the plugin opts into (voice.media, voice.screen_share, voice.moderation). An unrecognized value is a hard reject, never a silent drop.
managed_servicesstring[]nov1stableRuntime-supervised sidecar services the plugin requests, by registered slug. Recognized today: "livekit". Presence in the registry is checked by the resolver, which can see it.
public_schemaRecord<table, { columns, description }>nov1stableTables this plugin exposes for cross-plugin reads. Underscore-prefixed names are reserved for runtime internals (_config holds secrets) and cannot be exposed. See public_schema.
resources{ memory_mb?, cpu_weight?, disk_mb? }nov1stableRequested resource envelope, as positive integers. Parsed and persisted today; aggregate accounting and enforcement land with governance.
data{ dir_env?, migrate?: "app-export" | "volume-copy" | "none" }nov2stableThe plugin's DATA contract, so the platform can back up and migrate state without knowing the plugin's internals. Code and data are separate: an update swaps code and preserves data.
sidebar{ contributes, refresh_on?, section?, emits_delta? }nov1stableWhether and how the plugin contributes sidebar items. See Sidebar.
file_search{ contributes: boolean }nov1stableOpt in to answering the shell's searchFiles action so bottom-bar media search fans out to this plugin.
settingsPluginSetting[]nov1stableAdmin-configurable settings rendered as a form. Values live in the plugin's _config table; type "secret" is redacted from logs and masked in the UI. See Settings.
event_subscriptionsstring[] (dotted topics)nov1stableTopics outside the plugin's own namespace that its FRONTEND may receive. Undeclared topics are default-deny. Max 32.
serve_ready_handshakebooleannov1stableDefaults to false. Opt into the two-stage handshake: the plugin is not serveable until it calls plugin.serveReady(). Use it when post-spawn hydration must finish before user requests arrive. See Lifecycle.
proxy_mountsProxyMount[]nov1stableReverse-proxy mounts, each disabled until an owner approves it. Requires proxy.http:self or proxy.websocket:self in permissions. See proxy_mounts, Reverse-proxy guide.
trust{ posture: "source-available" | "adapter" | "closed" }nov2reserved (Phase 3)DISCLOSURE posture only. Publisher identity and the signature live in the detached package envelope, never here — a signature cannot live inside the bytes it signs.
companionsCompanionDeclaration[]nov2experimentalPreview workload declarations provisioned by the native supervisor. The complete launch, isolation, health, dependency and resource contract is enforced; availability remains operator-gated while the companion lane is exercised.

Status — stable is implemented and enforced. reserved fields are validated and stored but NOT implemented: declaring one is not silently ignored, it fails the install with a typed error naming the phase, so a plugin can never look installed-and-working while a capability it declared does nothing. Unknown top-level fields are rejected outright, so a typo fails loudly at install time instead of leaving you convinced a setting is wired up.

Plugin type & extends ​

typeMeaningextends
coreShipped by UnCorded (text-channels, voice-channels, members, moderation).forbidden
standaloneThird-party plugin with its own functionality and data.forbidden
extensionThird-party plugin that extends a base plugin.required — the base plugin slug

Most third-party plugins are standalone.

Settings ​

Each entry in settings[] is rendered as a form field in Server settings and is readable via plugin.settings.

json
{
  "key": "max_message_length",
  "label": "Max message length",
  "description": "Maximum characters allowed per message.",
  "type": "number",
  "default": 5000,
  "stops": [
    { "value": 2000, "label": "2k" },
    { "value": 5000, "label": "5k" },
    { "value": 0,    "label": "Unlimited" }
  ]
}
FieldTypeApplies toNotes
keystringallUnique within the plugin. Max 256 chars.
labelstringallShown in the admin panel.
descriptionstringallOptional help text.
type"string" | "secret" | "number" | "boolean"—secret values are redacted from logs and masked in the UI.
requiredbooleanallSurfaced as a warning if unset.
defaultstring | number | booleanallMust match type. Used when unset.
min / max / stepnumbernumberBounds and slider step (step > 0).
stops{ value, label }[]numberStepped slider with labelled positions. Stored value is the underlying number (e.g. 0 = "unlimited").
max_lengthnumberstring/secretServer-enforced length cap (positive).
enumstring[]stringRenders a select; default must be a member.

Cross-field validation enforces min ≤ default ≤ max, default length ≤ max_length, default ∈ enum, and default matching a stops value.

json
{ "sidebar": { "contributes": true, "section": "Chat", "refresh_on": ["text-channels.channel.created"] } }
FieldTypeNotes
contributesbooleanRequired. true if the plugin returns sidebar items.
sectionstringOptional default group name for this plugin's items, used when an item doesn't set its own section.
refresh_onstring[]Event topics that trigger a re-fetch of the plugin's sidebar items.
emits_deltabooleanDeclare true when the backend calls plugin.sidebar.emitDelta; the full sidebar.items response remains authoritative.

The items themselves come from the backend's sidebar.items handler, not the manifest. See Plugin anatomy → reserved actions.

public_schema ​

Declares which of your tables and columns are readable by other plugins (via their data.read capability):

json
{
  "public_schema": {
    "messages": {
      "columns": ["id", "channel_id", "author_id", "content", "created_at"],
      "description": "All messages across all channels."
    }
  }
}

Only listed columns are readable; everything else stays private.

proxy_mounts ​

json
{
  "proxy_mounts": [
    { "name": "demo", "upstream_setting": "demo_upstream_url", "access": "members" }
  ]
}
FieldTypeNotes
namestringSlug-safe, unique within the plugin. Appears in the URL /proxy/\<slug\>/\<name\>/*.
upstream_settingstringKey of a string/secret setting in this same manifest holding the upstream URL. The manifest never carries the URL directly.
access"members" | "owner"Optional, default "members".
max_frame_bytesintegerOptional. Max WebSocket frame (message) size relayed in either direction, in bytes. A larger frame closes the socket with 1009. Default 65536 (64 KiB); raise it for apps that bulk-sync over a socket (e.g. game state). Must be between 1024 (1 KiB) and 16777216 (16 MiB).

Declaring proxy_mounts requires at least one of proxy.http:self / proxy.websocket:self in permissions. Mounts are disabled until an owner approves them. Full guide: Reverse-proxy plugins.

Validation rules (summary) ​

  • At least one of backend / frontend.
  • type: "extension" ⇒ extends present and a valid slug; core/standalone ⇒ no extends.
  • Every permissions entry matches the capability grammar.
  • proxy_mounts[].upstream_setting references a declared string/secret setting; mount names unique; proxy permission present.
  • proxy_mounts[].max_frame_bytes, when present, is an integer in [1024, 16777216].
  • Settings default consistent with type/min/max/max_length/enum/stops.
  • resources.* positive integers; icon ≤ 64 chars; unknown top-level or per-setting fields rejected.
  • sidebar.contributes: true requires a backend — sidebar items are served by the backend's sidebar.items handler, so a frontend-only plugin cannot contribute them (SIDEBAR_REQUIRES_BACKEND).

Manifest v2 ​

  • manifest_version is optional; absent means 1, so every v1 manifest stays valid with no edit. An unrecognized value is rejected rather than guessed by shape.
  • runtime.kind is inferred when absent: a backend block ⇒ sandboxed-backend, frontend-only ⇒ ui-only. Declaring a kind that contradicts the tree — ui-only with a backend, or sandboxed-backend without one — is RUNTIME_KIND_MISMATCH, not a silent override. manifest_version: 2 requires runtime to be explicit.
  • Using any v2 field raises the api_version floor to ^1.1 (V2_REQUIRES_API_FLOOR). Older runtimes reject fields they do not know, and the floor makes them refuse the install up front instead of failing after the server has been stopped and the files copied.
  • Reserved fields fail closed. trust is validated and stored, but a plugin declaring it does not load — the runtime refuses it with CAPABILITY_UNSUPPORTED naming the field and the phase. A capability you declared never silently does nothing.
  • data is stable. It declares the plugin-specific data-directory environment variable and migration strategy so code can be replaced while state remains separate.
  • companions[] is experimental. It is accepted by the runtime and validated against the complete native-supervisor launch contract, including entry point, runtime, health, ports, environment, dependencies and resources. Provisioning remains operator-gated during the preview; an unavailable lane reports a warning instead of silently pretending that the workload started.
  • A provisioned companion receives COMPANION_DATA_DIR, the absolute path of its private durable writable directory. The supervisor owns this value; a manifest cannot redirect it. Everything outside that directory remains read-only under the companion sandbox.
  • Reinstalling an enabled preview plugin refreshes the companion's recorded package/source metadata while retaining its UID, network allocation, private data directory, and current desired lifecycle state.

The tests in packages/shared/src/manifest.test.ts and manifest-schema.test.ts are the exhaustive, executable spec.