Plugins and Extensibility
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
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.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.
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.