Skip to content

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.


"manifest_version": 2

Must 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": "1.0.0"

Semver string. Nexus compares this against the latest GitHub release tag to detect available updates.

"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.


All optional. Not used at runtime — shown on the extension card and admin detail page.

FieldDescription
descriptionOne-sentence summary. Truncated past ~200 chars in card views.
authorFree-form. GitHub username by convention.
homepageCanonical URL. Must start with http://, https://, or /.
repositoryExplicit GitHub URL. Used for release polling when homepage points elsewhere.
licenseFree-form. SPDX identifiers (MIT, Apache-2.0) recommended.
tags1–4 short strings shown as pills on the extension card.
logo_urlSquare icon. 200×200 px PNG or WebP. Reference as /ext/<slug>/assets/logo.webp.
banner_urlWide hero. 800×400 px PNG/WebP/JPEG. Reference as /ext/<slug>/assets/banner.webp.
compatible_withSemver range (e.g. "^1.0"). Currently informational.
"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_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:

TypeForm controlStored as
stringSingle-line text inputstring
textMulti-line textareastring
booleanToggle switchboolean
numberNumeric inputnumber
selectDropdown — requires optionsstring
colorHex color picker#RRGGBB string

Common attributes:

AttributePurpose
labelForm label. Defaults to the field key with underscores replaced by spaces.
defaultInitial value. Type-appropriate literal.
descriptionHelper text under the form control.
secretRenders as a masked password input. Value stored plaintext — masking is UI-only.
requiredBlock save while empty.
placeholderPlaceholder text for empty inputs.
optionsRequired for select. Array of {"value": "...", "label": "..."} objects.
"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.


"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": [
{
"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": [
{"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": [
{
"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 chars
  • label and description — shown in the user’s notification preferences page
  • icon — optional, short-form Font Awesome class, defaults to fa-bell
  • channels — non-empty list from ["web", "email", "push"]
  • default_preferences — map of channel → boolean; defaults: web on, others off
  • payload_schema — declared fields are required at send time
"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": [
{"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 chars
  • default — one of "everyone", "member", "moderator", "admin". Defaults to "member" if omitted.

The four role tiers:

TierWho passes
everyoneGuests and members — but only if site-wide guest browsing is enabled. With guest browsing off, even everyone requires a logged-in user.
memberAny logged-in user
moderatorModerators and admins
adminAdmins 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.


"slots": ["post_footer", "profile_sidebar", "compose_attachments"]

Array of slot names to fill with React components registered via registerSlot. Available slots:

SlotWhere it rendersProps
post_footerBelow post body on /post/:id, above replies{ post_id }
profile_sidebarLeft rail of /profile/:username{ username, current_user }
compose_attachmentsBelow post body in the composer, above footer bar. Post composer only — not the reply composer.{ attachments, set_attachments }
"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": {
"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": {
"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": [
{
"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": [
{
"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.
  • iconfull 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 — only id is 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": [
{"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_only hides 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.