Adding Features
Adding Features
Add a feature to ModulaCMS by following the standard flow from schema design through deployment.
Determine the Scope
Before writing code, determine the scope:
- Feature stores new data in an existing table -- Add a column to the existing schema, update queries, regenerate sqlc code.
- Feature introduces a new domain concept -- Create a new table with the full workflow described in Adding Tables.
- Feature uses existing data in a new way -- Skip directly to business logic. No schema or sqlc changes needed.
Follow the Development Flow
Schema Design (if needed)
|
SQL Files (schema + queries)
|
sqlc Code Generation
|
DbDriver Interface Update
|
Driver Implementations (SQLite, MySQL, PostgreSQL)
|
Business Logic
|
TUI Interface (if needed)
|
HTTP/API Endpoints (if needed)
|
Admin Panel Pages (if needed)
|
Testing
|
Deployment
Not every feature touches every layer. A read-only export feature skips the schema and sqlc steps. A background job skips the TUI and API steps. Follow the flow and skip layers that do not apply.
Step 1: Write Schema and Queries
If the feature requires database changes, follow the Adding Tables guide for new tables, or add ALTER TABLE statements to a new migration directory for column additions.
Key points:
- Create migrations for all three databases (SQLite, MySQL, PostgreSQL)
- Write sqlc-annotated queries for all three databases
- Update the combined schema files
- Run
just sqlcto generate Go code
Step 2: Update the DbDriver Interface
If new queries were added in Step 1, add the new methods to the DbDriver interface in internal/db/db.go. Then implement those methods on all three driver structs: Database (SQLite), MysqlDatabase, and PsqlDatabase.
Each implementation maps between sqlc-generated types and application-level types, handling NULL conversions and type width differences between database engines.
Step 3: Implement Business Logic
Place domain logic in the appropriate location:
- Simple CRUD -- Handled by the driver implementations from Step 2.
- Domain rules and validation --
internal/model/ - HTTP request handling --
internal/router/ - TUI interaction --
internal/tui/
Use structured logging at decision points and error paths.
Step 4: Build the TUI Interface
If the feature needs user interaction in the SSH TUI:
- Define message types for the feature's events.
- Add keyboard commands in the Update function.
- Create command functions that perform async operations and return messages.
- Update the View to render the new feature's state.
For entirely new screens, create a new model file in internal/tui/. For additions to existing screens, modify the relevant model's Update and View functions.
Step 5: Add HTTP/API Endpoints
If the feature needs REST API access:
- Create handler functions in
internal/router/. - Register routes in
internal/router/mux.gowith appropriate permission middleware. - Add permission labels if the feature needs new permissions -- add them to the bootstrap data.
Good to know: All admin endpoints must be wrapped with
RequireResourcePermissionorRequirePermissionmiddleware.
Step 6: Create Admin Panel Pages
If the feature needs a web admin interface:
- Create templ templates in
internal/admin/pages/andinternal/admin/partials/. - Create handlers in
internal/admin/handlers/. - Register routes in the
registerAdminRoutes()function. - Regenerate templates with
just admin generate.
Step 7: Write Tests
Every feature needs tests. At minimum:
- Unit tests for business logic and validation functions
- Database tests for new CRUD operations (using SQLite test databases)
- Manual testing of TUI commands via SSH
- Manual testing of API endpoints with curl
# Run all tests
just test
# Run specific package
go test -v ./internal/db -run TestCommentCRUD
# Run with coverage
just coverage
# Run with race detector
go test -race ./...
Testing Checklist
- Unit tests for all business logic
- Database CRUD operations tested
- Error cases handled and tested (invalid input, missing records, constraint violations)
- TUI commands tested manually
- API endpoints tested with curl
just testpassesjust lintpasses
Step 8: Deploy the Feature
Build and verify locally before deploying:
# Build for local testing
just dev
modula
# Run the full test suite
just test
# Build for production
just build
Test the feature locally by connecting to the TUI via SSH and hitting the API endpoints. Then deploy following the standard deployment process (push to dev branch for CI, or manual deploy with just build).
Common Patterns
Adding a Column to an Existing Table
- Create migration directory:
sql/schema/N_feature_name/ - Write ALTER TABLE statements for all three databases
- Add or update queries
- Run
just sqlc - Update DbDriver interface if new queries were added
- Implement on all three drivers
- Update business logic and interfaces
- Test and deploy
Creating a New Table
Follow Adding Tables for the full schema-to-code workflow, then continue with business logic, interfaces, and testing from this guide.
Read-Only Feature (No Database Changes)
- Implement business logic
- Add TUI interface or HTTP endpoints
- Test and deploy
Background Job
- Implement the job logic
- Add configuration fields to
modula.config.jsonif needed - Register the job in the server startup flow
- Add logging for monitoring
- Test and deploy
Avoid Common Pitfalls
Forgetting to implement on all three database drivers. The feature works in SQLite during development but fails with MySQL or PostgreSQL in production. Implement DbDriver methods on all three structs.
Not updating combined schema files. Fresh installations use all_schema*.sql. Missing tables cause failures on new deployments.
SQL dialect differences between databases. MySQL uses ? placeholders, PostgreSQL uses $1, $2, $3. MySQL does not support RETURNING. Test queries against all backends.
Breaking the TUI message loop. Returning nil for tea.Cmd when a command is expected causes the TUI to stop responding. Return the appropriate command from Update.
Missing permission guards on API endpoints. Every admin endpoint must be wrapped with permission middleware. Unguarded endpoints bypass RBAC.