The MIT-licensed SDK for official InvoiceShelf v3 modules. It extends nwidart/laravel-modules with the contracts used by InvoiceShelf's module host and signed marketplace packages.
SDK 3.4 supports Module API 1.3. First-party module packages live in their own repositories and are licensed AGPL-3.0-only; this SDK remains MIT.
- Sidebar entries and schema-driven, per-company settings.
- Local, compiled JavaScript and CSS registered with the host.
- Typed frontend contributions through
frontend/index.d.ts: full-page routes, settings pages, menus, HTTP access, translations, notifications, and lifecycle events. The host supplies the Vue, router, Axios, and i18n instances—modules do not bundle another framework runtime. - Company abilities contributed to the host's authorization catalogue, so a module can ship its own permissions instead of reusing host ones.
- Narrow host contracts under
src/Contracts/Hostfor settings, authorization, and read-only company-data queries.CompanyDataReadercovers the AI assistant's built-in queries pluscompanyMembers()andexistingInvoiceIds()for modules that assign work to members or stamp their entries against invoices. Module code must not depend on InvoiceShelf Eloquent models. - AI drivers through
src/Ai: extendAiDriver, returnAiChatResponse, and throwAiExceptionfor safe, localizable provider failures.
The host controls discovery, installation, activation, migrations, and provider registration. Official packages are not a general-purpose runtime Composer installer for arbitrary third-party code.
Declare compatibility in module.json, then test it against the host versions you support.
| Concern | Current contract |
|---|---|
| SDK | invoiceshelf/modules ^3.4 |
| Module API | ^1.3.0 |
| Host application | Your explicit InvoiceShelf 3.x range |
| PHP | The range your module actually supports |
Composer caret constraints do not include prereleases. For example, ^3.0.0 starts at the final 3.0.0, so it excludes 3.0.0-alpha.*. A module intended for the current v3 preview must explicitly opt in, for example:
"invoiceshelf": ">=3.0.0-alpha.2 <4.0.0"Use ^3.0.0 only when you mean final 3.x releases. The generated stub is a starting point; update its PHP and InvoiceShelf constraints before releasing.
New official modules use schema version 2. Their identity is permanent after the first marketplace release: keep slug and loader name unchanged. The manifest also declares an exact SemVer version, license, compatibility, required PHP extensions, module dependencies, local assets, and uninstall behavior.
{
"schema_version": 2,
"name": "SalesTaxUs",
"alias": "salestaxus",
"slug": "sales-tax-us",
"version": "1.2.3",
"license": "AGPL-3.0-only",
"providers": [
"Modules\\SalesTaxUs\\Providers\\SalesTaxUsServiceProvider"
],
"compatibility": {
"invoiceshelf": ">=3.0.0-alpha.2 <4.0.0",
"module_api": "^1.3.0",
"php": "^8.4.0",
"extensions": ["ext-json"]
},
"migration_policy": "reversible",
"dependency_policy": "host-provided-only",
"uninstall": {
"data_cleanup": "Modules\\SalesTaxUs\\Lifecycle\\DataCleanup"
},
"assets": ["dist/init.js", "dist/style.css"]
}Schema-v2 migrations must be reversible. Each migration has one concrete Laravel Migration class with non-empty up(): void and down(): void methods. The package validator rejects destructive operations in up() and runs no dependency resolver at installation time, so commit the local compiled assets declared in assets.
When an administrator chooses Remove module data, the host calls the module's cleanup hook while its tables still exist, runs every migration's down(), and removes host-owned module settings. Point uninstall.data_cleanup at a concrete, idempotent module-owned class:
<?php
namespace Modules\SalesTaxUs\Lifecycle;
use InvoiceShelf\Modules\Contracts\DataCleanup as DataCleanupContract;
final class DataCleanup implements DataCleanupContract
{
public function cleanup(): void
{
// Delete module-owned files, external resources, or shared-table rows.
}
}An intentionally empty method is valid for a module with nothing beyond reversible migrations. Throwing from cleanup() stops the uninstall so it can be retried safely.
Install the SDK in the module project, then use Laravel Modules as usual:
composer require invoiceshelf/modules:^3.4
php artisan module:make SalesTaxUsRegister server-side contributions from the module service provider:
use InvoiceShelf\Modules\Registry;
Registry::registerMenu('sales-tax-us', [
'title' => 'sales_tax_us::menu.title',
'link' => '/admin/modules/sales-tax-us/settings',
'icon' => 'CalculatorIcon',
]);registerMenu and registerUserMenu accept two placement keys besides title, link and icon:
group: the sidebar group the entry joins. The default ismodules, rendered last with the "Modules" label. A module may join a core group instead:main(Dashboard 10, Customers 20, Items 30),documents(Estimates 10, Invoices 20, Payments 30, Expenses 40) oradmin(Members 20, Reports 30, Settings 40).priority: an integer; lower sorts first inside the group, default100. Entries with equal priority keep registration order, so official modules set explicit values (10,20, ...). Groups keep the order the host sends them in: the core groups first, then module groups in registration order, so a low priority never lifts a module group above the core ones.
Registry::registerMenu('tasks-projects', [
'title' => 'tasksprojects::menu.title',
'link' => '/admin/modules/tasks-projects',
'icon' => 'ClipboardDocumentListIcon',
'priority' => 10,
]);Registry::registerAbility() adds a module's own permissions to the host's ability catalogue. Every
ability is stored namespaced as {slug}:{ability}, so a module can never collide with a host ability
or with another module:
Registry::registerAbility('sales-tax-us', [
'ability' => 'view-filing',
'name' => 'View Tax Filings',
'depends_on' => ['view-invoice', Registry::abilityId('sales-tax-us', 'view-rate')],
'owner_only' => false,
]);ability is plain kebab-case and name is the label shown in the role editor. depends_on lists
abilities implied by this one: host abilities in plain form (view-invoice), the module's own in
namespaced form via Registry::abilityId(). Module abilities are never model-scoped.
The host grants a module's abilities to owner roles when the module is enabled and removes them when
it is uninstalled; other roles get them through the role editor. Frontend pages must gate on the same
namespaced id through meta.ability (see below), and re-registering an identical entry is a no-op
while a conflicting redefinition throws.
extensions.registerPage() mounts a module page at /admin/modules/{slug}/{path}, where {slug} is
the module.json slug. Declare meta.ability with the namespaced ability id so the host route guard
can check it, and add children for sub-routes relative to the page. The settings path is reserved
by the host for the schema-rendered settings page.
Point the Registry::registerMenu link at /admin/modules/{slug} for a module with its own pages,
and at /admin/modules/{slug}/settings for a settings-only module.
registerPage, Registry::registerAbility, and the CompanyDataReader::companyMembers() and
existingInvoiceIds() queries are Module API 1.3.0 additions. Declare
"module_api": "^1.3.0" in module.json before using them.
The InvoiceShelf mobile clients run the same frontend as the web app, but as a static app package pointed at whichever server the user signed in to. A module that follows the SDK already works there; nothing about the SDK, the manifest or the install flow changes for it. Four rules are what make that true:
- Call the API only through
extensions.client. Never a barefetch, never an axios instance of your own. Only the host client carries the server base URL and the bearer token, so in a clientfetch('/api/v1/...')resolves against the app package, where there is no server and no credentials to send. - Never hardcode
/apior/storageas a root-relative URL in a template or in code. Root-relative is not server-relative in a client: the root is the app package. A path handed toextensions.clientis fine, because the host client resolves it against the server. For anything else, such as a stored file, the host exposes no asset-URL helper today, so ask for one rather than assembling a URL in the module. - Ship self-contained CSS. The host's release bundle is built in CI with no
Modules/directory present, so the host's Tailwind scan never sees module markup and cannot emit a class for it. Everything your markup needs must come out of your own compiled stylesheet. The host's reset and its theme custom properties are shared and may be relied on; a host utility class may not. - Register scripts and styles as local paths, never as remote http(s) URLs. An operator cannot add CORS headers to an origin they do not control, so the client manifest flags such a script as unsupported and clients skip it. The module then keeps working on the web and is silently absent on mobile.
A client loads modules from the server it is connected to, at boot. It reads a public manifest
listing each enabled module's script and style as absolute, versioned URLs, adds one
<link rel="stylesheet"> per style, then imports the scripts one at a time, in manifest order,
before window.InvoiceShelf.start(). The order is fixed and the imports are sequential because
modules register through window.InvoiceShelf.booting() and must see the same order the web
shell gives them. Each import runs under its own timeout, and a module that fails or times out is
reported and skipped rather than allowed to block boot, so a module broken in a client costs its
own features and not the whole app.
The full contract, including the client manifest, the CORS rules and the version gates on the server
side, is thin-clients.md in the private InvoiceShelf specs repository.
Validate both the manifest and the distributable package before every release:
vendor/bin/invoiceshelf-module validate-module module.json
vendor/bin/invoiceshelf-module validate-package .validate-package checks the manifest, providers, migrations, declared assets, and the closed host-provided dependency policy. The CLI also validates and canonicalizes generated release-manifest.json files.
Every release is a deterministic signed ZIP, built in CI from an exact, unprefixed SemVer tag matching module.json (for example, 1.2.3). The reusable workflow validates source and compiled assets, creates the package and signed release manifest, then registers it with the InvoiceShelf marketplace.
See RELEASING.md for the protected GitHub environment configuration and release workflow details. Never commit a signing key or marketplace token.
This SDK is MIT-licensed; see LICENSE.md. Official packages generated from its stubs default to AGPL-3.0-only and must include the source required to reproduce every shipped dist/ asset.