Amvionlie CMS
Where the Future Begins

Build Your First Addon

This walkthrough builds a minimal governed addon called `example_notes`. It gives the addon one admin list/form workflow, one public route, registered permissions, a schema contract, a Menu Manager target provider, and an update path.

1. Create The Addon Folder

Create this package shape:

addons/example_notes/
  manifest.php
  README.md
  bootstrap/install_contract.php
  routes/routes.php
  src/runtime.php
  src/admin_runtime.php
  src/public_runtime.php
  src/schema_contract.php
  src/permissions_contract.php
  src/public_targets.php
  languages/en_US/admin.php
  languages/en_US/public.php
  tests/smoke.php

Keep the folder name and `addon_key` identical: `example_notes`.

2. Write The Manifest

Start from Reference Samples/Sample Manifest.

Required decisions:

  • stable `product_uuid`
  • new `package_uuid`
  • `addon_key`
  • `display_name`
  • `slug`
  • `version`
  • `addon_type`
  • `summary`
  • `discovery`
  • `paths`
  • `routes_contract`
  • `admin_surface_contract`
  • `frontend_contract`
  • `risk_declarations`

See Addon Development/Manifest Contract for field rules.

3. Write The Install Contract

Create `bootstrap/install_contract.php`.

The install contract must declare:

  • `addon_key`
  • contract version
  • whether schema contract is present
  • whether permissions contract is present
  • schema provider path and apply function
  • permissions provider path or inline permissions
  • install/update/rollback/uninstall declarations

See Addon Development/Install Contract.

4. Write The Schema Contract

Use a schema contract when the addon owns tables.

Example owned table:

CREATE TABLE IF NOT EXISTS amv_example_notes (
    note_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    note_uuid CHAR(36) NOT NULL,
    title VARCHAR(190) NOT NULL,
    slug VARCHAR(190) NOT NULL,
    body_text LONGTEXT NOT NULL,
    state_key VARCHAR(40) NOT NULL DEFAULT 'draft',
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    PRIMARY KEY (note_id),
    UNIQUE KEY amv_example_notes_uuid_unique (note_uuid),
    UNIQUE KEY amv_example_notes_slug_unique (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

The schema apply function should only create/update addon-owned schema.

5. Write The Permissions Contract

Create `src/permissions_contract.php`:

return [
    'addon_key' => 'example_notes',
    'permissions' => [
        'example_notes.view' => [
            'label' => 'View Example Notes',
            'description' => 'View notes.',
        ],
        'example_notes.manage' => [
            'label' => 'Manage Example Notes',
            'description' => 'Create, edit, publish, and archive notes.',
        ],
    ],
];

Routes and admin saves must check these permissions.

6. Write Routes

Create `routes/routes.php`.

Expected routes:

  • `GET /admin/index.php?view=example_notes`
  • `POST /admin/index.php?view=example_notes`
  • `GET /example-notes`
  • `GET /example-notes/{slug}`

Use the Core route registry helpers already used by existing addons. Keep route declarations consistent with `routes_contract`.

7. Write Admin Runtime

The addon owns the inner admin content. The Admin shell owns the page frame.

Admin runtime should:

  • require `admin.access` and `example_notes.view` for list screens
  • require `example_notes.manage` for create/edit/save
  • validate CSRF on POST
  • use shared list/form helpers where possible
  • return a notice after save
  • redirect after successful POST to avoid duplicate submissions

See Developer Workflows/Admin Template Rules.

8. Write Public Runtime

Public runtime should:

  • render published records only
  • use public route params, not admin query strings
  • return 404 for missing/unpublished records
  • avoid requiring admin permissions for public pages

9. Write Public Target Provider

Create `src/public_targets.php`.

Expose:

  • home target: `/example-notes`
  • record targets: `/example-notes/{slug}`
  • stable target identifiers
  • labels and kind labels
  • availability state

See Developer Workflows/Menu Manager Public Targets.

10. Package The Addon

Package the addon folder so `manifest.php` is at the package root for addon packages. Avoid unsafe paths, symlinks, absolute paths, hidden traversal, and oversized archives.

Package safety limits currently enforced by Installer include:

  • archive max: 64 MB
  • file max: 16 MB
  • uncompressed total max: 96 MB
  • file count max: 2048
  • compression ratio max: 200

11. Install The Addon

Use Addon Installer for package intake.

Expected flow:

  1. Installer validates package safety.
  2. system governance validates identity, dependency, risk, route, slug, and route-key collisions.
  3. Installer deploys only the accepted package.
  4. Installer applies schema and permissions contracts.
  5. Installer syncs/hands off to system governance.
  6. system governance persists accepted lifecycle/runtime truth.

12. Update The Addon

For an update:

  • keep `product_uuid` unchanged
  • use a new `package_uuid`
  • increment `version`
  • keep `addon_key` stable
  • keep route keys stable unless a migration explains the change
  • include schema migrations in the schema/apply contract

An update is valid only when system governance confirms same lineage. Current Installer code still participates in some same-lineage checks; treat that as transitional drift.

13. Verify

Before release, verify:

  • system governance sees the addon without issues.
  • Addon Installer reports successful deployment.
  • Schema tables exist.
  • Permissions are registered.
  • Admin list renders.
  • Admin create/edit POST uses CSRF and redirects.
  • Public list/detail routes return `200`.
  • Missing public records return `404`.
  • Menu Manager target provider lists home and record targets.
  • Smoke test covers manifest, schema, permissions, routes, and public targets.
  • Changelog entry exists for the release.

Contract Pages To Read

Use these pages while building:

Updated: 2026-05-07 02:18:09