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:
| Skill | Covers |
|---|---|
cbgenesis-crud-resource | The full vertical slice below - entity, service, handler, route, view, component - end to end. |
cbgenesis-rbac-permissions | The resource:action permission model, @secured, and self-action guards. |
cbgenesis-csrf-frontend | The mandatory fetchWithCsrf() pattern for any mutating frontend request. |
cbgenesis-alpine-components | Alpine.js component shape, registration, and the shared utils/ library. |
cbgenesis-testing-conventions | BaseIntegrationSpec, the real transaction-rollback isolation mechanism, and fixture helpers. |
cbgenesis-settings-config | When 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
In app/models/<domain>/, extending BaseEntity — see Database & ORM.
Extending BaseService, marked singleton threadSafe — see the service pattern.
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.
In app/config/Router.bx, near the // @app_routes@ marker.
In app/views/<domain>/, reusing existing _components/ui/ partials.
In resources/assets/js/components/<domain>/, then register it in App.js — see Frontend.
In resources/assets/scss/views/, imported from app.scss.
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
Add the resource:action slug to resources/database/seeds/AdminData.bx and assign it to the appropriate role(s).
@secured( "resource:action,resource:admin" ) — comma means OR. See Security & Permissions.
<bx:if prc.authUser.hasPermission( "resource:action,resource:admin" )>
so the UI never offers something the handler would reject.
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.