Abantu

Plugins and Extensibility

How a self-hosted abantu instance can add its own features — a real, working extension point, not just a roadmap idea.
This is the summer 2026 guide.
Some of it has changed since. A new guide (v1) is being written.

Plugins and Extensibility

Abantu has no platform owner, and neither does its code. If your community self-hosts its own instance, you can add features to it — a new kind of signed event, a new tab on the community page — without forking the whole app and without waiting on anyone else's release schedule.

This page is for whoever runs your instance's build, not for everyday members — you won't find a settings toggle here, just a real, working convention with a reference implementation you can copy.

Why this needs to be a real mechanism

Abantu's community state is derived by replaying a signed event log through a fixed set of reducers — one per event type. Historically that set was a single hardcoded switch statement: adding a new kind of event meant editing that file directly, which put every extension behind a fork of the core app.

The same was true of the community page's tabs (Proposals, Roles, Protocols, Projects, Members) — five hardcoded entries with no way to add a sixth without editing the page itself.

Both are now registries instead: a plugin calls a function to add its own event type or its own tab, at your build's startup, and core never has to know the plugin exists.

The two extension points

A new event type. Call registerEventReducer(type, reducer) to teach the community log a signed event type of your own. Your reducer gets state.pluginState — a plain object reserved for plugins, separate from everything core reads and writes — to store whatever your event introduces.
A new community-page tab. Call registerCommunityTab({ id, label, icon, component }) to add a tab alongside the five built-in ones. Your component receives the community's id as a prop, and can read/write state through an injected context — see below — without needing to import anything from inside the abantu app itself.

Both live in @paramira/community-plugin-api, a small package with no abantu-specific code in it — the only thing a plugin package and abantu's own app share.

Reading and writing state from a tab

A plugin's tab component is bundled into abantu's build, but it isn't part of the app's own source tree, so it can't reach abantu's internal composables (~/composables/useCommunity, and so on) the normal way. Instead, abantu provides a small context object your component reads with Vue's inject():

import { inject } from 'vue'
import { PLUGIN_CONTEXT_KEY } from '@paramira/community-plugin-api'

const ctx = inject(PLUGIN_CONTEXT_KEY)

// Read: current derived state for a community, including your pluginState
const state = ctx.getState(communityId)

// Write: append a signed event of a type you've registered a reducer for
await ctx.appendCustomEvent(communityId, 'your:event-type', { ...payload })

That's the entire surface. No other abantu internals are reachable from a plugin, on purpose — the state your reducer writes and the events you append are the only channel.

Versioning

A plugin declares which version of the extension contract it was built against:

const plugin = {
  id: 'your-plugin-id',
  name: 'Your Plugin',
  version: '0.1.0',
  apiVersion: 1, // must match @paramira/community-plugin-api's PLUGIN_API_VERSION
  install() {
    registerEventReducer('your:event-type', yourReducer)
    registerCommunityTab({ id: 'your-tab', label: 'Your Tab', icon: 'i-lucide-star', component: YourTab })
  },
}
export default plugin

installPlugin(plugin) checks apiVersion before ever calling install() — a plugin built against a contract your build has moved past is skipped with a console warning instead of running against assumptions that no longer hold.

Installing a plugin

Everything above is how you write a plugin. Installing one into your own build is one file:

// apps/abantu/app/plugins/community-plugins.client.ts
import { installPlugin } from '@paramira/community-plugin-api'
import yourPlugin from '@your-org/your-plugin'

export default defineNuxtPlugin(() => {
  installPlugin(yourPlugin)
})

Add the plugin package as a dependency, add the import and the installPlugin() call, rebuild. Abantu ships as a static site, so a plugin takes effect at your next build — not at runtime, and not for anyone else's instance.

There is no registry or marketplace of installable plugins yet — for now this is a source-level convention between your build and a plugin package you (or someone you trust) wrote, whether that's a workspace package or a private npm package. packages/example-community-plugin in the paramira monorepo is a complete, tested reference implementation — a "Badges" tab and the event type behind it — built exactly to this contract. Copying its shape is the fastest way to build your own.

What this doesn't cover

This convention is specific to abantu's own app — its community state and its community page. Isoko (the marketplace app) has a comparable extension point for its own scenario automation (see its registerScenarioAction), but it's a separate mechanism in a separate app, since isoko and abantu are different running processes on different devices. A plugin that wants to extend both writes two small packages, one per app, not one that reaches across.

This is also a build-time convention, not a runtime one — a plugin can't be toggled on for a live instance without a rebuild and redeploy. That's a deliberate trade-off for a statically generated app with no server of its own; a dynamic, install-without-rebuilding story is a bigger undertaking left for later, if it ever turns out to be worth the added complexity.