# Creating Add-ons

- [Introduction](#introduction)
    - [Creating Add-ons Using AI](#creating-add-ons-using-ai)
- [Local Development](#local-development)
- [Package Structure](#package-structure)
    - [The Composer File](#the-composer-file)
    - [The Service Provider](#the-service-provider)
    - [Conventions](#conventions)
- [Routes](#routes)
- [Navigation](#navigation)
- [View Slots](#view-slots)
- [Gym Obstacles](#gym-obstacles)
- [Rule Fields](#rule-fields)
- [Visitor Data](#visitor-data)
- [Other Extension Points](#other-extension-points)
- [Testing Add-ons](#testing-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](https://laravel.com/docs/packages#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:

```text
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:

```shell
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:

```shell
composer require acme/hello
```

## Package Structure

A typical add-on looks like this. Every directory is optional except `src`:

```text
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:

```json
{
    "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:

```php
<?php

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:

```php
/**
 * 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:

<div class="content-list" markdown="1">

- `config/{slug}.php` is merged into `config('{slug}')`.
- `database/migrations` run alongside NiceTryBot's own migrations.
- `resources/views` are available under the `{slug}::` namespace.
- `resources/views/livewire` become Livewire components under the `{slug}::` namespace.
- `gym/obstacles/{Vendor}/{Name}` holds the front-end of any gym obstacles.

</div>

## 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:

```php
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](https://docs.nicetrybot.com/docs/0.x/concepts.md#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:

```php
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](https://docs.nicetrybot.com/docs/0.x/concepts.md#available-fields) 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](https://docs.nicetrybot.com/docs/0.x/privacy.md), 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](https://docs.nicetrybot.com/docs/0.x/mcp.md). |
| `$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](https://pestphp.com), 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:

```shell
composer ci:check
```
