Provide a Nix glibc locale archive with en_GB.UTF-8 so tools inside the devenv shell inherit a valid UTF-8 locale.
Tribes Plugin Template
Template for creating Tribes plugins.
Getting Started
- Click "Use this template" on GitHub to create your own repo
- Clone and rename:
git clone https://github.com/you/your-plugin.git
cd your-plugin
./scripts/rename.sh your_plugin YourPlugin
- Edit
manifest.json— set description, capabilities, requirements - Implement your plugin in
lib/your_plugin/plugin.ex - Run validation and tests:
mix deps.get
mix tribes.plugin.validate
scripts/plugin test
If you are using the shared tribes devenv, run through the local helper instead:
devenv shell -- plugin validate
devenv shell -- plugin test
devenv shell -- plugin precommit
Development
For local development alongside a Tribes checkout:
# Symlink into the host plugins directory once
cd /path/to/tribes
ln -s /path/to/your-plugin plugins/your_plugin
# Start Tribes dev server
iex --sname dev -S mix phx.server
Then edit the plugin in its own repo. In development, the host now watches symlinked external plugins and automatically:
- runs
mix compilefor Elixir/HEEx or manifest changes - runs
npm run build --prefix assetswhenassets/package.jsonis present - reloads the plugin in the running Tribes VM
- triggers a Phoenix browser reload after the rebuild finishes
That means the normal edit/compile loop is:
cd /path/to/your-plugin
mix deps.get
# in another terminal
cd /path/to/tribes
iex --sname dev -S mix phx.server
Inside the plugin repo's devenv shell, the plugin helper forwards to the host devenv automatically:
plugin validate
plugin test
plugin precommit
If your plugin does not need a custom frontend pipeline, you can skip
assets/package.json and write browser-ready files directly under assets/js
and assets/css; the host dev watcher will copy them into priv/static for
you in development.
Project Structure
your_plugin/
├── manifest.json # Plugin metadata (Nix build + runtime)
├── mix.exs # Dependencies
├── config/ # Host-backed test config
├── lib/
│ ├── your_plugin/
│ │ ├── plugin.ex # Tribes.Plugin entry point
│ │ └── application.ex # OTP supervision tree (optional)
│ └── your_plugin_web/
│ └── live/ # LiveView pages
├── assets/ # JS/CSS (one bundle per plugin)
│ ├── package.json # Optional build script used by dev + Guix packaging
│ └── package-lock.json # Optional, recommended for reproducible builds
├── priv/
│ ├── static/ # Built assets for release
│ └── repo/migrations/ # Ecto migrations
├── scripts/
│ ├── plugin # Shared devenv/non-devenv test wrapper
│ └── rename.sh
└── test/
Manifest
manifest.json declares your plugin's identity and capabilities:
{
"id": "org.example.plugins.your-plugin",
"slug": "your_plugin",
"display_name": "Your Plugin",
"entry_module": "TribeOne.TribesPlugin.YourPlugin.Plugin",
"host_api": "1",
"otp_app": "your_plugin",
"provides": ["some_capability@1"],
"requires": ["org.tribe-one.caps.ui@1"],
"enhances_with": ["org.tribe-one.caps.inference@1"]
}
- entry_module — must be
<Owner>.<Plugin>.Plugin - otp_app — required, vendor-prefixed Mix application name
- provides — capabilities this plugin makes available
- requires — hard dependencies (build fails without them)
- enhances_with — optional dependencies (plugin degrades gracefully)
See the Plugin System docs for the full specification.
The default template assumes the plugin owns a top-level page such as /your_plugin
and any views beneath it.
Sender renders its LiveView pages inside the host chrome through
Tribes.Plugin.Layouts.app, so org.tribe-one.caps.ui@1 is a hard runtime dependency.
LiveView Hooks
External plugins register browser hooks from JS bundles listed in
manifest.json assets.global_js:
window.TribesPluginHooks = window.TribesPluginHooks || {};
window.TribesPluginHooks.SenderHook = {
mounted() {}
};
The host loads plugin JS before connecting LiveSocket and does not auto-import
external plugin phoenix-colocated/<otp_app> modules into its app.js bundle.
Plugin Data
If your plugin adds Ash resources that should replicate cluster-wide, use
extensions: [AshNostrSync] on those resources and follow the host defaults:
- persisted attributes sync by default unless explicitly excluded
- synced resources should use one UUID primary key by default
AshNostrSyncrequires exactly one primary key; composite keys are not supported- synced resources get extension-managed
deleted_atsoft-delete support by default - destroy actions can use
soft_delete()when they should tombstone rows soft_delete?(false)is the opt-out if a synced resource should not use the default tombstone model
Keep resource-specific side effects explicit in actions. AshNostrSync owns the
default tombstone/filter/projection behavior, but your plugin still decides which
actions are semantic deletes and what else they should do.
Testing
The template starts with two host-backed test layers:
- Contract tests (
test/your_plugin/plugin_contract_test.exs) — manifest and runtime spec stay aligned - Page tests (
test/your_plugin/home_page_test.exs) — the plugin renders through the real host page pipeline
Run them with:
mix tribes.plugin.validate
scripts/plugin test
Use plugin test / plugin precommit for host-backed tests. The helper invokes
Mix with the host database/services and host plugin-manager paths, including
built-in providers such as tribes_ui. Raw mix test in a plugin checkout is
guarded and prints this guidance; mix raw_test / mix raw_precommit are
available only for unusual manual debugging when you have set the same host
environment yourself.
Building for Release
MIX_ENV=prod mix compile
npm run build --prefix assets
mkdir -p dist/your_plugin
cp -r _build/prod/lib/your_plugin/ebin dist/your_plugin/
cp -r priv dist/your_plugin/
cp manifest.json dist/your_plugin/
For Guix-based deployment, package your plugin in the guix-tribes channel and
enable it from the node config.
Licence
TODO: Choose a licence.