> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dezerx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Addon Modules

> Anything that isn't provisioning or payments: affiliate programs, blogs, knowledge bases, forms, compliance tooling, status pages, loyalty schemes.

Addons are the freest type. Core has no conventions it looks for inside one, so you're writing a
normal Laravel and Inertia feature that happens to live in `Modules/Addons/`.

<Note>
  Read [Fundamentals](/extensions/tutorials/fundamentals) first if you haven't.
</Note>

## Generate it

```bash theme={null}
php artisan make:module LoyaltyRewards --type=addon
php artisan module:enable LoyaltyRewards
php artisan module:migrate LoyaltyRewards
php artisan module:doctor LoyaltyRewards
```

<Accordion title="What you get" icon="folder-tree">
  ```text theme={null}
  Modules/Addons/LoyaltyRewards/
  ├── module.json                 with permissions declared
  ├── app/
  │   ├── Providers/
  │   ├── Http/Controllers/SettingsController
  │   └── Models/LoyaltyRewardsCredential   rename or delete this
  ├── database/migrations/
  └── routes/                     web, api, settings
  ```
</Accordion>

<Tip>
  The generated credentials model assumes you talk to an external API. If you don't, delete it and its
  migration, and swap the settings controller over to a key-value settings table instead.
</Tip>

## How addons differ

|                        | Service modules | Gateways | Addons           |
| ---------------------- | --------------- | -------- | ---------------- |
| Classes core looks for | \~12            | 1        | **none**         |
| Registration           | By name         | Explicit | **you drive it** |

Everything happens through four things: routes you register, listeners on core events, Inertia pages
in `register.ts`, and your own tables.

## Hooking into core

Events are the main integration point. Register them in `EventServiceProvider`:

```php theme={null}
protected $listen = [
    \App\Events\ServicePaymentCompleted::class => [AwardPurchasePoints::class],
    \App\Events\ServiceRenewalInitiated::class => [AwardRenewalPoints::class],
    \Illuminate\Auth\Events\Registered::class => [GiveSignupBonus::class],
];
```

The full list is in [Fundamentals](/extensions/tutorials/fundamentals#events).

<Warning>
  Two things to get right there: keep `configureEmailVerification()` as a no-op, and never list a
  listener that already lives in `app/Listeners`.
</Warning>

Queue your listeners:

```php theme={null}
class AwardPurchasePoints implements ShouldQueue
{
    public function handle(ServicePaymentCompleted $event): void { /* ... */ }
}
```

<Warning>
  An addon that throws inside a synchronous listener can break checkout for everyone.
</Warning>

<Tip>
  `Affiliates` is the best reference here. It listens to registration, payment and renewal, and does
  payouts off the back of them.
</Tip>

## Settings

<Tabs>
  <Tab title="Flat fields">
    Keep the generated `SettingsController` and just change the fields. You get a full admin page with
    no React. See [the settings section](/extensions/tutorials/fundamentals#admin-settings-with-no-frontend-work).
  </Tab>

  <Tab title="Custom UI">
    Like `Forms` does with its 12 pages. Write your own controller and page, then register it:

    ```ts resources/js/register.ts theme={null}
    registerModulePages({
        'LoyaltyRewards/Settings': () => import('./Pages/Admin/Settings.tsx'),
        'LoyaltyRewards/Dashboard': () => import('./Pages/User/Dashboard.tsx'),
    });
    ```
  </Tab>

  <Tab title="Key-value store">
    A small table and model is the usual pattern:

    ```php theme={null}
    class LoyaltySetting extends Model
    {
        protected $table = 'loyalty_settings';
        protected $fillable = ['key', 'value'];

        public static function get(string $key, mixed $default = null): mixed
        {
            return static::where('key', $key)->value('value') ?? $default;
        }
    }
    ```

    Seed sensible defaults so the module works the moment it's enabled. `Affiliates` does this with
    `AffiliateSettingsSeeder`.
  </Tab>
</Tabs>

## User-facing routes

```php routes/web.php theme={null}
Route::middleware(['auth', 'verified', CheckUserSuspension::class])->group(function () {
    Route::get('/rewards', [RewardsController::class, 'index'])->name('rewards.index');
    Route::post('/rewards/redeem', [RewardsController::class, 'redeem'])->name('rewards.redeem');
});
```

Admin routes and their permissions are already set up by the generator. See
[Admin permissions](/extensions/tutorials/fundamentals#admin-permissions) if you need to add more.

## Navigation

<Info>
  There's no manifest hook for sidebar links. They come from the `SidebarLink` table, which admins
  manage in the admin panel.
</Info>

So either seed a row when your module is enabled, which is friendlier, or document the URL and let the
admin add it:

```php theme={null}
SidebarLink::firstOrCreate(
    ['url' => '/rewards'],
    ['label' => 'Rewards', 'icon' => 'gift', 'is_active' => true, 'order' => 50],
);
```

## Commands and cron

```php theme={null}
protected function moduleCommands(): array
{
    return [ExpireLoyaltyPoints::class];
}

protected function moduleSchedule(Schedule $schedule): void
{
    $schedule->command('loyalty:expire-points')->daily();
}
```

<Warning>Use `moduleSchedule()` or `routes/console.php`, not both.</Warning>

## Touching core data

Adding a column to `users` or `services` is fine. Make it nullable and the migration idempotent:

```php theme={null}
if (! Schema::hasColumn('users', 'loyalty_points')) {
    Schema::table('users', fn (Blueprint $t) => $t->unsignedInteger('loyalty_points')->default(0));
}
```

<Warning>
  Never change or drop a core column, and don't edit `App\Models\User`. Point a relationship at it
  from your own model instead:

  ```php theme={null}
  class LoyaltyAccount extends Model
  {
      public function user(): BelongsTo
      {
          return $this->belongsTo(\App\Models\User::class);
      }
  }
  ```
</Warning>

<Tip>
  If you need more than one field, prefer a side table with a foreign key. It keeps your module
  removable.
</Tip>

## Test it

```bash theme={null}
php artisan module:doctor LoyaltyRewards
```

<Steps>
  <Step title="Enable it">
    Migrations run, defaults seed, admin page loads.
  </Step>

  <Step title="Trigger each event you listen to">
    Check the side effects.
  </Step>

  <Step title="Disable it, and confirm the app still works completely">
    Nothing in core should depend on your tables, listeners or routes.
  </Step>

  <Step title="Re-enable it">
    No duplicate rows, no failed migrations.
  </Step>
</Steps>

<Warning>Step 3 is the one people skip and the one that breaks production.</Warning>

## Worth reading

<CardGroup cols={2}>
  <Card title="LoyaltyProgram" icon="gift">
    Small and event-driven.
  </Card>

  <Card title="News" icon="newspaper">
    Content CRUD with admin and public pages.
  </Card>

  <Card title="Affiliates" icon="users">
    Listeners, payouts, a console command, 4 React pages.
  </Card>

  <Card title="Forms" icon="table-list">
    The most frontend-heavy, 12 pages.
  </Card>
</CardGroup>

<Info>
  `Affiliates`, `Blog`, `KnowledgeBase` and `News` still use an older hand-written service provider.
  Copy their features, not their providers.
</Info>
