@open-core/framework
v1.0.8
Published
Secure, event-driven TypeScript Framework & Runtime engine for CitizenFX (Cfx).
Readme
OpenCore Framework - Stable v1
OpenCore is a TypeScript multiplayer runtime framework targeting CitizenFX runtimes (Cfx/RageMP) via adapters.
It is not a gamemode or RP framework. It provides:
- A stable execution model (server and client)
- Dependency Injection and metadata-driven wiring
- An event/command system
- Security primitives (validation, access control, rate limiting)
License: MPL-2.0
Discord Community | Docs | OpenCore CLI
Installation
pnpm add @open-core/framework reflect-metadata tsyringe zod uuidThis framework uses TypeScript decorators. Ensure your project has decorators enabled.
Imports and entry points
The package exposes subpath entry points:
@open-core/framework(root)@open-core/framework/server@open-core/framework/client
Architecture
OpenCore follows a Ports & Adapters (Hexagonal) architecture.
- Kernel (
src/kernel): engine-agnostic infrastructure (DI, logger, metadata scanning) - Runtime (
src/runtime): multiplayer execution model (controllers, processors, security, lifecycle) - Adapters (
src/adapters): platform integration (Cfx, Node testing)
The runtime never auto-detects the platform. Adapters are selected explicitly at bootstrap time.
Cfx game profiles
OpenCore treats CitizenFX (cfx) as the platform and supports game profiles (gta5 and rdr3).
- Shared runtime APIs (events, exports, transport, DI) are registered through the Cfx adapter.
- Game-specific behavior is controlled through platform capabilities/config (
gameProfile,defaultSpawnModel, etc.). - Optional RedM-specific enhancements can be layered as external libraries without changing core runtime contracts.
Operating modes
Each instance runs in exactly one mode configured via Server.init():
CORE: authoritative runtime. Typically provides identity/auth/players via exports.RESOURCE: a normal Cfx resource using CORE as provider for some features.STANDALONE: a self-contained runtime (useful for tooling, simulations, or small servers).
Server bootstrap
Initialize the server runtime:
import { Server } from '@open-core/framework/server'
await Server.init({
mode: 'CORE'
})Some features require providers (depending on your mode and configuration). Configure them before calling init():
import { Server } from '@open-core/framework/server'
Server.setPrincipalProvider(MyPrincipalProvider)
Server.setSecurityHandler(MySecurityHandler)
Server.setPersistenceProvider(MyPlayerPersistence)
Server.setNetEventSecurityObserver(MyNetEventSecurityObserver)Controllers and decorators
OpenCore uses a decorator + processor pattern.
Decorators store metadata with Reflect.defineMetadata(). During bootstrap, the MetadataScanner reads metadata and processors register handlers.
Commands
import { Controller, Command, Guard, Throttle, Player } from '@open-core/framework/server'
import { z } from 'zod'
const TransferSchema = z.tuple([z.coerce.number().int().positive(), z.coerce.number().min(1)])
@Controller()
export class BankController {
@Command({
command: 'transfer',
usage: '/transfer <id> <amount>',
schema: TransferSchema,
})
@Guard({ rank: 1 })
@Throttle(1, 2000)
async transfer(player: Player, args: z.infer<typeof TransferSchema>) {
const [targetId, amount] = args
player.emit('chat:message', `transfer -> ${targetId} (${amount})`)
}
}Network events
@OnNet() handlers always receive Player as the first parameter.
import { Controller, OnNet, Player } from '@open-core/framework/server'
import { z } from 'zod'
const PayloadSchema = z.object({ action: z.string(), amount: z.number().int().positive() })
@Controller()
export class ExampleNetController {
@OnNet('bank:action', { schema: PayloadSchema })
async onBankAction(player: Player, payload: z.infer<typeof PayloadSchema>) {
player.emit('chat:message', `action=${payload.action} amount=${payload.amount}`)
}
}Security decorators
@Guard({ rank })or@Guard({ permission })@Throttle(limit, windowMs)@RequiresState({ missing: [...] })
Library events
Use library wrappers to emit domain events and @OnLibraryEvent() to observe them.
@OnLibraryEvent() listens to events emitted through library.emit(...) only.
It does not listen to emitExternal, emitNetExternal, or emitServer.
import { Server } from '@open-core/framework/server'
const characters = Server.createServerLibrary('characters')
@Controller()
export class CharacterListeners {
@OnLibraryEvent('characters', 'session:created')
onSessionCreated(payload: { sessionId: string; playerId: number }) {
// optional listener for library domain events
}
}
characters.emit('session:created', { sessionId: 's-1', playerId: 10 })Client usage follows the same pattern with Client.createClientLibrary(...) and
@Client.OnLibraryEvent(...).
Plugins
Plugin contracts are exposed by runtime entrypoint, not by root:
- Server plugins:
@open-core/framework/server - Client plugins:
@open-core/framework/client
import { Server, type OpenCorePlugin } from '@open-core/framework/server'
import { Client, type OpenCoreClientPlugin } from '@open-core/framework/client'
const serverPlugin: OpenCorePlugin = {
name: 'server-example',
install(ctx) {
ctx.server.registerApiExtension('ExampleServerDecorator', () => {})
},
}
const clientPlugin: OpenCoreClientPlugin = {
name: 'client-example',
install(ctx) {
ctx.client.registerApiExtension('ExampleClientDecorator', () => {})
},
}
await Server.init({ mode: 'CORE', plugins: [serverPlugin] })
await Client.init({ mode: 'CORE', plugins: [clientPlugin] })Module augmentation for plugin APIs:
declare module '@open-core/framework/server' {
interface ServerPluginApi {
ExampleServerDecorator: () => void
}
}
declare module '@open-core/framework/client' {
interface ClientPluginApi {
ExampleClientDecorator: () => void
}
}Testing
Tests run with Vitest.
pnpm test
pnpm test:unit
pnpm test:integration
pnpm test:coverageNote: pnpm test does not run benchmarks.
Benchmarks
Benchmarks are split by value, so the default run focuses on framework features that matter for real servers.
pnpm bench
pnpm bench:value
pnpm bench:gold
pnpm bench:startup
pnpm bench:diagnostic
pnpm bench:soak
pnpm bench:load
pnpm bench:allbench/bench:value: value-focused suite. Commands, net events, RPC, lifecycle, ticks, binary path, bootstrap.bench:gold: hot-path load scenarios only.bench:startup: startup and registration cost.bench:diagnostic: internal and low-level synthetic benchmarks.bench:soak: long-running stress scenario.
Snapshot (latest local run)
Use benchmark/reports/ as the source of truth. Results vary by machine and should be compared relatively, not treated as product guarantees.
- Primary benchmark targets:
- full command execution
- full net event handling
- RPC processing
- player lifecycle churn
- tick budget impact
- bootstrap cost
- binary transport cost
Full reports and methodology are available in benchmark/README.md.
Reports
Benchmark reports are generated under benchmark/reports/.
pnpm bench:allgenerates aggregated reports (text/json/html)- Load metrics used by load benchmarks are persisted in
benchmark/reports/.load-metrics.json
For details about the benchmark system, see benchmark/README.md.
Development scripts
pnpm build
pnpm watch
pnpm lint
pnpm lint:fix
pnpm formatLicense
MPL-2.0. See LICENSE.
