Extending the App

Add a new CRUD module, permission, setting, or scheduled task, following the app's own conventions.

On this page

Extending the App

CBGenesis is a launchpad, not a finished product. These are the same steps its own Users/Roles/Permissions/Settings modules follow - use them as the template for anything new.

Building with an AI agent

If you're extending cbGenesis with an AI coding agent (Claude Code, Copilot, Cursor, or similar), point it at .agents/skills-custom/ before it writes any code - these skills encode the exact steps below as machine-readable instructions, with real code excerpts from this codebase, so the agent doesn't have to reverse-engineer them by exploring every handler:

SkillCovers
cbgenesis-crud-resourceThe full vertical slice below - entity, service, handler, route, view, component - end to end.
cbgenesis-rbac-permissionsThe resource:action permission model, @secured, and self-action guards.
cbgenesis-csrf-frontendThe mandatory fetchWithCsrf() pattern for any mutating frontend request.
cbgenesis-alpine-componentsAlpine.js component shape, registration, and the shared utils/ library.
cbgenesis-testing-conventionsBaseIntegrationSpec, the real transaction-rollback isolation mechanism, and fixture helpers.
cbgenesis-settings-configWhen to use an environment variable versus the DB-backed settings registry.

New convention worth an agent (or a human) not having to rediscover it by trial and error? Add it as a new skill here rather than leaving it as tribal knowledge in a PR description. See Built for AI-Assisted Development for why this matters and a measured before/after comparison.

Adding a new CRUD module

1
Create the entity

In app/models/<domain>/, extending BaseEntity — see Database & ORM.

2
Create the service

Extending BaseService, marked singleton threadSafe — see the service pattern.

3
Create the handler

Extending BaseSecureHandler, with an @secured annotation — see Handlers & Routing. Inheriting that base means every POST/PUT/DELETE action you add is CSRF-verified automatically; there is nothing to opt into, but your forms and Alpine components must send rc.csrf.

4
Add routes

In app/config/Router.bx, near the // @app_routes@ marker.

5
Create views

In app/views/<domain>/, reusing existing _components/ui/ partials.

6
Create an Alpine component

In resources/assets/js/components/<domain>/, then register it in App.js — see Frontend.

7
Add SCSS

In resources/assets/scss/views/, imported from app.scss.

8
Write tests

Unit specs in tests/specs/unit/<domain>/, plus an integration spec in tests/specs/integration/ for the routes you added — see Testing.

Adding a new permission

1
Seed the slug

Add the resource:action slug to resources/database/seeds/AdminData.bx and assign it to the appropriate role(s).

2
Guard the handler

@secured( "resource:action,resource:admin" ) — comma means OR. See Security & Permissions.

3
Gate the view
<bx:if prc.authUser.hasPermission( "resource:action,resource:admin" )>

so the UI never offers something the handler would reject.

4
Re-seed

box migrate seed run against an existing database - or grant the permission to a role directly from the Roles admin page.

Adding a setting

Add a new key to the DEFAULTS struct in SettingService.bx. preFlightCheck() seeds it automatically on next boot, and it appears in the /settings admin page with no further wiring — see Configuration.

Customizing layouts

Layouts live in app/layouts/. Selection happens per-handler, typically in preHandler:

function preHandler( event, rc, prc ){
    event.setLayout( "Admin" );
}

Adding a scheduled task

Register tasks in app/config/Scheduler.bx, next to the three that already run there - see Architecture for what they do:

task( "My Task" )
    .call( () => getInstance( "MyService" ).doWork() )
    .everyDayAt( "03:45" )
    .onOneServer()
    .withNoOverlaps();

onOneServer() and withNoOverlaps() matter the moment you deploy more than one instance: without them, every instance runs the task on its own schedule. Put the actual purge/cleanup logic on the service (doWork() above), not inline in the closure, so it stays unit-testable.

Overriding module configuration

Module configs in app/config/modules/ extend the module's own defaults. Override any key there — changes take effect on the next ?fwreinit.

Edit this page Download Markdown Last updated Oct 13, 2026, 9:08:05 PM