The template plugin
Source: vrcnext-example-plugin
A working VRCNext plugin that uses every capability the host provides, one per file, so you can start from it and delete what you do not need.
Press Use this template on GitHub, or clone it. It installs and runs as-is — enable it and you get a nav tab, a dashboard card, a settings section, context-menu entries, OSC, the game log, notifications and an in-page HTTP route, all of them live.
Making it yours
- Rename it.
id,name,description,authorandhomepageinplugin.json, andidinmain.tsto match — the host refuses to activate a plugin whose two ids disagree. An id is[a-z0-9][a-z0-9-]{1,39}and is also the directory the bridge clones into. - Delete the sections you do not need. Every
install*call inmain.tsis one file undersrc/sections/. Remove the line, remove the file, and remove what it needed frompermissions,eventsandhosts. Declaring less is the point: each category is shown to the user with a risk tone when they enable your plugin. - Trim
src/settings.ts. It demonstrates every setting kind the host can render, which is far more than a real plugin wants. The host derives both the stored type and the control from this schema, so there is no form and no parser anywhere in the plugin. - Make a signing key and sign it. See Signing plugins. The bridge will not install an unsigned repository.
What is in here
| File | Capability |
|---|---|
main.ts |
the manifest of sections; the only file you must edit |
src/settings.ts |
every setting kind, including a list, an embed and a custom control |
src/state.ts |
the session state the sections share, and the one function they log through |
src/sections/events.ts |
typed host events, the untyped escape hatch, settings changes |
src/sections/game-log.ts |
the VRChat game log: live, by type, and the backlog |
src/sections/osc.ts |
OSC in and out, guarded on available |
src/sections/deep-links.ts |
wrld: / usr: links VRCNext delivers |
src/sections/routes.ts |
in-page HTTP routes, reachable from this page and nothing else |
src/sections/context-menu.ts |
items, dividers, submenus, and the clipboard |
src/sections/notifications.ts |
toasts, VRCNext-styled notifications, OS and VR |
src/sections/native.ts |
the bridge: VR overlay and desktop targets, asked for by name |
src/sections/http.ts |
outbound HTTP behind an optional permission |
src/sections/vrchat.ts |
friends, groups, the current instance, avatars — no dialogs |
src/sections/ui-nav-tab.ts |
a whole tab from ctx.ui.kit — the UI file to copy |
src/sections/ui-dashboard.ts |
the same job in plain DOM, plus a modal and an entity picker |
src/sections/ui-settings-section.ts |
a Settings section of the plugin’s own, and sidebar shortcuts |
Everything registered through ctx — listeners, panels, routes, timers on ctx.disposables —
is torn down when the plugin is disabled, and ctx.signal aborts at the same moment. That is
why deactivate is empty.
Working on it
npm install
npm run check # tsc --noEmit && eslint .
The typed API is not on npm: @vrcnext/plugin-api is a file: link to a checkout of
vrcnext-plugin-system beside the
plugin, the layout every plugin in this organisation uses:
projects/
vrcnext-plugin-system/
my-plugin/
Nothing is bundled on your side: the bridge compiles the repository together with the host, so
main.ts is imported as TypeScript straight from the clone. tsconfig.json is the host’s own
strict configuration and eslint.config.mjs mirrors its rules and most of the
source policy — no any, no non-null assertions, no enum, 100 lines per
function, 1000 per file, 4 parameters, depth 3 — so anything check accepts installs. Before
pushing, also run the protocol check, then
sign the release.
The manifest fields are on plugin.json; the refused constructs on Source policy.