convoro.co/llms.txt (or the JSON at
/api/ai/spec.json) and it has everything it needs —
manifest schema, slots, theme tokens, the window.Convoro API, and the publish flow —
to scaffold a correct, installable extension or theme on the first try.
A Convoro extension is a self-contained folder. There is no build step, no Composer requirement, and no dump-autoload — you zip the folder and upload it in Admin → Marketplace, or drop it in the extensions/ directory. Convoro discovers it, autoloads its PHP, runs its migrations on enable, and injects its prebuilt frontend bundle.
my-extension/
├─ extension.json # manifest (required)
├─ src/
│ └─ Extension.php # a Laravel ServiceProvider (optional)
├─ migrations/ # standard Laravel migrations (run on enable)
│ └─ 2026_01_01_000000_create_my_table.php
└─ assets/
├─ forum.js # prebuilt ESM, injected on the forum (optional)
└─ admin.js # prebuilt ESM, injected in admin (optional)
extension.json{
"id": "acme-hello", // unique; folder-safe
"name": "Hello",
"version": "1.0.0",
"description": "Adds a friendly hello.",
"author": "Acme",
"convoro": ">=0.1.0", // version constraint
"type": "extension", // "extension" | "theme"
"namespace": "Acme\\Hello\\", // PSR-4 root for src/
"provider": "Acme\\Hello\\Extension", // a ServiceProvider FQCN (optional)
"migrations": "migrations", // dir to run on enable (optional)
"permissions": [ // merged into the group editor
{ "key": "hello.use", "label": "Use Hello", "category": "Hello", "baseline": true }
],
"settings": [ // rendered as a form on the Marketplace card
{ "key": "greeting", "label": "Greeting", "type": "text", "default": "Hi" }
],
"assets": { "forum": "assets/forum.js" }, // prebuilt bundles to inject
"admin_url": "/admin/ext/hello" // optional management page link
}
Only id and name are required. convoro accepts *, exact versions, >=/<=/>/<, ^ and ~ ranges.
If your extension needs backend behavior, point provider at a normal Laravel ServiceProvider under src/. It boots in the standard lifecycle with full framework access — register routes, bindings, events, anything.
<?php
namespace Acme\Hello;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class Extension extends ServiceProvider
{
public function boot(): void
{
Route::middleware('web')->get('/api/ext/hello', function () {
return response()->json(['message' => 'Hello from an extension!']);
});
}
}
boot() are picked up by route:cache too — they work in production, not just locally.Drop standard Laravel migrations in migrations/. They run automatically when the extension is enabled, and roll back on uninstall. Use Schema::hasTable() guards so re-enabling is safe.
Declare permissions in the manifest and they appear in Admin → Members → groups automatically. Set "baseline": true to grant to every signed-in member. Check them in code with $user->hasPermission('hello.use').
List settings in the manifest and Convoro renders a form when an admin clicks your extension's card. Field type can be text, textarea, boolean, number, select (with options), or color. Read values at runtime:
use App\Support\ExtensionManager;
$greeting = ExtensionManager::setting('acme-hello', 'greeting', 'Hi');
window.ConvoroShip a prebuilt ESM file (no build step on the server) under assets/ and list it in assets.forum / assets.admin. Convoro injects it for enabled extensions. It hooks into the UI through the global window.Convoro API:
const c = window.Convoro;
// Render into a named slot. Provide a mount callback (gets the DOM element),
// an html string, or a Vue component.
c.registerSlot('topic:below', {
ext: 'acme-hello',
mount(el) {
el.innerHTML = '<div>👋 Hello from my extension</div>';
},
});
// Cross-extension event bus
c.on('something', (payload) => { /* … */ });
c.emit('something', { ok: true });
Available slots: header:end (forum header, right side), forum:footer (below every page), topic:below (under the opening post; receives { topicId, slug }). Slots are reactive, so an extension that finishes loading after the page still appears. Your bundle is served from /ext-asset/{id}/forum.
For richer management, register an authenticated route in your provider and point admin_url at it — the Marketplace shows an "Open management page" link. Gate it with the auth + admin middleware:
Route::middleware(['web', 'auth', 'admin'])
->get('/admin/ext/hello', fn () => response('<!doctype html>…'));
Convoro has a Packagist-style registry: you publish an extension by linking its public GitHub repo — no zip uploads, no Packagist account.
extension.json at the repo root.Push a new release and admins just hit Refresh (or re-install) to update. Premium add-ons distribute through the store with a license key instead.
The first-party extensions (Announcement Bar, Horizon, Analytics, and the Members Online / Online Now / Trending Topics widgets) are the best references — each exercises a provider, and variously a migration, a permission, settings/admin pages, and a frontend slot widget.