Authoring a module
Creates modules/bank/ with a ready-to-go shape. Open
modules/merinaa.config.ts, import the module and add it to the array
— that's it.
Folder convention
modules/bank/
├── module.config.ts manifest (metadata, pages, overlays)
├── server/
│ ├── index.ts default-exports the @Module class
│ ├── bank.module.ts @Module
│ ├── bank.controller.ts @Controller
│ ├── bank.service.ts @Injectable
│ ├── bank.guard.ts @Guard (optional)
│ └── entities/ @entity classes for the ORM
├── client/
│ ├── index.ts side-effect entry
│ └── bank.client.ts decorator-based client controller
├── shared/ types used by both server and client
└── ui/
├── pages/ page components (one per registered page)
├── overlays/ overlay components
└── components/ module-local reusable React bits
Server side
// bank.service.ts
import { Injectable, LoggerService } from '@merinaa/core';
@Injectable()
export class BankService {
constructor(private logger: LoggerService) {}
deposit(accountId: number, amount: number): void {
this.logger.info(`[Bank] +$${amount} to ${accountId}`);
}
}
// bank.controller.ts
import { Controller, OnClient, UseGuards } from '@merinaa/core';
import { BankService } from './bank.service';
import { BankGuard } from './bank.guard';
@Controller()
@UseGuards(BankGuard)
export class BankController {
constructor(private bank: BankService) {}
@OnClient('bank:deposit')
deposit(src: number, accountId: number, amount: number): void {
this.bank.deposit(accountId, amount);
}
}
// bank.module.ts
@Module({
providers: [BankService, BankController, BankGuard],
exports: [BankService],
})
export class BankModule {}
// index.ts
export { BankModule as default } from './bank.module';
Client side
// client/bank.client.ts
import { showPage, hidePage } from '@merinaa/client';
RegisterCommand('bank', () => {
showPage('bank', { accountId: 42 });
}, false);
showPage('bank', data) handles all of the following for you:
SetNuiFocus(true, true)(per thefocusflag in module.config)- Starts a
DisableControlActiontick loop for movement/camera - Binds Escape to
hidePage()(percloseOnEscape) - Dispatches
{ type: 'bank:open', accountId: 42 }over NUI
UI side
Pages are always mounted — preloaded by createUI() before the
first render. The component manages its own visibility via its internal
useNuiEvent subscriptions. This is important: a page that uses
useState(false) and only flips to true inside useNuiEvent('bank:open', ...)
will render nothing until the event fires.
// ui/pages/BankPage.tsx
import { useNuiEvent } from '@merinaa/ui/hooks';
import { useState, useCallback } from 'react';
export default function BankPage() {
const [visible, setVisible] = useState(false);
const [data, setData] = useState<any>(null);
useNuiEvent('bank:open', useCallback((payload) => {
setData(payload);
setVisible(true);
}, []));
useNuiEvent('bank:close', useCallback(() => {
setVisible(false);
}, []));
if (!visible) return null;
return <div>...</div>;
}
Overlays
Overlays live in ui/overlays/ and are registered under
ui.overlays in module.config.ts.
-
alwaysMounted: true(default) — component mounts at app start, manages visibility internally. Use this for HUDs, persistent widgets, death screens. -
alwaysMounted: false— component mounts/unmounts onoverlay:show:<id>/overlay:hide:<id>events dispatched byshowOverlay(id)/hideOverlay(id)on the client.
Where to next
- Pages & overlays in detail
- Security — guards, rate limiting
- Database & migrations
- Lifecycle hooks