Tribes Plugin Template
DEPRECATED! Use the generator (tribes-plugin-new) instead.
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, smoke checks, and tests:
mix deps.get
mix tribes.plugin.validate
scripts/plugin smoke
scripts/plugin test
If you are using the shared tribes devenv, run through the local helper instead:
devenv shell -- plugin validate
devenv shell -- plugin smoke
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 smoke
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. The default template uses TypeScript under assets/ts.
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/ # TS/CSS (one bundle per plugin)
│ ├── ts/ # TypeScript browser entry points
│ ├── package.json # Optional build script used by dev + Guix packaging
│ ├── tsconfig.json # TypeScript compiler settings
│ └── 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:
{
"name": "your_plugin",
"entry_module": "Tribes.Plugins.YourPlugin.Plugin",
"host_api": "1",
"otp_app": "your_plugin",
"provides": ["some_capability@1"],
"requires": ["ui@1"],
"enhances_with": ["inference@1"]
}
- entry_module — must be
Tribes.Plugins.*.Plugin - otp_app — required and must match
name - provides — capabilities this plugin makes available
- requires — hard plugin/API contracts beyond the
host_apifoundation (build fails without them) - enhances_with — optional dependencies (plugin degrades gracefully)
The default template includes ui@1 because the generated LiveView renders
inside the host chrome through Tribes.Plugin.Layouts.app. Keep ui@1 when a
plugin uses host chrome, imports Tribes.UI.Components, or uses use Tribes.UI:
"requires": ["ui@1"]
host_api is the versioned foundation contract. It provides the supported
Phoenix, Ash, PubSub, data, and cluster-event APIs for plugins. Do not add
separate framework capability requirements for APIs that belong to that
foundation.
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. Render normal plugin pages with
Tribes.Plugin.Layouts.app so they live inside the Tribes chrome/navigation;
only omit it for intentionally unwrapped fullscreen surfaces.
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 smoke
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
devenv shell -- 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.