Creating Add-ons
Introduction
Everything NiceTryBot's own add-ons can do, yours can too. An add-on is a Composer package with a service provider. It may add pages to the sidebar and settings, panels to a domain's page, obstacles to the gym, fields to the rule language, columns to Activity, and tools to the MCP server, and it can say how long the visitor data it keeps should live.
If you have written a Laravel package before, you will feel right at home. NiceTryBot's control panel is a Laravel application, and add-ons are discovered using Laravel's package discovery.
Creating Add-ons Using AI
The extension points below are regular and well-defined, which makes add-ons a natural job for a coding agent. Open your agent in a checkout of NiceTryBot and give it a prompt like the following:
Read https://docs.nicetrybot.com/docs/0.x/creating-addons.md, then study packages/privacy-mode in this repository as a working example. Help me build a NiceTryBot add-on in packages/ that does the following: ...
Follow the add-on conventions exactly, write Pest tests, and run them before you tell me it's done.
Local Development
To develop an add-on, clone NiceTryBot and set it up locally. You will need PHP 8.5, Composer, and Node 22:
git clone https://github.com/NiceTryBot/nicetrybot.git
cd nicetrybot
composer setup
composer dev
NiceTryBot's root composer.json file already includes a path repository for packages/*, so the easiest place to build your add-on is a new directory inside packages. Once its composer.json file exists, require it like any other package:
composer require acme/hello
Package Structure
A typical add-on looks like this. Every directory is optional except src:
packages/hello/
├── composer.json
├── config/hello.php
├── database/migrations/
├── resources/views/
│ └── livewire/
├── routes/team.php
├── src/HelloServiceProvider.php
└── tests/Feature/
The Composer File
An add-on's composer.json file must use the nicetry-addon type and register its service provider for discovery. The extra.nicetry section describes the add-on to the control panel:
{
"name": "acme/hello",
"description": "One line that shows on the Add-ons page.",
"type": "nicetry-addon",
"version": "1.0.0",
"license": "Elastic-2.0",
"autoload": {
"psr-4": {
"Acme\\Hello\\": "src/"
}
},
"extra": {
"nicetry": {
"name": "Hello",
"edition": "free"
},
"laravel": {
"providers": [
"Acme\\Hello\\HelloServiceProvider"
]
}
}
}
The extra.nicetry section accepts the following keys:
| Key | Description |
|---|---|
name |
The add-on's display name. Defaults to a headline of its slug. |
slug |
The add-on's slug. Defaults to the part of the package name after the /. |
edition |
Either free, the default, or paid. |
The Service Provider
An add-on's service provider extends App\Addons\AddonServiceProvider. Most of what an add-on contributes is declared using properties and methods on this class, and NiceTryBot wires everything up for you:
namespace Acme\Hello;
use App\Addons\AddonServiceProvider;
use App\Enums\ViewSlot;
use App\Support\NavItem;
class HelloServiceProvider extends AddonServiceProvider
{
/**
* The add-on's route files.
*
* @var array<string, string>
*/
protected array $routes = [
'team' => 'routes/team.php',
];
/**
* The views drawn in the control panel's view slots.
*
* @var array<string, string>
*/
protected array $slots = [
ViewSlot::DomainPanels->value => 'hello::domain-panel',
];
/**
* The add-on's sidebar links.
*
* @return list<NavItem>
*/
protected function nav(): array
{
return [
new NavItem('Hello', 'hand-raised', 'hello.index', order: 75),
];
}
}
The provider's register and boot methods are reserved for NiceTryBot. If your add-on needs to do anything else, override the registerAddon and bootAddon methods instead:
/**
* Bootstrap the add-on.
*/
protected function bootAddon(): void
{
// ...
}
Conventions
Like Laravel itself, add-ons favour convention over configuration. NiceTryBot loads the following from your package's root automatically, using the add-on's slug:
config/{slug}.phpis merged intoconfig('{slug}').database/migrationsrun alongside NiceTryBot's own migrations.resources/viewsare available under the{slug}::namespace.resources/views/livewirebecome Livewire components under the{slug}::namespace.gym/obstacles/{Vendor}/{Name}holds the front-end of any gym obstacles.
Routes
Add-ons declare their route files using the $routes property. Each key determines how the routes are loaded:
| Key | Description |
|---|---|
team |
Signed-in pages, served under the current team, such as /{team}/hello. They use the web, auth, and verified middleware and require team membership. |
gym |
The visitor's side of the gym, served under the gym path. These routes have no session, CSRF protection, or Livewire. |
web |
Loaded as-is. |
For example, the following routes/team.php file serves a Livewire page from the add-on's resources/views/livewire directory:
use Illuminate\Support\Facades\Route;
Route::livewire('hello', 'hello::index')->name('hello.index');
Every route an add-on registers returns a 404 while the add-on is turned off, so you never need to check for that yourself.
Navigation
Add-ons may add links to three menus by overriding the corresponding method on their service provider:
| Method | Returns | Menu |
|---|---|---|
nav |
list<App\Support\NavItem> |
The sidebar. |
settingsNav |
list<App\Support\SettingsNavItem> |
The Settings menu. |
workspaceMenu |
list<App\Support\WorkspaceMenuItem> |
The cog menu in the top right. |
Each item accepts an order, which determines where it appears among NiceTryBot's own links. Core sidebar links are spaced ten apart, from Overview at 10 to Bot gym at 90, so there is always room to slot yours in between. The cog menu's core links are Domains & origins at 10 and Visitor data at 20.
View Slots
View slots are places in the control panel where an add-on may draw its own views. Map a slot to a view, or a list of views, using the $slots property:
| Slot | Where it draws |
|---|---|
head |
The <head> of every control panel page. |
public-nav |
Buttons in the public header. |
welcome-sections |
Sections on the welcome page. |
login-actions |
Below the login form. |
app-banner |
Notices above signed-in pages. |
topbar-actions |
Small buttons before the cog menu. |
body-end |
Scripts and modals at the end of the <body>. |
domains-actions |
Buttons beside Add domain. Receives $team. |
domains-sections |
Sections on the Domains & origins page. Receives $team. |
domain-panels |
Panels in a domain page's side column. Receives $domain. |
rules-gallery |
Below the rule template gallery. Receives $team. |
rule-panels |
Below Send its clients to the gym when editing a rule. Receives $team and $rule. |
Gym Obstacles
An add-on may add obstacles to the bot gym by listing them in the $obstacles property. Each obstacle implements the App\Gym\Contracts\Obstacle interface, and most obstacles extend App\Gym\BaseObstacle, which provides sensible defaults:
protected array $obstacles = [
StretchingObstacle::class,
];
The interface asks an obstacle to describe itself to the routine editor, and to run its challenge for the visitor:
| Method | Description |
|---|---|
key() |
A stable key that routines store, such as proof-of-work. Never change it once released. |
name(), tagline(), icon() |
How the obstacle appears in the routine editor. Icons are Heroicon names. |
settings(), configure(), summarize() |
The obstacle's settings, and a short summary of a configured obstacle. |
humanSeconds(), workHashes() |
How long the obstacle takes a person, and how much work it costs a bot. |
begin(), view(), viewData() |
Start the challenge and render it. |
attempt() |
Check a visitor's answer, returning an ObstacleOutcome. |
The obstacles that ship with NiceTryBot, in gym/obstacles/NiceTryBot, are the best reference for writing your own.
Rule Fields
Add-ons may add fields to the rule language and Activity search. List a backed enum implementing App\Support\RuleFieldDefinition in the $ruleFields property, and each case becomes a field. The interface's methods describe the database column behind each field, whether it holds text or numbers, and the English name and hint shown in the editor. The Tarpit add-on's tarpit and tarpit.outcome fields are a good example.
Add-ons may also offer ready-made rules and gym routines through the $ruleTemplates and $routineTemplates properties.
Visitor Data
If your add-on stores anything about visitors, it must tell NiceTryBot how to keep that data in check. List a class implementing App\Support\VisitorDataHolder in the $visitorData property. It answers three questions:
| Method | Description |
|---|---|
retention() |
How long each kind of data the add-on stores is kept. |
prune() |
Delete a team's data older than its retention period. |
forget() |
Erase everything stored about a visitor, by IP address or fingerprint. |
This keeps your add-on's data inside the retention periods and erasure requests described in the privacy documentation, and those duties continue even while the add-on is turned off.
Other Extension Points
The service provider offers a number of other properties for more specialised add-ons:
| Property | Description |
|---|---|
$policies |
Authorization policies to register, keyed by model. |
$mcpTools |
Tools to add to the control panel's MCP server. |
$requestAddons |
Marks, details, and actions for requests in Activity. |
$actionAddons |
Badges for Activity's action column. |
$clientAddons |
Extra details for client cells. |
$edgeLineAddons |
Keep your own data from each line of the edge's access log. |
$attemptListeners |
Listen to every obstacle attempt in the gym. |
$gymAddons |
Add a setting to the routine editor, and HTML to the gym's page. |
$captureFeed, $lockdownFeed, $tarpitFeed |
Feed instructions to the edge. |
To see these in use, the free add-ons in NiceTryBot's packages directory are complete, working examples. packages/privacy-mode is the smallest place to start, packages/tarpit shows rule fields and edge feeds, and packages/lockdown shows policies, MCP tools, and the cog menu.
Note
Paid add-ons are licence-gated by NiceTryBot automatically when their edition is paid, so add-on authors never write licence code of their own.
Testing Add-ons
NiceTryBot is tested using Pest, and your add-on's tests may live in its own tests directory alongside its code. Before sharing your add-on, run NiceTryBot's checks, which include Pint, PHPStan, and the test suite:
composer ci:check