Skip to content

Cheat sheet

One page of the things you look up most. Every item links to the page that explains it.

Where files go

What Path in your module
Grid definition view/adminhtml/grid/<id>.json
Form definition view/adminhtml/form/<id>.json ("type": "eav" for EAV forms)
Snippet view/adminhtml/snippet/<id>.json plus a .phtml under view/adminhtml/templates/
Skin view/adminhtml/nebula/skin/<id>.json
Layout handle that mounts a grid view/adminhtml/layout/<route>_<controller>_<action>.xml
Aliases and renderers etc/di.xml
Module stylesheet and script view/adminhtml/web/css/*.css, view/adminhtml/web/js/*.js, loaded from layout <head>

The same id in two modules is an overlay, merged in module load order. The definition engine

Minimal grid

{
  "id": "vendor_thing_listing",
  "acl": "Vendor_Module::thing",
  "dataSource": { "provider": "grid.collection", "config": { "collection": "vendor_thing.collection" } },
  "settings": { "pageSize": 20, "defaultSort": { "field": "thing_id", "direction": "asc" } },
  "columns": {
    "thing_id": { "label": "ID", "type": "text", "sortable": true, "filter": "range", "position": 10 },
    "name":     { "label": "Name", "type": "text", "sortable": true, "filter": "text", "searchable": true, "position": 20 }
  }
}
<referenceContainer name="content">
    <block class="Nebula\Grid\Block\Grid" name="nebula.vendor_thing_listing">
        <arguments><argument name="grid_id" xsi:type="string">vendor_thing_listing</argument></arguments>
    </block>
</referenceContainer>

Your first grid · Grid reference · Grid examples

Minimal form

{
  "id": "vendor_thing_edit",
  "acl": "Vendor_Module::thing",
  "route": "vendor_module_thing_edit",
  "dataSource": { "provider": "vendor_thing.form", "config": { "identifierParam": "thing_id" } },
  "settings": { "title": "Thing", "identifierField": "thing_id", "backUrl": "vendor_module/thing/index" },
  "fieldsets": {
    "general": {
      "label": "General", "position": 10, "open": true,
      "fields": {
        "thing_id": { "type": "hidden", "position": 10 },
        "name":     { "label": "Name", "type": "text", "required": true, "position": 20 }
      }
    }
  }
}

saveUrl defaults to */*/save. Your first form · Form reference

Overlay recipes

Do this Write this
Add a column { "columns": { "my_col": { "label": "Mine", "type": "text", "position": 55 } } }
Relabel a field { "fieldsets": { "general": { "fields": { "title": { "label": "Name" } } } } }
Remove a column, field or fieldset { "columns": { "old": { "$remove": true } } }
Remove one item of an id-keyed list { "settings": { "massActions": [ { "id": "delete", "$remove": true } ] } }
Change a field's type { "fieldsets": { "content": { "fields": { "content": { "type": "textarea" } } } } }
Add a snippet panel { "fieldsets": { "mine": { "label": "Mine", "position": 25, "renderer": "snippet.my_panel" } } }

Never write an empty object; "columns": {} deletes the map. Sequence your module after the one you overlay. Grid overlays · Form overlays

DI bindings

<type name="Nebula\Grid\Model\Registry\CollectionRegistry">
    <arguments><argument name="bindings" xsi:type="array">
        <item name="vendor_thing.collection" xsi:type="string">Vendor\Module\Model\ResourceModel\Thing\Collection</item>
    </argument></arguments>
</type>
<type name="Nebula\Component\Model\Registry\DataProviderRegistry">
    <arguments><argument name="bindings" xsi:type="array">
        <item name="vendor_thing.form" xsi:type="string">Vendor\Module\Model\DataProvider\ThingFormDataProvider</item>
    </argument></arguments>
</type>
<type name="Nebula\Component\Model\Registry\OptionSourceRegistry">
    <arguments><argument name="bindings" xsi:type="array">
        <item name="vendor.thing.status" xsi:type="string">Vendor\Module\Model\Source\Status</item>
    </argument></arguments>
</type>
<type name="Nebula\Component\Model\SnippetViewModelRegistry">
    <arguments><argument name="viewModels" xsi:type="array">
        <item name="my_panel" xsi:type="string">Vendor\Module\ViewModel\MyPanel</item>
    </argument></arguments>
</type>

Definitions name aliases, never classes, and every block above belongs in etc/di.xml: an argument declared in etc/adminhtml/di.xml replaces the global one for that area and drops every other module's bindings — nebula:doctor reports it. Extension points

Admin routes

Nine admin front names ship. Nebula_Core declares the nebula front name and four more modules join it, each declaring <module name="..." before="Nebula_Core"/> in its own etc/adminhtml/routes.xml, so nebula/grid/*, nebula/form/*, nebula/merchandiser/* and nebula/taxrate/* sit side by side under one front name.

Front name Module Serves
nebula Nebula_Core, joined by Nebula_Grid, Nebula_Form, Nebula_Theme and Nebula_Merchandiser Grid and form endpoints, menu pinning, the merchandiser, and the entity screens Nebula replaces such as tax rates, ratings and agreements
nebuladirective Nebula_Directive The variable and widget pickers the editors open
nebulaemail Nebula_EmailTemplateEditor The email template editor, its preview and its test send
nebula_landing Nebula_LandingPage Landing page screens
nebulamedia Nebula_Media The media library screen and the browser's JSON calls
nebulamenu Nebula_Menu Nothing in the shipped admin; pinning goes through nebula/menupin
nebulasystem Nebula_System Admin notifications, custom variables, grid export filters
nebulapasskey Nebula_Passkey Admin passkey enrolment, sign-in and management
nebula_theme_switcher Nebula_ThemeSwitcher Switching the whole admin between Nebula and another registered admin theme

Two storefront front names ship as well. nebula_landing serves landing pages, and passkey serves the customer passkey screens under the route id nebulapasskey.

JSON endpoints

Endpoint Verb Guarded by
nebula/grid/data?id=<grid_id> GET the acl of the resolved grid definition
nebula/grid/export?grid_id=<grid_id>&format=csv GET the acl of the resolved grid definition
nebula/selection/search?source=<alias> GET getAclResource() on the registered selection source
nebula/form/save POST the acl of the resolved form definition
nebula/form/validate POST the acl of the resolved form definition
nebula/menupin POST Magento_Backend::admin
nebula/menupin/reorder POST Magento_Backend::admin

The grid parameters are the request contract in Grid reference. Selection search is paged and returns 100 rows at most, described under Extension points. Save and validate both take a JSON body. Save answers {"success": true, "entityId": 12}, and validate answers {"valid": true} or {"valid": false, "errors": {...}}. Saving and validation

nebula/menupin toggles one pin from a posted item_id and answers {"success": true, "pinned": true, "item_id": "..."}. nebula/menupin/reorder takes order, a JSON-encoded array of menu item ids in the new order. Both URLs are built with _nosecret, so neither carries the admin secret key segment. Both are still POSTs, so send form_key in the body. Both store against the signed-in administrator's own user record, which is why one administrator's pins never show up for another. Navigation

One pin endpoint, not two

Nebula_Menu also ships nebulamenu/pin/index. It takes the same item_id and writes the same storage, but nothing in the shipped admin calls it. Call nebula/menupin.

Saved grid views

Read or seed a saved view through the repository rather than writing to the nebula_grid_view table.

use Nebula\Grid\Api\V1\Data\GridViewInterfaceFactory;
use Nebula\Grid\Api\V1\GridViewRepositoryInterface;

$view = $this->gridViewFactory->create()
    ->setAdminUserId($adminUserId)
    ->setGridId('vendor_thing_listing')
    ->setName('Open items')
    ->setIsDefault(true)
    ->setSortOrder(10)
    ->setConfig((string) json_encode(['filters' => ['status' => 'open']]));

$this->gridViewRepository->save($view);

The repository has four methods: save(), getById(int $viewId), deleteById(int $viewId) and getListForUser(int $adminUserId, string $gridId). A view belongs to one grid and one administrator, which is why the listing call takes both. config is the whole view state as a JSON string. Both interfaces are bound by <preference> in code/Nebula/Grid/etc/di.xml, and neither carries an @api tag yet, so pin your version if you depend on them. Saved views

Commands

Command When
bin/magento nebula:lint After writing or overlaying a definition; in CI
bin/magento nebula:compile Production mode, after deploy; --verify in CI
bin/magento nebula:doctor Before a support request
bin/magento nebula:crud:generate <table> --module=Vendor_Module --dry-run Scaffold a CRUD module
bin/magento nebula:ui:transform <path> Convert UI Component XML to draft definitions
bin/magento nebula:passkey:reset-two-step --user=<user> Unlock an administrator

CLI reference

Cache cleans

After changing Run
A definition (default mode) bin/magento cache:clean nebula_definitions
A definition (production mode) bin/magento nebula:compile
route, replaces, routeWhen the above, plus bin/magento cache:clean layout block_html
A skin or the Color Scheme setting bin/magento cache:clean config
Nothing, in developer mode definitions are read live

Caching and production mode

Template variables in a snippet

Grid cell Form section
record, nebula_context = grid, view_model, column, key, item, value record, nebula_context = form, view_model, entity_data, form_block, section

Snippets

Module stylesheet and script

view/adminhtml/web/css/thing.css       # loaded from the layout of the screen it belongs to
.vendor-thing { background: var(--nebula-bg-secondary); color: var(--nebula-text-primary); border: 1px solid var(--nebula-border-default); }
<!-- view/adminhtml/layout/default.xml: your script, after the core bundle -->
<referenceContainer name="before.body.end">
    <block class="Magento\Framework\View\Element\Template" name="vendor.module.scripts"
           template="Vendor_Module::scripts.phtml" after="nebula.component.scripts"/>
</referenceContainer>
document.addEventListener('alpine:init', () => { Alpine.data('vendorThing', () => ({ open: false })); });

No LESS, no RequireJS, and no Tailwind utilities from a module's own markup — the theme does not scan your templates, so the class is emitted nowhere. Plain CSS over the tokens instead. Translations: JsPhrasesProviderInterface → window.Nebula.t(). Types: web/types/nebula.d.ts. CSS and JavaScript in your module

Config paths you will set

Path Purpose
nebula/theme/skin Color scheme id
nebula/theme/menu_position sidebar, top or magento (the icon rail)
nebula_passkey/general/rp_id Relying Party ID for passkeys
twofactorauth/general/force_providers google,nebula_passkey by default
nebula_theme_switcher/general/enabled and active_theme The admin theme every administrator gets, set at default scope
csp/mode/admin/report_only 0 to enforce the admin CSP

Configuration reference