npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

yabosfivemrpc

v0.1.1

Published

A secure, Promise-based RPC library for FiveM TypeScript/JavaScript resources.

Downloads

12

Readme

FiveM-RPC

A small, secure, Promise-based RPC library for FiveM TypeScript and JavaScript resources.
Call code in any context — server, client, or browser (NUI) — and get the result back as a plain Promise. No manual event wiring, no response-routing boilerplate, no missed callbacks.


Table of Contents


Installation

Step 1 — Download the resource

Go to the Releases page and download the latest YabosFiveMRPC.zip.
Extract it — you will get a folder called YabosFiveMRPC.

Step 2 — Add to your server

Place the YabosFiveMRPC folder inside your server's resources/ directory and add this line to your server.cfg:

ensure YabosFiveMRPC

Step 3 — Declare the dependency

In your own resource's fxmanifest.lua, add:

dependency 'YabosFiveMRPC'

Step 4 — Use it in your scripts

Server-side — import directly in your compiled/bundled server script:

import { registerRpc, callClient } from 'path/to/YabosFiveMRPC/server/server.js';

Client-side — import directly in your compiled/bundled client script:

import { registerRpc, callServer, callBrowser } from 'path/to/YabosFiveMRPC/client/client.js';

Browser (NUI) — two options depending on your setup:

Option A — Plain <script> tag (no bundler)

<script src="../../YabosFiveMRPC/browser/browser.js"></script>
<script>
  // Exposed as window.FiveMRPC
  const { registerRpc, callClient } = FiveMRPC;

  registerRpc('showMenu', () => {
    document.getElementById('menu').style.display = 'block';
  });
</script>

Option B — ESM import (Vite, webpack, esbuild, or any bundler)

import { registerRpc, callClient } from '../../YabosFiveMRPC/browser/browser.esm.js';

registerRpc('showMenu', () => {
  document.getElementById('menu').style.display = 'block';
});

Step 5 — Your NUI manifest

FiveM-RPC does not ship an index.html — it is a communication library, not a UI. You provide your own HTML page.

In your resource's fxmanifest.lua, declare your own UI page and include browser.js (or browser.esm.js) in your files:

-- your-resource/fxmanifest.lua
fx_version 'cerulean'
game 'gta5'

dependency 'YabosFiveMRPC'

client_scripts { 'client.js' }
server_scripts { 'server.js' }

ui_page 'html/index.html'

files {
  'html/index.html',
  'html/app.js',
  -- copy browser.js from YabosFiveMRPC into your own html/ folder,
  -- or reference it via a relative path if FiveM allows it in your setup
  'html/browser.js',
}

The simplest approach is to copy YabosFiveMRPC/browser/browser.js into your own resource's HTML folder as part of your build step.


How It Works

The Problem It Solves

In a standard FiveM resource, getting data from the server to the browser (NUI) requires manually wiring up four separate events:

Browser  →  (PostMessage)     →  Client  →  (emitNet)       →  Server
Server   →  (emitNet)         →  Client  →  (SendNUIMessage) →  Browser

Every request needs its own event name pair, its own response routing, and its own logic to match a response to the correct caller. When multiple requests happen simultaneously, it is extremely easy to route a response to the wrong caller.

FiveM-RPC wraps all of that into a single function call that returns a Promise:

// Instead of all that event wiring — just this:
const plates = await callServer('getAllLicensePlates');

Any context (server, client, or browser) can call a procedure registered on any other context and await its return value directly.


Architecture

The library ships three separate self-contained bundles — one per FiveM context:

| File | Context | Format | |---|---|---| | server/server.js | Server-side scripts | Node CJS bundle | | client/client.js | Client-side scripts | Node CJS bundle | | browser/browser.js | NUI pages (script tag) | IIFE — exposes window.FiveMRPC | | browser/browser.esm.js | NUI pages (bundler) | ESM — named exports |

Each bundle is fully self-contained. There are no shared files and no inter-bundle imports at runtime. All internal modules (core, shared) are inlined into each bundle by esbuild at build time.


Message Flow

Client → Server

Client calls callServer('myProcedure', payload)
  │
  ├─ Creates a pending Promise internally, keyed by a unique ID
  └─ Sends emitNet('fivem-rpc:server:request', envelope)

Server receives the event
  ├─ Validates the envelope (age, resource name, payload size)
  ├─ Runs the registered handler
  └─ Sends emitNet('fivem-rpc:server:response', playerId, result)

Client receives the response event
  ├─ Matches the response ID to the pending Promise
  └─ Resolves or rejects it

Server → Client

Server calls callClient(playerId, 'myProcedure', payload)
  │
  ├─ Creates a pending Promise tagged with expected playerId
  └─ Sends emitNet('fivem-rpc:client:request', playerId, envelope)

Client receives the event
  ├─ Validates the envelope
  ├─ Runs the registered handler
  └─ Sends emitNet('fivem-rpc:client:response', result)

Server receives the response
  ├─ Verifies the responder's playerId matches the expected one (anti-spoof)
  ├─ Matches ID to the pending Promise
  └─ Resolves or rejects it

Client → Browser (NUI)

Client calls callBrowser('myProcedure', payload)
  │
  ├─ Creates a pending Promise
  └─ Sends SendNUIMessage({ type: 'fivem-rpc:nui:request-message', ... })

Browser receives via window.addEventListener('message', ...)
  ├─ Validates the message type and resource name
  ├─ Runs the registered handler
  └─ POSTs result to https://<resource>/fivem-rpc:nui:response

Client receives via RegisterNuiCallbackType
  └─ Resolves or rejects the pending Promise

Browser → Client (NUI)

Browser calls callClient('myProcedure', payload)
  │
  └─ POSTs to https://<resource>/fivem-rpc:nui:request

Client receives via RegisterNuiCallbackType
  ├─ Runs the registered handler
  └─ Returns result directly in the POST response body

Browser receives the fetch response
  └─ Resolves or rejects the Promise

Note: Browser → Server is not a direct path. The browser always goes through the client (browser → client → server). This is intentional and matches FiveM's NUI security model — the browser cannot emit net events directly.


Quick Start

Server registers a procedure, client calls it

Server

import { registerRpc } from '...server.js';

registerRpc('getPlayerMoney', (payload, ctx) => {
  return getPlayerMoney(ctx.playerId!);
});

Client

import { callServer } from '...client.js';

const money = await callServer('getPlayerMoney');
console.log(`You have $${money}`);

Server calls a client procedure

Client

import { registerRpc } from '...client.js';

registerRpc('isPlayerDriving', () => {
  return IsPedInAnyVehicle(PlayerPedId(), false);
});

Server

import { callClient } from '...server.js';

const driving = await callClient(source, 'isPlayerDriving');

Browser calls client, client calls server

Server

registerRpc('getServerTime', () => Date.now());

Client

registerRpc('getServerTime', () => callServer('getServerTime'));

Browser

const { callClient } = FiveMRPC; // or import from browser.esm.js

const time = await callClient('getServerTime');
document.getElementById('time').textContent = new Date(time).toLocaleString();

Fire-and-forget (no return value needed)

Use trigger* variants when you don't need a response back. They send the call and return immediately — no Promise, no await.

// Client
triggerServer('logEvent', { type: 'entered', zone: 'hospital' });

// Server
triggerClient(playerId, 'showNotification', { message: 'Welcome!' });

// Client
triggerBrowser('updateHUD', { health: 87, armor: 100 });

API Reference

Server

registerRpc(name, handler, options?)

Register a procedure on the server that any client or the server itself can call.

registerRpc('getWeather', () => GetWeatherTypeHashName(GetPrevWeatherTypeHashName()));

unregisterRpc(name)

Remove a registered procedure. Returns true if it existed.

call(name, payload?, options?)

Call a procedure registered on the same server (local — no network round-trip).

callClient(playerId, name, payload?, options?)

Call a procedure registered on a specific client and await the result.

const pos = await callClient(source, 'getPosition');

triggerClient(playerId, name, payload?)

Fire-and-forget version of callClient.


Client

registerRpc(name, handler, options?)

Register a procedure on the client that the server or browser can call.

unregisterRpc(name)

Remove a registered procedure.

call(name, payload?, options?)

Call a procedure on the same client (local).

callServer(name, payload?, options?)

Call a procedure registered on the server.

const inventory = await callServer('getInventory');

callBrowser(name, payload?, options?)

Call a procedure registered in the NUI browser.

const value = await callBrowser('getInputValue');

triggerServer(name, payload?)

Fire-and-forget version of callServer.

triggerBrowser(name, payload?)

Fire-and-forget version of callBrowser.


Browser (NUI)

Load via <script src="browser.js"> (exposes window.FiveMRPC) or import from browser.esm.js in a bundled app.

registerRpc(name, handler, options?)

Register a procedure in the browser that the client can call.

FiveMRPC.registerRpc('toggleInventory', (payload) => {
  document.getElementById('inventory').classList.toggle('visible');
});

unregisterRpc(name)

Remove a registered procedure.

call(name, payload?, options?)

Call a procedure in the same browser context (local).

callClient(name, payload?, options?)

Call a procedure registered on the client and await the result.

const name = await FiveMRPC.callClient('getLocalPlayerName');

triggerClient(name, payload?)

Fire-and-forget version of callClient.


TypeScript: Typed Procedures

By default all payloads and return values are typed as unknown. You can get full end-to-end types by augmenting the RpcProcedures interface once in a shared declaration file:

// types/rpc.d.ts
declare module 'fivem-rpc/server' { // adjust path to match your import
  interface RpcProcedures {
    getPlayerMoney: {
      request:  { playerId: number };
      response: number;
    };
    getInventory: {
      request:  void;
      response: { item: string; count: number }[];
    };
  }
}

After that, all calls are fully typed — no casts needed:

const money = await callServer('getPlayerMoney', { playerId: 1 });
//    ^? number  — TypeScript knows this

Options

Call options

await callServer('heavyQuery', payload, {
  timeoutMs: 15_000  // default is 7000ms
});

| Option | Type | Default | Description | |---|---|---|---| | timeoutMs | number | 7000 | Milliseconds before the call rejects with TIMEOUT |

Register options

registerRpc('submitForm', handler, {
  rateLimit: { windowMs: 5_000, maxRequests: 3 },
  validate: (payload) => {
    if (!payload?.name) return 'name is required';
    return null;
  }
});

| Option | Type | Default | Description | |---|---|---|---| | rateLimit | { windowMs, maxRequests } | 30 req/s | Per-caller rate limit. Set false to disable. | | validate | (payload, ctx) => string \| null | — | Return an error string to reject with BAD_REQUEST |


Handler Context

Every handler receives a context object as its second argument:

registerRpc('example', (payload, ctx) => {
  ctx.id        // unique ID for this request
  ctx.name      // procedure name
  ctx.peer      // 'server' | 'client' | 'browser' | 'local'
  ctx.playerId  // net ID of the calling player (server-side only)
  ctx.resource  // resource name the call came from
});

Use ctx.playerId on the server to identify the caller without trusting any player-supplied value.


Rate Limiting

All procedures are rate-limited by default: 30 requests per second per caller.

Each bucket is scoped independently to peer + playerId + procedureName — players cannot starve each other's quotas, and one spammy procedure doesn't affect others.

// Stricter
registerRpc('expensiveQuery', handler, {
  rateLimit: { windowMs: 10_000, maxRequests: 5 }
});

// Disabled (trusted internal calls)
registerRpc('internalSync', handler, {
  rateLimit: false
});

Local call() invocations always bypass rate limiting.


Payload Validation

registerRpc('sendMessage', handler, {
  validate: (payload) => {
    if (typeof payload?.text !== 'string') return 'text must be a string';
    if (payload.text.length > 200)         return 'text is too long';
    return null; // ok
  }
});

Returning any non-null string rejects the call with BAD_REQUEST before your handler ever runs. The caller receives that message as the rejection reason.


Security

All protections below are automatic — no configuration required.

| Protection | What it does | |---|---| | Resource name validation | Every envelope must carry the correct resource name. Cross-resource spoofed calls are dropped. | | Replay attack prevention | Envelopes older than 10 seconds are rejected even if otherwise valid. A captured packet cannot be replayed. | | Payload size cap | Payloads over 256 KB are rejected before any handler runs. | | Player ID verification | Server-side responses are verified to come from the expected player. Another player cannot inject a fake response. | | Rate limiting | Every handler is protected against call flooding by default. | | Error detail stripping | Internal error details are never forwarded over the network. Callers only receive a safe message and error code. | | Pending call cap | Maximum 512 simultaneous pending calls per context prevents memory exhaustion. |


Configuration Convars

Set these in server.cfg if needed. Both default to false and are safe to omit.

# Print a debug log for every RPC call (development only)
set fivem_rpc_debug "true"

# Include internal handler error details in responses (never use in production)
set fivem_rpc_expose_errors "true"

Error Handling

All call* functions return a Promise that rejects with an RpcError on failure:

import { callServer, RpcError } from '...client.js';

try {
  const result = await callServer('getInventory');
} catch (err) {
  if (err instanceof RpcError) {
    console.log(err.message); // human-readable
    console.log(err.code);    // machine-readable (see table below)
  }
}

Error codes

| Code | Cause | |---|---| | PROCEDURE_NOT_FOUND | No handler registered under that name in the target context | | TIMEOUT | Handler did not respond within timeoutMs | | BAD_REQUEST | Payload failed validation or envelope was malformed | | RATE_LIMITED | Caller exceeded the procedure's rate limit | | HANDLER_ERROR | Handler threw an unhandled exception | | TRANSPORT_ERROR | Network or protocol-level failure | | MALFORMED_RESPONSE | Response envelope could not be parsed | | FORBIDDEN | Available for your own handlers: throw new RpcError('...', 'FORBIDDEN') |


Limits

| Limit | Value | |---|---| | Maximum payload size | 256 KB | | Maximum procedure name length | 128 characters | | Maximum simultaneous pending calls | 512 per context | | Default request timeout | 7 seconds | | Maximum request age (replay protection window) | 10 seconds | | Default rate limit | 30 requests / second / caller |


License

Unlicense — public domain. Do whatever you want with it.