Security
FiveM is a hostile environment — every @OnClient event handler is a
public HTTP endpoint in all but name. Merinaa ships three building blocks
you should know about.
Guards — authorization
Guards are classes that implement canActivate(context) and return
boolean. Attached via @UseGuards(SomeGuard) on a controller or a
specific handler, they run before the handler. Return false and the
handler is skipped silently.
Rate limiting
RateLimitService is a built-in token-bucket limiter. Configure it in
framework.config.yaml (or app.framework.rateLimit in merinaa.config.ts)
— if any rateLimit.* key is set, bootstrapServer() automatically
registers the global RateLimitGuard.
# framework.config.yaml
rateLimit:
windowMs: 1000 # default window
max: 20 # default max calls per window per source
events:
bank:deposit:
windowMs: 5000
max: 3 # tighter — real money flow
admin:kick:
windowMs: 1000
max: 1
Throttled calls are logged at warn level and silently rejected — no error propagates to the client.
Input validation
Merinaa doesn't validate @OnClient arguments for you. The client can
send anything. Two defensive patterns:
1. Pipes — for transforming + validating arguments:
@OnClient('bank:deposit')
deposit(
src: number,
@Body(new ValidationPipe({ min: 1, max: 100_000 })) amount: number,
) {
// amount is guaranteed to be 1..100_000 here
}
2. Deepkit runtime types — combined with Deepkit's type-compiler, you can validate full object shapes:
interface DepositRequest {
accountId: string & MinLength<1>;
amount: number & Positive & Maximum<100_000>;
}
@OnClient('bank:deposit')
deposit(src: number, request: DepositRequest) {
// Deepkit + ValidationPipe rejects malformed requests
}
Common pitfalls
-
Trusting the source of truth —
@Source() srcis the only thing you can trust from a client call. Don't accept a player id as a parameter and skip authorization. -
Admin commands registered via
RegisterCommandrun in the FiveM command dispatcher, not via@OnClient— they bypass guards. If you register an admin command client-side, verify admin status on the server via net-events, not just in the command body. -
NUI callbacks (
RegisterNuiCallbackType) also run outside the@OnClientflow and bypass guards. If your UI has a "make me admin" button, authorize on the server.