AirLink
Blog

Inside the addon system

thavanish · Mon Jun 01 2026 00:00:00 GMT+0000 (Coordinated Universal Time) · 3 min read

AirLink's addon system lets you extend the panel without touching core files. This post explains how it works under the hood.

Loading

When the panel starts, it scans storage/addons/ for directories with a valid package.json. For each addon:

  1. Read package.json for metadata, entry point, and migrations
  2. If enabled is not false, proceed with loading
  3. Run any unapplied migrations (tracked in AddonMigration table)
  4. Import the entry point and call its default export with an Express router and the API object

Addon loading process

Panel
Scan storage/addons/
Read package.json
Run migrations
Import entry point
Addon
Register routes
Register sidebar items
PanelScan storage/addons/
PanelRead package.json
PanelRun migrations
PanelImport entry point
AddonRegister routes
AddonRegister sidebar items

The entry point's default function receives two arguments: an Express router and an API object.

The API object

The API object is the panel handing you the keys. Everything the panel can do, your addon can do.

export default function(router: Router, api: any) {
  const {
    logger,         // write to panel logs
    prisma,         // database access
    addonPath,      // your addon's directory
    viewsPath,      // your views/ folder
    utils,          // user/server helpers
    ui,             // sidebar, server menu, server sections
  } = api;
}

logger

Standard four levels: info, warn, error, debug. Output goes to the panel's log file and stdout.

logger.info('addon loaded');
logger.error('something broke', error);

prisma

Full Prisma client connected to the panel's database. For tables defined by your migrations, use $queryRaw and $executeRaw since they are not in the Prisma schema.

// Panel tables — use Prisma methods
const users = await prisma.users.findMany();

// Your tables — use raw SQL
const items = await prisma.$queryRaw`SELECT * FROM MyAddonItems`;

utils

await api.utils.isUserAdmin(userId);           // boolean
await api.utils.checkServerAccess(userId, serverId); // boolean
await api.utils.getServerById(serverId);        // server object
await api.utils.getServerByUUID(uuid);          // server object
api.utils.getPrimaryPort(server);               // number

ui

// Add to the main sidebar
api.ui.addSidebarItem({
  id: 'my-addon',
  name: 'My Addon',
  icon: '<svg ...></svg>',
  link: '/my-addon',
  section: 'main',   // or 'admin'
  order: 50
});

// Add to the per-server sidebar
api.ui.addServerMenuItem({
  id: 'my-feature',
  name: 'My Feature',
  icon: '<svg ...></svg>',
  link: '/server/:id/my-feature'
});

// Add a section to the server page
api.ui.addServerSection({
  id: 'my-section',
  title: 'My Section',
  content: '<div>...</div>'
});

Routing

Your addon gets a plain Express router. Anything Express can do, your routes can do.

router.get('/', (req, res) => { /* page */ });
router.get('/api/data', (req, res) => { /* JSON API */ });
router.post('/api/action', (req, res) => { /* handle POST */ });

Routes are prefixed with your router value from package.json. If router is /my-addon, then router.get('/') handles GET /my-addon/.

Views

Views are EJS templates. Use the panel's layout components to keep the UI consistent.

res.render(path.join(api.viewsPath, 'main.ejs'), {
  user: req.session?.user,
  req,
  settings,
  components: {
    header:   api.getComponentPath('views/components/header'),
    template: api.getComponentPath('views/components/template'),
    footer:   api.getComponentPath('views/components/footer')
  }
});

Place templates in views/desktop/ and views/mobile/ for viewport-specific layouts. If only one version exists, put it in views/.

Migrations

Define migrations in package.json. They run once when the addon is first enabled.

{
  "migrations": [
    {
      "name": "my_addon_v1_create",
      "sql": "CREATE TABLE IF NOT EXISTS MyTable (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)"
    }
  ]
}

See Database Migrations for the full reference.

Lifecycle

The addon lifecycle:

  1. Load — panel reads package.json, runs migrations, imports entry point
  2. Register — entry point calls router methods and UI registration
  3. Serve — routes handle requests, views render
  4. Disable — panel unregisters routes (data stays in database)
  5. Re-enable — unapplied migrations run, entry point re-registers

What addons cannot do

  • They cannot modify core panel routes or templates
  • They cannot access other addons' routes or data
  • They cannot run arbitrary code at startup (only through the entry point function)
  • They cannot prevent the panel from starting (errors are caught and logged)

These constraints keep addons sandboxed. A broken addon does not break the panel.

Community addons

The addon registry at airlinklabs/addons lists community-built addons. Install them from Admin > Addons > Store with one click.

If you built something useful, submit a PR to the registry.

All posts