Vayu Compatibility Bible (VCB) — VayuPress
Status: Authoritative
Last reviewed: 2026-07-16
What this is
The Vayu Compatibility Bible is the complete, machine-checkable contract for
building plugins, themes, and tools that work with VayuPress —
first time, every time, whether the author is a developer or an AI agent. It
exists because "compatibility" must not depend on tribal knowledge: every rule
in this document is enforced by code that ships inside VayuPress
(internal/vcb), and the vayu-compat CLI applies exactly the same code, so
a package that validates cleanly cannot drift from what the host enforces.
Three principles govern everything below:
- Declare everything, receive nothing implicitly. An extension states its identity, host range, hooks, API permissions, and sandbox capabilities in a manifest. Anything not declared is denied.
- Fail closed, fail early. Unknown manifest fields are parse errors, not silent no-ops. Unknown hooks, capabilities, option keys, and categories are validation errors — before install, not surprises after.
- One source of truth. Vocabularies (hooks, capabilities, theme options, categories) are exported from the code that enforces them and re-checked in CI. This document describes them; it never overrides them.
Related: /docs/compatibility/vayuapi (API keys, permissions, rate limits) · /docs/compatibility/stability-matrix · /docs/compatibility/api-contracts · /docs/adr/ADR-0135-vayu-compatibility-bible
Extension kinds
| Kind | Ships as | Runtime contract | Manifest |
|---|---|---|---|
| Plugin | a sandboxed executable + plugin.json |
JSON-per-line IPC over stdin/stdout; capability-scoped subprocess | plugin.json (this doc, §Plugin manifest) |
| Theme | design tokens + theme.json |
compiled to CSS variables by the host theme compiler | theme.json (this doc, §Theme manifest) |
| Tool | a standalone binary (see tools/*) |
reads/writes the VayuPress SQLite database and file formats, or calls the HTTP API with a scoped key | none required; API tools declare permissions like plugins |
Plugin manifest (plugin.json)
A plugin package carries a plugin.json at its root. Unknown fields are
rejected (a typo can never silently drop a declaration). Maximum size
256 KiB.
{
"vcb": 1,
"name": "seo-stamp",
"version": "1.2.0",
"description": "Stamps SEO metadata after publish.",
"author": "Example Dev",
"license": "MIT",
"min_host": "3.13.40",
"max_host": "",
"hooks": ["article.create", "article.update"],
"api_permissions": ["posts:read", "posts:write"],
"executable": "bin/seo-stamp",
"executable_sha256": "<64-hex sha-256 of the binary>",
"sandbox": {
"allow_network": false,
"allowed_read_paths": ["/data/public"],
"allowed_write_paths": [],
"timeout_ms": 2000,
"max_restarts": 3,
"max_message_bytes": 1048576,
"memory_max_bytes": 134217728,
"cpu_quota_percent": 50,
"max_pids": 16,
"run_as": "1000:1000"
},
"distribution": {
"download_url": "https://example.com/seo-stamp-1.2.0.bin",
"sha256": "<64-hex sha-256 of the artifact>"
}
}
Field rules (each is checked by vayu-compat and the host):
| Field | Rule |
|---|---|
vcb |
Must equal the host's manifest schema version (currently 1). A higher version is refused outright — never partially interpreted. |
name |
Required. ≤ 100 chars, no path separators. The registry keys packages by name@version. |
version |
Required. Dotted numeric (1.2.0). |
min_host / max_host |
Optional VayuPress version bounds. The validator refuses a package whose range excludes the running host. |
hooks |
Every entry must be in the hook catalogue below. A name outside it would never fire. |
api_permissions |
Every entry must be a valid, non-wildcard section:action (see /docs/compatibility/vayuapi). *:* and section:* are refused — least privilege is the contract. |
executable |
Required. Relative path inside the package; absolute paths and .. traversal are refused. |
executable_sha256 |
Optional but strongly recommended. 64-hex SHA-256; the sandbox verifies the binary before every launch and refuses a mismatch. |
sandbox.* |
Deny-by-default capability requests, mapped 1:1 onto the runtime sandbox: read/write paths must be absolute prefixes; run_as must be numeric uid:gid; limits must be in sane bounds. Network access and write paths are surfaced to the operator as warnings. |
distribution |
Optional. download_url must be HTTPS; sha256 is verified on install. |
Sandbox defaults when omitted: no filesystem access, no network, 2 s hook timeout, 3 restarts before quarantine, 1 MiB IPC message cap, own PID and IPC namespaces, parent environment never inherited.
Hook catalogue
The host fires exactly these hooks through the plugin manager. This is a
closed, enumerated set (internal/vcb/hooks.go); the validator refuses a
manifest subscribing to anything else, because an uncatalogued name silently
never fires.
| Hook | Fired when | Payload keys |
|---|---|---|
article.create |
An article was created | slug, id |
article.update |
An article was updated | slug |
article.delete |
An article was deleted | slug, id |
cache.purge |
The page cache was purged from the admin | purge_type, slug, purged_count |
Historical note. Older drafts of the plugin SPEC and examples referred to
articles.writeandarticle.created.v1as hook names. Neither has ever been fired by the host; both were corrected when VCB formalised the catalogue. If you built against those names, resubscribe to the table above.
Payload schemas are additive: the host may add keys; a plugin must ignore unknown keys. Delivery is asynchronous and lossy under overload (a full queue drops the event); a hook that fails five times in a row is auto-disabled.
The IPC wire contract (one JSON Request per line on stdin, one Response
per line on stdout, with hook, payload, correlation_id, causation_id,
trace_id, capabilities request keys) is a Stable contract, frozen by
golden test and documented in
/docs/compatibility/api-contracts.
Separate vocabularies that are not plugin hooks: outbound webhook
events (article.created.v1, article.updated.v1, article.deleted.v1,
payment.order_created.v1, payment.completed.v1, or *) delivered as
HMAC-signed HTTP POSTs, and the internal outbox event types. They share domain
facts with hooks but keep their own versioned spellings.
Theme manifest (theme.json)
A theme package carries a theme.json wrapping the exact token struct the
host compiler consumes — what validates is literally what runs.
{
"vcb": 1,
"tokens": {
"Name": "Nightfall",
"BgDark": "#0b1020", "SurfaceDark": "#141a2e", "TextDark": "#e2e8f0",
"MutedDark": "#8ba4c4", "AccentDark": "#0ea5e9", "Accent2Dark": "#a78bfa",
"HiDark": "#fbbf24", "GreenDark": "#34d399",
"BgLight": "#ffffff", "SurfaceLight": "#f1f5f9", "TextLight": "#0f172a",
"MutedLight": "#475569", "AccentLight": "#0284c7", "Accent2Light": "#7c3aed",
"HiLight": "#b45309",
"FontSans": "Inter, sans-serif",
"FontMono": "JetBrains Mono, monospace",
"FontSizeBase": "1rem", "LineHeight": "1.6",
"MaxWidth": "72rem", "RadiusSm": "6px", "RadiusLg": "14px",
"custom_css": ".hero { letter-spacing: 0.02em; }",
"options": { "width": "wide", "corners": "soft" }
},
"meta": {
"name": "Nightfall",
"tagline": "A deep-blue night theme.",
"description": "…",
"tags": ["dark", "blue"],
"category": "Dark"
}
}
Theme rules — each one lifted from what the compiler actually does at apply time:
| Rule | Why |
|---|---|
The 15 colour tokens must be empty or #hex (#rgb … #rrggbbaa). |
A bad colour aborts the whole compile — the theme simply fails to apply. |
FontSizeBase, LineHeight, MaxWidth, RadiusSm, RadiusLg must be a number with an optional unit (rem em px ch % vw vh). |
Invalid dimensions are silently dropped by the compiler; the validator turns that silence into an error. |
Font stacks must avoid \ ; : ( ) { } < > and non-ASCII. Quotes are allowed — the compiler strips them harmlessly (unquoted multi-word family names are valid CSS). |
Other stripped characters change what renders. |
Every options key must be in the option schema, every value in that option's choices. |
Unknown keys/values are silent no-ops at apply time. Keys: archetype, fontpair, scheme, width, corners, feedlayout, headeralign, headingcase, accentfill, herostyle, herobg, heroheight, navstyle, cardstyle, linkstyle, articlealign, articlemeta, relatedposts, trendingposts, authorbox, cardimage (+ per-theme extras). Run vayu-compat capabilities/hooks or read the design studio for choice values. |
custom_css: no @import, no external url() (http/https///), no <, ≤ 64 KiB (warning at 16 KiB). |
The site runs CSP style-src 'self' and promises zero third-party fetches; the stylesheet is inlined into every page load on low-resource hosts. |
meta.name must equal tokens.Name; meta.category must be one of Flagship, Dark, Light, Minimal, Reading, Editorial, Newsletter, Business, Community, Creative, Vibrant, Mono. |
Deploying a store entry applies the preset by that exact name; the store filters by the fixed category set. |
| The name must not collide with a built-in preset. | The catalogue is keyed by name. |
Every built-in preset passes these rules — enforced by test
(TestBuiltinPresetsSatisfyThemeContract), so the published contract and the
shipped themes can never disagree.
Motion & elevation tokens (build on the shared system)
Every compiled theme (/theme.css) exposes a shared, sovereign motion and
elevation vocabulary (ADR-0136) that your custom_css can use directly — the
same premium primitives the VayuOS admin uses, with zero build step and no
dependency. Prefer these over hand-typed durations and shadows so your theme
feels consistent with the platform and honours reduced-motion automatically.
| Token (bare alias) | What it is | Example |
|---|---|---|
--sh-sm / --sh / --sh-lg |
Scheme-adaptive elevation (subtler on dark, more present on light) | box-shadow: var(--sh) |
--ease / --ease-out / --ease-spring |
Easing curves | transition-timing-function: var(--ease) |
--dur-fast / --dur / --dur-slow |
Durations (90 / 160 / 280 ms) | transition-duration: var(--dur) |
--t / --t-fast / --t-slow / --t-spring |
Composed duration + easing (drop into a transition) |
transition: box-shadow var(--t) |
--vp-sp-1 … --vp-sp-12 |
Spacing scale (rem) | padding: var(--vp-sp-4) |
--vp-z-base … --vp-z-toast |
z-index scale | z-index: var(--vp-z-modal) |
(The --vp-*-prefixed canonical names are also available, e.g. --vp-sh,
--vp-ease-spring.) Example:
/* in a theme's custom_css */
.post-card { transition: transform var(--t), box-shadow var(--t); }
.post-card:hover { transform: translateY(-2px); box-shadow: var(--sh); }
Motion is enhancement, never requirement: the public stylesheet already carries
a global prefers-reduced-motion guard, and native page View Transitions
(@view-transition { navigation: auto }) are enabled site-wide — same-origin
navigation crossfades with zero JavaScript and degrades cleanly where
unsupported. You do not need to add either; just build on the tokens.
The validator catches token typos. These --vp-* names are the sovereign
vocabulary the compiler owns — a theme never defines them. If your custom_css
references a --vp-* token the compiler does not emit and gives no fallback
(e.g. var(--vp-shdw-lg)), it would silently resolve to nothing; vayu-compat
flags it as a warning so you catch the misspelling before shipping. A reference
with a fallback (var(--vp-x, 0)) is always accepted. The flagship themes
themselves are built on this vocabulary, as reference examples.
API permissions and key provisioning
Extensions that call the VayuPress HTTP API declare the capabilities they need
as section:action tokens (full vocabulary and semantics:
/docs/compatibility/vayuapi).
The provisioning flow:
- The manifest declares, e.g.
"api_permissions": ["posts:read", "posts:write"]. vayu-compat(and the host) refuse unknown tokens and any wildcard — an extension may never ask for*:*.- The operator mints a key in VayuOS → API Keys, ticking exactly the declared boxes (the 12 × 6 permission grid). The key can do nothing else.
- At runtime, every call is checked against the route's required capability, metered against the key's per-minute budget, and appended to the tamper-evident audit log.
Security rules (non-negotiable)
- No external fetches, ever. CSP is
style-src 'self'; themes cannot@importor reference third-party hosts; plugin network access must be declared and is surfaced to the operator. - Integrity is verified, not trusted. Plugin binaries are SHA-256-checked before every launch; distribution artifacts are hash-verified on install over HTTPS only.
- Least privilege. Deny-by-default sandbox (no FS, no network, own PID/IPC namespaces, no inherited environment); deny-by-default API keys; wildcards refused in manifests.
- Zero telemetry. An extension must not phone home. Analytics stay on the operator's box.
Performance rules
- Themes: CustomCSS ≤ 64 KiB hard, ≤ 16 KiB advised — it ships inline on every page load.
- Plugins: declare
memory_max_bytes(warning if you don't); default hook timeout is 2 s; the IPC message cap defaults to 1 MiB. VayuPress targets low-resource VPSes — design for them. - API calls: default budget 600 requests/minute per key (per-key override at
mint time). Handle
429+Retry-After.
Validating a package
cd tools/vayu-compat && go build -o vayu-compat ./cmd/vayu-compat
./vayu-compat check --manifest ./my-plugin/plugin.json --host 3.13.41 --files
./vayu-compat check --manifest ./my-theme/theme.json
Exit code 0 = compatible; 1 = at least one ERROR finding. Every finding
carries a stable machine code (theme.color.invalid,
plugin.hook.unknown, …) suitable for CI annotations. The same checks run in
this repository's CI gate (scripts/preflight-ci.sh, section 11), so the
contract cannot rot.
Versioning and stability
"vcb": 1is the manifest schema version. The host refuses higher versions outright (fail-closed); additive fields within a version keep old manifests valid.min_host/max_hostexpress host compatibility; versions compare numerically part by part (3.13.41).- The plugin IPC wire shape, the permissions JSON envelope, and the signed- article schema are Stable contracts under /docs/compatibility/stability-matrix: changing one requires an RFC + a deliberate golden update.
Known limitations (deliberately deferred)
Documented so nobody mistakes silence for a promise — see ADR-0135 for the reasoning:
- The event namespaces are not unified. Plugin hooks (
article.create), webhook events (article.created.v1), outbox envelope types, SSE broadcast names, and the typed in-process buses are separate vocabularies describing overlapping facts. VCB catalogues each honestly rather than pretending one name space exists. - Event payload schema validation is shallow. The JSON-schema registry
checks strings/integers/booleans and required-field presence only; arrays,
objects and
date-timeformats are declared but not enforced (and it is not wired into the emit paths). - In-process hooks have no capability model. Only subprocess plugins carry sandbox capabilities; Go code registered in-process runs with full process privileges. Third-party code must ship as a sandboxed subprocess — the in-process registry is for first-party wiring.
Development process
- Follow the governance constitution: zero telemetry, minimal dependencies, sovereignty by default.
- Validate with
vayu-compatlocally and in your CI before publishing. - Changing a Stable interface needs an RFC (see the stability matrix); adding a hook, option, or capability is additive and needs a catalogue entry plus documentation.