Manifest reference
The manifest.json is the contract between your extension and Nexus. Every surface your extension contributes must be declared here before Nexus will wire it up. The manifest is validated at install time — unknown or malformed fields cause the install to fail with specific error messages.
Nexus serves the full JSON Schema at /manifest_schema.json on every instance. Add "$schema": "https://your-nexus-host/manifest_schema.json" to your manifest for live editor validation.
Required fields
Section titled “Required fields”manifest_version
Section titled “manifest_version”"manifest_version": 2Must be 2. The only currently-accepted version.
"name": "My Extension"Display name. Shown on the extension card, admin lists, and the admin panel title.
"slug": "my-extension"Machine identifier. Must match ^[a-z0-9-]+$ and be unique across all extensions on the instance. Used as the URL prefix (/ext/<slug>/...), asset directory name, and internal lookup key. Cannot be renamed after install without a full uninstall and reinstall.
version
Section titled “version”"version": "1.0.0"Semver string. Nexus compares this against the latest GitHub release tag to detect available updates.
module
Section titled “module”"module": "MyExtension"The Elixir module name that implements Nexus.Extensions.Behaviour. Must be a valid Elixir module name (starts with uppercase, letters/digits/underscores/dots). The loader finds the module by this exact name after compilation.
Metadata fields
Section titled “Metadata fields”All optional. Not used at runtime — shown on the extension card and admin detail page.
| Field | Description |
|---|---|
description | One-sentence summary. Truncated past ~200 chars in card views. |
author | Free-form. GitHub username by convention. |
homepage | Canonical URL. Must start with http://, https://, or /. |
repository | Explicit GitHub URL. Used for release polling when homepage points elsewhere. |
license | Free-form. SPDX identifiers (MIT, Apache-2.0) recommended. |
tags | 1–4 short strings shown as pills on the extension card. |
logo_url | Square icon. 200×200 px PNG or WebP. Reference as /ext/<slug>/assets/logo.webp. |
banner_url | Wide hero. 800×400 px PNG/WebP/JPEG. Reference as /ext/<slug>/assets/banner.webp. |
compatible_with | Semver range (e.g. "^1.0"). Currently informational. |
js_bundle
Section titled “js_bundle”"js_bundle": "my-extension.js"Optional. Filename of the JavaScript bundle relative to priv/static/. The file is copied to the extension’s assets directory at install time and injected into every Nexus page as a <script> tag. Omit for server-only extensions.
Settings
Section titled “Settings”settings_schema
Section titled “settings_schema”"settings_schema": { "api_key": { "type": "string", "label": "API key", "secret": true, "description": "Your API key from the service dashboard." }, "enabled": { "type": "boolean", "label": "Enable feature", "default": false }}A map from field key to field definition. Keys match what you read in Elixir callbacks as settings["key"].
Field types:
| Type | Form control | Stored as |
|---|---|---|
string | Single-line text input | string |
text | Multi-line textarea | string |
boolean | Toggle switch | boolean |
number | Numeric input | number |
select | Dropdown — requires options | string |
color | Hex color picker | #RRGGBB string |
Common attributes:
| Attribute | Purpose |
|---|---|
label | Form label. Defaults to the field key with underscores replaced by spaces. |
default | Initial value. Type-appropriate literal. |
description | Helper text under the form control. |
secret | Renders as a masked password input. Value stored plaintext — masking is UI-only. |
required | Block save while empty. |
placeholder | Placeholder text for empty inputs. |
options | Required for select. Array of {"value": "...", "label": "..."} objects. |
settings_tabs
Section titled “settings_tabs”"settings_tabs": [ { "key": "credentials", "label": "Credentials", "icon": "fa-key", "fields": ["api_key"] }, { "key": "behavior", "label": "Behavior", "fields": ["enabled"] }]Groups settings into tabs. Each tab has a key (unique, used as URL fragment), label, optional icon (short-form Font Awesome class), and fields (array of keys from settings_schema). If omitted, all settings render on a single untabbed page.
Backend surfaces
Section titled “Backend surfaces”"hooks": [ {"event": "post_created", "priority": 50}, "user_registered"]Events to subscribe to. Each entry is a string event name or an object with event and optional priority (default 50). Lower priority numbers run first. Declaring any hook requires handle_event/3 to be exported.
Available events: post_created, post_updated, post_deleted, reply_created, reply_deleted, reaction_added, reaction_removed, report_created, report_resolved, user_registered, user_login.
digest_sections
Section titled “digest_sections”"digest_sections": [ { "key": "my_section", "label": "My Section", "icon": "fa-chart-bar", "enabled_by_default": true }]Sections contributed to digest emails. Each has a key (passed to your callback), label (shown in Admin → Digest), optional icon (short-form Font Awesome class), and optional enabled_by_default (defaults to false). Declaring any section requires handle_digest_section/3 to be exported.
side_data
Section titled “side_data”"side_data": [ {"entity": "post", "kind": "my_attachment"}, {"entity": "reply", "kind": "my_reply_attachment"}]Declares composer attachment types your extension owns. entity is "post", "reply", or "user". kind is a free-form string — namespace it with your slug to avoid collisions. Only one extension can own a given {entity, kind} pair. Declaring any side_data requires persist_attachment/3 to be exported.
notification_types
Section titled “notification_types”"notification_types": [ { "key": "my_notif", "label": "My notification", "description": "Fires when...", "icon": "fa-bell", "channels": ["web", "email"], "default_preferences": {"web": true, "email": false}, "payload_schema": {"post_id": "The post that triggered this"} }]Declares notification categories. Each has:
key— matches^[a-z][a-z0-9_]*$, max 64 charslabelanddescription— shown in the user’s notification preferences pageicon— optional, short-form Font Awesome class, defaults tofa-bellchannels— non-empty list from["web", "email", "push"]default_preferences— map of channel → boolean; defaults: web on, others offpayload_schema— declared fields are required at send time
capabilities
Section titled “capabilities”"capabilities": []Forward-compatible declarations of privileged operations. Currently informational — unknown values produce warnings but don’t block install. Declare anything your extension does that may need capability gating in future Nexus releases.
permissions
Section titled “permissions”"permissions": [ {"key": "can_view_gallery", "label": "Can view the gallery", "default": "everyone"}, {"key": "can_upload_image", "label": "Can upload an image", "default": "member"}, {"key": "can_manage_gallery", "label": "Can manage the gallery", "default": "moderator"}]Access gates enforced by Nexus.Extensions.Permissions.check/3 in your Elixir code. Each entry has:
key— matches^[a-z0-9_]+$, max 64 chars. Also the storage key in the settings column.label— shown to the admin on the Permissions page, max 120 charsdefault— one of"everyone","member","moderator","admin". Defaults to"member"if omitted.
The four role tiers:
| Tier | Who passes |
|---|---|
everyone | Guests and members — but only if site-wide guest browsing is enabled. With guest browsing off, even everyone requires a logged-in user. |
member | Any logged-in user |
moderator | Moderators and admins |
admin | Admins only |
Group-aware gates: Admins configure permission gates from the Permissions page using a gate picker that lets them select both a role tier and zero or more custom groups. Access is granted when either the user’s role meets the required tier or the user belongs to any of the selected groups. Your extension code calls Permissions.check/3 exactly as before — the group evaluation happens inside the check function automatically.
The default field in your manifest sets the initial role tier shown in the picker before the admin has saved any custom configuration. Groups are never part of the manifest default.
Frontend surfaces
Section titled “Frontend surfaces”"slots": ["post_footer", "profile_sidebar", "compose_attachments"]Array of slot names to fill with React components registered via registerSlot. Available slots:
| Slot | Where it renders | Props |
|---|---|---|
post_footer | Below post body on /post/:id, above replies | { post_id } |
profile_sidebar | Left rail of /profile/:username | { username, current_user } |
compose_attachments | Below post body in the composer, above footer bar. Post composer only — not the reply composer. | { attachments, set_attachments } |
routes
Section titled “routes”"routes": [ {"path": "/", "title": "Home"}, {"path": "/games/:slug", "title": "Game details"}]SPA page routes. path is relative to your extension’s namespace — do not include /ext/. Nexus prefixes it automatically. title is optional — shown in the back-header when the route is active. Bound to components via registerRoute.
admin_panel
Section titled “admin_panel”"admin_panel": { "label": "My Extension", "icon": "fa-cog"}Adds a page to the admin sidebar under “Installed extensions”. icon takes the short-form Font Awesome class. Bound to a component via registerAdminPanel. Use settings_schema with the auto-rendered fallback form for extensions that only need settings — admin panels are for custom content the host can’t render.
explore
Section titled “explore”"explore": { "label": "My Extension", "icon": "fa-puzzle-piece", "path": "/"}Adds one entry to the Explore section of the left sidebar. path defaults to "/". Additional Explore entries can be registered from the JS bundle. Bound via registerExploreItem.
right_widgets
Section titled “right_widgets”"right_widgets": [ { "id": "my-widget", "label": "My Widget", "scope": "extension", "priority": 50 }]Right sidebar panels. Each has:
id— unique within your extension. Convention: prefix with your slug.label— shown in Admin → Layout → Right sidebar, not on the widget itself.scope— where the widget appears. Options:"extension"(your pages only, default),"global"(everywhere),{"path": "/x"}(a specific path),{"corePages": [...]}(specific core pages). Core page names:feed,post,profile,members,leaderboard,badges,search,notifications,messages,saved,drafts.priority— lower numbers render higher up. Default 50.
Bound to components via registerRightWidget.
toolbar_buttons
Section titled “toolbar_buttons”"toolbar_buttons": [ { "id": "my-button", "icon": "fa-solid fa-flask", "tip": "My button tooltip", "scope": "both", "priority": 50 }]Buttons in the post and reply composers. Each has:
id— unique within your extension.icon— full Font Awesome class with style prefix (e.g."fa-solid fa-flask"). This differs from all other surfaces which use the short form. Mixing them up renders as plain text.tip— tooltip text. Can be changed freely without breaking saved admin layouts — onlyidis stable.scope—"both"(default),"posts", or"replies".priority— lower numbers render earlier among extension buttons. Default 50.
Bound to click handlers via registerToolbarButton.
profile_tabs
Section titled “profile_tabs”"profile_tabs": [ {"id": "my-tab", "label": "My Tab", "icon": "fa-star", "visibility": "always"}, {"id": "my-own-tab", "label": "My Private Tab", "icon": "fa-lock", "visibility": "own_only", "priority": 60}]Additional tabs on user profile pages. Each has:
id— unique within your extension.label— shown in the tab bar.icon— optional, short-form Font Awesome class.visibility—"always"(default) or"own_only".own_onlyhides the tab button when the viewer is not the profile owner. This is a UX hint, not access control — your component must enforce access server-side.priority— lower numbers render earlier. Default 50.
Bound to components via registerProfileTab.