richnessagency / rich-addons
Modular add-on / plugin system for Richness Laravel applications. WordPress-inspired hooks, discovery, licensing & admin UI.
Requires
- php: ^8.3
- illuminate/console: ^13.0
- illuminate/contracts: ^13.0
- illuminate/database: ^13.0
- illuminate/encryption: ^13.0
- illuminate/http: ^13.0
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
- illuminate/view: ^13.0
Requires (Dev)
- laravel/pint: ^1.27
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-08 03:07:54 UTC
README
richnessagency/rich-addons is the shared Laravel add-on runtime used by
Richness applications. It lets a host application discover add-ons, install
signed marketplace releases, activate free or paid modules, register hooks,
expose admin pages, and render storefront widgets.
The package is intended to be installed through Composer, not copied into a project as local source code.
composer require richnessagency/rich-addons
What This Package Provides
- Local add-on discovery from
addons/*/addon.json. - Marketplace catalog sync from a central Laravel application.
- Signed ZIP release download and installation.
- SHA-256 checksum verification.
- HMAC or OpenSSL release signature verification.
- Paid and subscription license activation.
- Signed license payload validation bound to add-on id, domain, and system key.
- Runtime license enforcement before paid/subscription add-ons are booted.
- Action and filter hooks inspired by WordPress.
- Optional admin routes and storefront widgets.
Host Application Setup
Install the package:
composer require richnessagency/rich-addons
Publish the config:
php artisan vendor:publish --tag=rich-addons-config
Run migrations:
php artisan migrate
Add the runtime settings to .env:
RICH_ADDONS_PATH=addons RICH_ADDONS_MARKETPLACE_URL=https://your-core-marketplace.test RICH_ADDONS_LICENSE_SERVER=https://your-core-marketplace.test RICH_ADDONS_SYSTEM_KEY=client-store-001 RICH_ADDONS_SYSTEM_SECRET=server-issued-system-secret RICH_ADDONS_RELEASE_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----..." RICH_ADDONS_LICENSE_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----..."
For development or HMAC-only environments you may use:
RICH_ADDONS_SECRET_KEY=shared-development-secret
Production should prefer public/private key signing:
- The central system keeps the private key.
- Client applications only receive the public key.
- Client applications must not know the signing secret/private key.
Configuration Reference
The package config lives in config/rich-addons.php.
| Key | Env | Purpose |
|---|---|---|
addons_path |
RICH_ADDONS_PATH |
Directory where installed add-ons live. |
auto_discover |
RICH_ADDONS_AUTO_DISCOVER |
Enables runtime discovery. |
admin_route_prefix |
RICH_ADDONS_ROUTE_PREFIX |
Admin route prefix for add-on management. |
marketplace_url |
RICH_ADDONS_MARKETPLACE_URL |
Central marketplace API base URL. |
system_key |
RICH_ADDONS_SYSTEM_KEY |
Stable id for the consuming system. |
system_secret |
RICH_ADDONS_SYSTEM_SECRET |
Bearer token used with central API calls. |
staging_path |
RICH_ADDONS_STAGING_PATH |
Temporary extraction directory for releases. |
release_public_key |
RICH_ADDONS_RELEASE_PUBLIC_KEY |
Public key for release ZIP signatures. |
license_server_url |
RICH_ADDONS_LICENSE_SERVER |
Central license API base URL. |
license_public_key |
RICH_ADDONS_LICENSE_PUBLIC_KEY |
Public key for license payload signatures. |
public_key_path |
RICH_ADDONS_PUBLIC_KEY |
Path to a public key file. |
secret_key |
RICH_ADDONS_SECRET_KEY |
HMAC fallback secret. |
heartbeat_interval_hours |
RICH_ADDONS_HEARTBEAT_HOURS |
Intended remote validation cadence. |
cache_seconds |
RICH_ADDONS_CACHE_SECONDS |
Local registry cache TTL. |
Add-on Directory Structure
A local or marketplace-installed add-on should be a self-contained directory:
addons/
richness-announcement-bar/
addon.json
src/
AnnouncementBarAddon.php
Http/
Controllers/
Models/
Providers/
database/
migrations/
resources/
views/
js/
css/
routes/
admin.php
web.php
The only required file is addon.json. The src directory and extra folders
depend on what the add-on needs.
Manifest: addon.json
Example manifest:
{
"id": "richness/announcement-bar",
"name": "Announcement Bar",
"name_en": "Announcement Bar",
"version": "1.0.0",
"author": "Richness Agency",
"description": "Adds a configurable announcement bar to the storefront.",
"tier": "subscription",
"min_app_version": "1.0.0",
"provider": "Richness\\AnnouncementBar\\AnnouncementBarAddon",
"permissions": [
"settings.read",
"settings.write"
],
"hooks": [
"storefront.layout.before_header"
],
"icon": "fa-solid fa-bullhorn",
"psr4": {
"Richness\\AnnouncementBar\\": "src/"
}
}
Supported keys:
| Key | Required | Notes |
|---|---|---|
id |
yes | Unique machine id. Prefer vendor/addon-name. |
name |
yes | Display name. |
name_en |
no | Optional English display name. |
version |
yes | Semantic version. |
author |
no | Defaults to Unknown. |
description |
no | Short purpose statement. |
tier |
no | free, paid, or subscription. Defaults to free. |
min_app_version |
no | Host app compatibility marker. |
provider |
yes for bootable add-ons | Main add-on PHP class. |
mainClass |
legacy alias | Alias for provider. |
permissions |
no | Declared capabilities for review/UI. |
hooks |
no | Hooks used or provided by the add-on. |
icon |
no | Admin UI icon class. |
checksum |
marketplace | Release checksum metadata. |
psr4 |
local add-ons | Runtime autoload mapping for non-Composer add-ons. |
Main Add-on Class
The main class must implement Richness\RichAddons\Contracts\Addon.
The easiest path is extending AbstractAddon.
<?php declare(strict_types=1); namespace Richness\AnnouncementBar; use Richness\RichAddons\Data\AddonTier; use Richness\RichAddons\Support\AbstractAddon; final class AnnouncementBarAddon extends AbstractAddon { public function id(): string { return 'richness/announcement-bar'; } public function name(): string { return 'Announcement Bar'; } public function version(): string { return '1.0.0'; } public function description(): string { return 'Adds a configurable storefront announcement bar.'; } public function tier(): AddonTier { return AddonTier::Subscription; } public function install(): void { // Create tables, seed settings, or publish assets if needed. } public function boot(): void { // Register bindings, view namespaces, config defaults, or lightweight services. } public function uninstall(): void { // Remove add-on-owned data only when the admin explicitly uninstalls it. } }
Keep boot() lightweight. Expensive jobs should be queued, cached, or executed
from explicit admin actions.
Backend Integration
Use normal Laravel code inside the add-on:
- Controllers for admin or public actions.
- Form requests for validation.
- Policies/gates for authorization.
- Jobs/listeners for async work.
- Models for add-on-owned tables.
- Config defaults inside the add-on class or a service provider.
An add-on should not edit host application core files. Integration should happen through hooks, service container bindings, routes, views, migrations, and published configuration.
Database Integration
Place migrations under the add-on directory:
database/
migrations/
2026_09_07_000000_create_announcement_bars_table.php
Recommended rules:
- Prefix add-on-owned tables, for example
addon_announcement_bars. - Keep foreign keys explicit and nullable only when the relationship is optional.
- Avoid changing host tables unless the host application exposes a documented extension point.
- Migrations must be idempotent where possible by checking existing tables or columns.
uninstall()must never delete customer data silently. Destructive cleanup should require a clear admin action.
Example:
Schema::create('addon_announcement_bars', function (Blueprint $table): void { $table->id(); $table->string('message'); $table->boolean('is_enabled')->default(true); $table->timestamp('starts_at')->nullable(); $table->timestamp('ends_at')->nullable(); $table->timestamps(); });
Hooks
Add-ons can register action and filter listeners by implementing HasHooks.
use Richness\RichAddons\Contracts\HasHooks; use Richness\RichAddons\Hooks\HookManager; final class AnnouncementBarAddon extends AbstractAddon implements HasHooks { public function registerHooks(HookManager $hooks): void { $hooks->addAction('storefront.layout.before_header', function (): void { echo view('announcement-bar::bar')->render(); }); $hooks->addFilter('cart.discount_total', function (float $total): float { return $total; }, priority: 20); } }
Host applications can call hooks with:
do_action('storefront.layout.before_header'); $price = apply_filters('product.final_price', $price, $product);
Hook naming convention:
domain.area.event
Examples:
storefront.layout.before_headercheckout.order.before_createcheckout.order.after_createcart.line.before_totalproduct.price.after_resolveadmin.sidebar.items
Actions are for side effects. Filters must return the transformed value.
Admin UI Integration
Implement HasAdminPanel when an add-on contributes admin pages.
use Illuminate\Support\Facades\Route; use Richness\RichAddons\Contracts\HasAdminPanel; final class AnnouncementBarAddon extends AbstractAddon implements HasAdminPanel { public function registerAdminRoutes(): void { Route::middleware(['web', 'auth']) ->prefix('admin/addons/announcement-bar') ->name('admin.addons.announcement-bar.') ->group(__DIR__ . '/../routes/admin.php'); } public function adminNavItems(): array { return [ [ 'label' => 'Announcement Bar', 'route' => 'admin.addons.announcement-bar.index', 'icon' => 'fa-solid fa-bullhorn', 'active_pattern' => 'admin/addons/announcement-bar*', ], ]; } }
Admin pages should follow the host application's design system and middleware. Never expose add-on admin routes without authentication and authorization.
Storefront Widgets
Implement HasStorefrontWidgets when the add-on renders storefront fragments.
use Richness\RichAddons\Contracts\HasStorefrontWidgets; final class AnnouncementBarAddon extends AbstractAddon implements HasStorefrontWidgets { public function widgets(): array { return [ 'storefront.header.before' => fn () => view('announcement-bar::bar')->render(), ]; } }
Widgets should return HTML strings, renderable views, or callables accepted by the host application. Keep widget rendering fast and cache data when needed.
Frontend Integration
An add-on may ship Blade views, CSS, JavaScript, Livewire components, or assets.
Recommended layout:
resources/
views/
admin/
index.blade.php
storefront/
bar.blade.php
js/
admin.js
css/
storefront.css
Guidelines:
- Scope CSS classes with an add-on prefix, for example
.ra-announcement-bar. - Avoid global CSS resets.
- Avoid modifying host Vite config from inside the add-on.
- Prefer Blade components or view namespaces over editing host templates.
- Use hooks and widgets for storefront placement.
- Keep JavaScript progressive: the feature should fail gracefully when JS is not loaded.
- Do not place secrets, license keys, system keys, or payment data in frontend code.
Marketplace Flow
The consuming app talks to the central marketplace using:
GET /api/v1/addons/catalogGETrelease download URL from the catalog metadataPOST /api/v1/addons/licenses/activatePOST /api/v1/addons/licenses/ping
Requests include:
X-Rich-Addons-System: <system_key>Authorization: Bearer <system_secret>when configured- The current domain from the consuming application
The central system is responsible for:
- Maintaining catalog records.
- Publishing release metadata.
- Signing release checksums.
- Issuing license keys.
- Signing license payloads.
- Enforcing paid/subscription download eligibility.
- Granting manual access for selected systems/domains.
- Applying subscription expiration and grace periods.
Signed Release Requirements
Marketplace add-ons are installed from ZIP releases. A release is accepted only when:
- The ZIP downloads successfully.
- The SHA-256 checksum matches catalog metadata.
- The signature validates.
- The archive contains
addon.json. - The manifest
idmatches the selected add-on. - The manifest
versionmatches the selected release. - The archive does not contain unsafe paths such as
../file.phpor absolute paths.
The installer extracts to a staging directory first, validates the release, then
moves it to RICH_ADDONS_PATH.
Licensing Flow
Paid and subscription add-ons require a valid license.
Activation:
- Admin installs the add-on from marketplace.
- Admin enters a license key in the consuming application.
- The package sends license key, add-on id, system key, and domain to the central system.
- The central system returns a signed payload.
- The package verifies the signature and stores the payload locally.
Runtime:
- On boot, free add-ons load normally.
- Paid/subscription add-ons are checked before autoload and boot.
- A valid signed payload allows the add-on to boot.
- An expired license may continue only until
grace_until. - After
grace_until, the add-on is markedsuspendedand is not booted.
The runtime never deletes customer files or data as a licensing action. It disables execution by refusing to boot unlicensed paid/subscription add-ons.
License Payload Shape
The central system should sign a payload shaped like:
{
"license_key": "RICH-XXXX-XXXX-XXXX",
"addon_id": "richness/announcement-bar",
"domain": "client.test",
"allowed_domains": ["client.test", "staging.client.test"],
"system_key": "client-store-001",
"status": "active",
"expires_at": "2026-10-07T00:00:00+00:00",
"grace_until": "2026-10-22T00:00:00+00:00",
"timestamp": "2026-09-07T00:00:00+00:00",
"signature_alg": "openssl-sha256",
"signature": "base64-signature"
}
The signature is calculated over the same payload without the signature key.
Supported algorithms:
openssl-sha256: recommended for production.hmac-sha256: useful for local development and controlled internal systems.
Security Rules
- Do not add backdoors.
- Do not delete files or customer data because of billing state.
- Do not execute arbitrary uploaded PHP.
- Do not install a ZIP unless checksum and signature pass.
- Do not trust marketplace responses without signature verification.
- Do not store private signing keys in consuming applications.
- Do not bind a paid license to only a license key; bind it to add-on id, domain, system key, status, expiration, and signature.
- Do not use loose domain checks such as
str_contains. - Do not allow
../or absolute paths in release archives. - Do not expose admin routes without host application middleware.
Building a New Add-on
- Choose a stable id, for example
richness/reviews. - Create an add-on folder with
addon.json. - Create a main class extending
AbstractAddon. - Add
psr4mapping in the manifest for local discovery. - Implement optional interfaces:
HasHooksfor backend/storefront integration.HasAdminPanelfor admin routes and nav items.HasStorefrontWidgetsfor renderable storefront blocks.
- Add migrations for add-on-owned data.
- Add Blade/Livewire/assets under
resources. - Test install, activate, boot, deactivate, and uninstall paths.
- Package a ZIP containing
addon.jsonat the archive root. - Publish release metadata from the central marketplace.
Example Minimal Add-on
addons/richness-reviews/
addon.json
src/ReviewsAddon.php
addon.json:
{
"id": "richness/reviews",
"name": "Reviews",
"version": "1.0.0",
"author": "Richness Agency",
"description": "Adds product reviews.",
"tier": "free",
"provider": "Richness\\Reviews\\ReviewsAddon",
"psr4": {
"Richness\\Reviews\\": "src/"
}
}
src/ReviewsAddon.php:
<?php declare(strict_types=1); namespace Richness\Reviews; use Richness\RichAddons\Support\AbstractAddon; final class ReviewsAddon extends AbstractAddon { public function id(): string { return 'richness/reviews'; } public function name(): string { return 'Reviews'; } public function version(): string { return '1.0.0'; } public function boot(): void { // } }
Testing Checklist
For every add-on:
- Manifest parses successfully.
- Discovery creates or updates the
rich_addonsrecord. - Free add-ons activate without a license.
- Paid/subscription add-ons reject empty or invalid license keys.
- Signed license payload works only for the intended add-on, domain, and system.
- Expired license inside grace period still boots.
- Expired license after grace period becomes
suspendedand does not boot. - Marketplace ZIP rejects checksum mismatch.
- Marketplace ZIP rejects unsafe paths.
- Admin routes require authentication.
- Frontend widgets render without leaking secrets.
Composer And Packagist
The package is distributed as:
composer require richnessagency/rich-addons
Use Packagist auto-update or a GitHub webhook so every pushed commit/tag becomes available to Composer. For stable production releases, prefer semantic version tags:
git tag v1.0.0 git push origin v1.0.0
Host applications may require ^1.0 for stable releases or dev-main for
active internal development.
License
MIT License. Created by Richness Agency.