Professional Bun + TypeScript Toolkit for FiveM Developers
Taki_Kit © Taki
A framework for building large-scale FiveM (CFX) resources with TypeScript, using Bun as the development environment, package manager, bundler and test runner.
- Zero runtime dependencies. Nothing is installed into your resource.
- Client and server are provably isolated. A server-only import in a client bundle is a build failure, not a review comment.
- Type-safe events and callbacks. A wrong payload is a compile error.
- Untrusted input by default. Every inbound request passes a fixed security checklist: source, payload size, rate limit, permission, schema.
- No leaks by construction. Every listener, timer and pending request is tracked and released on resource stop.
- Why TypeScript
- Why Bun
- Install
- Build process
- FiveM setup
- Architecture
- Client development
- Server development
- Shared code
- Events
- Callbacks
- Commands
- Players
- Permissions
- Security
- NUI
- Taki_UI
- Database
- Framework adapters
- Resource lifecycle
- Testing
- Troubleshooting
- Examples
- Contributing
- Project reference
On a FiveM server, every client is an attacker. They can call any net event, with any payload, as often as they like. TypeScript is not about types being nice; it is about making the class of mistake that gets your server owned impossible to write:
// A typo, or a payload from a mismatched client version. Caught at build time.
Taki.Events.emitServer("taki:player:data", { source: "1" });
// ^^^^^^^^^^^^^^^^^^^^^^^ string, not numberThe type system also does the tedious work that would otherwise be a hundred
if (typeof x !== "number") throw lines, and — because it runs through a
schema engine — it becomes an actual runtime check, not just a hint.
Taki_Kit is compiled at strict plus exactOptionalPropertyTypes,
noUncheckedIndexedAccess, noUnusedLocals and noUnusedParameters.
Bun is used for everything that happens before the code reaches a player:
| Stage | Tool |
|---|---|
| Install | bun install (one bun.lock, no npm/yarn/pnpm lockfile) |
| Bundling | Bun.build |
| Testing | bun test |
| Type checking | tsc via bun run typecheck |
| Linting / formatting | eslint, prettier via bunx |
Bun is not the FiveM runtime. FiveM's client and server are V8 with a CFX
shim; there is no Bun global. So Bun.file(), Bun.serve() and Bun.env are
compile errors in this project (see Bun-API guard), and the
tooling that legitimately needs them lives in tools/, which is never shipped.
bun testruns your tests in Bun, so a test cannot catch a Bun API that only breaks in FiveM. That is exactly why the build refuses to emit one.
Requires Bun 1.2+.
bun installThat is the only install command. There is no npm install, and the repository
contains no package-lock.json, yarn.lock or pnpm-lock.yaml.
| Command | Does |
|---|---|
bun run build |
Type-safe production build (6 gates, see below) |
bun run dev |
Rebuild on change, with the same gates |
bun run typecheck |
tsc --noEmit over both TypeScript worlds |
bun test |
Run the test suite |
bun run lint |
ESLint (type-aware) |
bun run format |
Prettier, writing changes |
bun run check |
format → lint → typecheck → test → build |
bun run clean |
Remove dist/ |
Adding a dependency:
bun add <package> # runtime
bun add -d <package> # devTaki_Kit itself has no runtime dependencies, so most projects never need
bun add at all.
bun run build is a pipeline of gates. Each one fails loudly rather than
emitting something subtly wrong.
1. clean rm -rf dist/ a stale file can never be shipped
2. isolation import-graph walk client may not reach server code
3. Bun-API guard source scan no Bun.* in FiveM runtime code
4. bundle Bun.build, IIFE three separate outputs
5. scan output bundle scan no side-specific native in the wrong bundle
6. manifest fxmanifest.lua every referenced file exists
Output layout — client and server are never combined:
src/shared/index.ts -> dist/shared/taki-kit.js (shared_script, both sides)
src/client/index.ts -> dist/client/taki-kit.js (client_script)
src/server/index.ts -> dist/server/taki-kit.js (server_script)
Bun.build({ format: "iife" }) produces a self-contained script. FiveM executes
each listed script in its own scope, and although modern CFX understands ESM, an
IIFE is the only format that cannot be broken by script load order, a stale
dist/, or a duplicated module instance.
dist/shared/ runs on both sides and owns all the state: the event bus, the
callback table, the player cache, the disposable store. The client and server
bundles only attach their native bindings to it.
If each side inlined the core instead, Taki.Events would be two different
objects in the same V8 context and nothing would ever be delivered.
// src/client/foo.ts
import { getPlayers } from "../server/players";
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// This typechecks perfectly well. TypeScript has no idea which side it is
// compiling for.Without the check, that mistake survives review and ships a player list to everyone. With it:
SIDE-ISOLATION VIOLATION
src/client/foo.ts
imports ../server/players
resolves src/server/players/index.ts
reason client bundle may not import "src/server/"
chain src/client/index.ts -> src/client/foo.ts -> src/server/players/index.ts
A second, independent gate then scans the emitted bundle for server-only
natives (GetPlayers, DropPlayer, TriggerClientEvent, …), which catches
anything the graph walk cannot see.
fxmanifest.lua is included and validated by the build. It uses explicit
paths, not globs, on purpose — a glob that silently matches nothing produces a
resource that loads with no code and no error.
fx_version 'cerulean'
game 'gta5'
shared_scripts { 'dist/shared/taki-kit.js' }
client_scripts { 'dist/client/taki-kit.js' }
server_scripts { 'dist/server/taki-kit.js' }shared_scripts must come first. FiveM executes a resource's shared scripts
before its client and server scripts, which is what lets the side bundles extend
the single Taki object. If you get this wrong the bundle says so:
Taki_Kit: the shared bundle did not load before the client bundle.
OneSync is a soft dependency: it is required for Taki.Server.requireNear, and
distance checks report measurable: false rather than silently passing without
it.
src/
├── shared/ side-agnostic. Runs on BOTH sides.
│ ├── index.ts entry: creates the core, publishes the `Taki` global
│ ├── config/ the one file most projects edit
│ ├── types/ event maps, framework contract, commands, database
│ ├── constants/ wire event names and safety limits
│ ├── validators/ the schema engine
│ └── utils/ pure helpers
│
├── core/ the runtime kernel. Pure: it never calls a native.
│ ├── install.ts assembles every service into `Taki`
│ ├── natives/ the ONE interface the sides implement
│ ├── events/ typed event bus
│ ├── callbacks/ request/response transport
│ ├── security/ guard, rate limiter, permissions
│ ├── commands/ registry + argument parser
│ ├── players/ player service and cache
│ ├── database/ provider-agnostic facade + in-memory provider
│ ├── nui/ typed NUI bridge
│ ├── resources/ lifecycle + disposable store
│ ├── scheduler/ tracked timers
│ ├── logger/ structured, redacting logger
│ ├── errors/ the error hierarchy
│ └── config/ typed configuration
│
├── client/ client-only. Cannot import from server/.
├── server/ server-only. Cannot import from client/.
└── adapters/ framework adapters: standalone, esx, qbcore
1. adapters/ lives under src/, not beside it.
The brief's layout put adapters/ at the project root. They contain runtime
FiveM code — they call exports.es_extended — so outside src/ they would not
be typechecked, not be linted, not be covered by the isolation check, and not be
built. That is a hole in the security boundary, so they moved.
2. Two tsconfigs, not one.
| Project | Covers | Bun types? |
|---|---|---|
tsconfig.json |
src/, types/ |
no (types: []) |
tsconfig.tools.json |
tools/, tests/ |
yes |
src/ is compiled without Bun's types, so Bun.file() in runtime code is a
compile error before the Bun-API guard script even runs. A single config with
Bun types would have made this a lint-only concern.
3. The native bridge is a single seam.
src/core is pure. Everything needing the host is one interface
(src/core/natives/bridge.ts) implemented by src/client/natives/ and
src/server/natives/. This buys testability (the whole core runs under
bun test with no FiveM), side isolation (server-only capabilities do not exist
on the client bridge) and one auditable file listing every native the toolkit
touches.
// src/game/client/main.ts -> bundled into dist/client/taki-kit.js
// Listen for a server push.
Taki.Events.onClient("taki:ui:toast", ({ type, title, message }) => {
Toast.Show({ type, title, message });
});
// Send to the server.
Taki.Events.emitClient("taki:player:ready", { resource: GetCurrentResourceName() });
// Ask the server for something and await it.
const state = await Taki.Callback.call("taki:diagnostics:clientState");Local player helpers:
if (!Taki.Client.isAlive()) return;
Taki.Client.whenReady(() => {
// Runs once the ped exists; safe against the pre-spawn window.
});Taki.NUI.send, Taki.NUI.callback and Taki.NUI.setFocus are also available
on the client. See NUI.
// Guarded: this is what application code should use.
const Withdraw = Taki.Validator.tuple([Taki.Validator.number().int().positive().max(1_000_000)]);
Taki.Events.onServer(
"bank:withdraw",
{
args: Withdraw,
permission: "bank.withdraw",
rateLimit: Taki.Security.defineRateLimit("bank", 3, 10_000),
},
async (source, amount) => {
const player = Taki.Player.get(source);
if (player === null) return;
// Authoritative, never client-supplied.
const balance = Taki.Player.getMoney(source, "bank") ?? 0;
if (balance < amount) return;
await Taki.Database.update("accounts", { balance: balance - amount }, { owner: source });
},
);// Push to a client, or broadcast.
Taki.Events.emitServer("taki:player:data", source, data);
Taki.Events.emitServerAll("taki:ui:toast", { type: "info", title: "Hi", message: "…" });
// Ask one client for something.
const result = await Taki.Callback.callClient(source, "taki:diagnostics:clientState");A FiveM server has two join events, in an order the resource cannot control: the
socket connecting (playerConnecting) and the framework loading the record
(esx:playerLoaded). Taki_Kit tracks a player from the first and only publishes
authoritative data after the second, so a handler never sees a half-loaded
player.
Taki.bus.on("taki:framework:playerLoaded", (source: number, data: unknown) => { … });Anything in src/shared/ runs on both sides, which makes it the right home for
configuration, validators, types and pure helpers.
The one rule: shared code must not need a native. If it does, it belongs in
src/client or src/server, and the build will tell you.
Both event maps are open global interfaces. Extend them from any module:
// src/shared/types/my-events.ts
import type {} from "../../shared/types/events";
declare global {
interface TakiClientToServerEvents {
"shop:open": { shopId: number };
"shop:purchase": [item: string, quantity: number];
}
}| Map | Direction | Listen | Emit |
|---|---|---|---|
TakiClientToServerEvents |
client → server | onServer |
emitClient |
TakiServerToClientEvents |
server → client | onClient |
emitServer |
onNet / emitNet are aliases of onClient / emitClient, for muscle memory.
A map entry may be a single payload or a positional tuple. Both are fully checked:
// "shop:open": { shopId: number }
Taki.Events.emitClient("shop:open", { shopId: 1 }); // ok
Taki.Events.emitClient("shop:open", { shopId: "1" }); // compile error
// "shop:purchase": [item: string, quantity: number]
Taki.Events.onServer("shop:purchase", (source, item, quantity) => { … });Why
ArgsOflooks unusual. A plain conditional such asM[K] extends readonly unknown[] ? M[K] : [M[K]]is rejected by TypeScript in a rest-parameter position ("A rest parameter must be of an array type").ArgsOfusesinfer R extends readonly unknown[], which keeps the type lazy and provably an array. Five formulations were tested; only this one both compiles and correctly rejects wrong payloads, missing arguments, extra arguments and unknown event names.
Every on/register returns a Disposer. Call it when you mean it.
A FiveM net event is one-way: no answer, no failure signal, no timeout. The naive request/response implementation is a global array of pending promises that grows forever when the server never answers.
Taki_Kit's transport fixes all three:
// Client asks the server.
const { echo } = await Taki.Callback.call("taki:diagnostics:ping", { nonce: "abc" });
// Server answers a client.
Taki.Callback.register("taki:diagnostics:ping", (source, { nonce }) => ({ echo: nonce }));
// Server asks a client.
const state = await Taki.Callback.callClient(source, "taki:diagnostics:clientState");- Unique ids — a monotonic counter plus a random suffix, so two players issuing the same logical request can never collide.
- Bounded —
config.callbacks.maxPendingcaps the table; beyond that, new calls reject instead of growing the heap. - Self-cleaning — every entry removes itself on resolve, reject, timeout,
abort, and on resource stop. There is no path that leaks an entry, and the
test suite asserts
pendingCount === 0after teardown. - Owner tracking —
cancelForSource(source)onplayerDropped, because FiveM never signals that a peer has gone away.
Taki.Commands.register(
{
name: "give",
aliases: ["g"],
description: "Give money to a player",
side: "server",
permission: "admin.give",
args: [
{ name: "target", type: "player" },
{ name: "amount", type: "number", min: 1, max: 1_000_000 },
{ name: "reason", type: "string", rest: true, optional: true },
],
},
({ source, args }) => {
// args are typed: `target` is number, `amount` is number.
},
);Argument types: string, number, boolean, player. Parsing is strict on
purpose:
numbermust be fully numeric —Number("1abc")isNaNandparseFloat("1abc")is1; both would be wrong.booleanaccepts onlytrue/false, not1/yes.playermust be a connected slot.maxLengthandchoicesstop a client stuffing junk into a field.
A duplicate name or alias throws rather than overwriting: two resources
disagreeing about who owns /teleport is a real bug, and the player should not
silently get whichever won the race.
FiveM cannot unregister a command. Disposal therefore tombstones the entry: the command stops doing anything. A design that pretended otherwise would leave a live handler behind after every restart.
Built-ins: /taki version | debug | config | players | rate | permissions.
/taki debug prints live listener, timer, callback and rate-limit counters —
which is how you actually tell a memory leak from a slow query.
Taki.Player.exists(source); // live presence, not the cache
Taki.Player.get(source); // framework data, or null
Taki.Player.require(source); // throws NotFoundError instead
Taki.Player.getName(source);
Taki.Player.getIdentifier(source); // prefers license:, survives identity change
Taki.Player.getMoney(source, "bank");
Taki.Player.getInventory(source);
Taki.Player.hasPermission(source, "admin.give");
Taki.Player.setData(source, "cart", items); // resource-local, never persistedThere is deliberately no setPlayerData(source, dataFromClient). A client
that sends { identifier: "license:admin" } changes nothing, because no method
accepts a client-supplied identifier.
getPlayer() returns null — not a placeholder — when a player is connected
but the framework has not finished loading them.
Taki.Permissions.defineRole({
name: "moderator",
permissions: ["admin.command.tp", "admin.command.kick"],
inherits: ["staff"],
});
Taki.Permissions.grantRole(source, "moderator");
Taki.Permissions.has(source, "admin.command.tp");
Taki.Permissions.require(source, "admin"); // throws PermissionErrorHierarchical matching is on by default: holding admin satisfies
admin.command.teleport. This removes the most common permission bug in large
resources.
Permissions are always resolved server-side, from the framework's groups merged with resource grants. A client may ask to use a privileged feature; it never answers whether it is allowed to.
Treat every client as hostile. Taki.Security.guard applies the whole checklist
in one call, in this order:
| # | Check | Why here |
|---|---|---|
| 1 | source is a real player | rejects console and other resources |
| 2 | payload size | bounded before any parsing work |
| 3 | rate limit | keyed per player, so one abuser cannot exhaust others |
| 4 | permission | before validation, so an unprivileged flooder never reaches the schema |
| 5 | schema validation | declared shape and arity |
| 6 | business logic | fully isolated from the FiveM event pump |
Taki.Security.defineRateLimit("bank", 3, 10_000);
Taki.Security.rateLimit("bank:withdraw", 3, 10_000); // peek, does not spend
Taki.Security.validate(Withdraw, args);
Taki.Security.requirePermission(source, "admin");
Taki.Security.stats(); // rejections by reasonThe limiter is a token bucket, not a fixed window: a fixed window lets a
client send limit requests at 0.999s and limit more at 1.001s. The bucket
table is bounded and evicts in insertion order, so a client that varies its own
key cannot grow the map without limit.
Failures never escape into the FiveM event pump. A guarded handler that throws logs a structured error and is otherwise inert.
Never trust money, inventory, permissions, jobs, identifiers, ownership or admin status when supplied by the client. Taki_Kit provides no API that lets you.
// Client -> browser
Taki.NUI.send("shop:setItems", { items });
Taki.NUI.sendAll("shop:select", "bread", 3);
Taki.NUI.setFocus(true);
Taki.NUI.close();
// Browser -> client, with a result
Taki.NUI.callback("taki:diagnostics:ping", async (payload) => {
return { echo: payload.nonce };
});
// Server -> one client's UI
Taki.NUI.sendTo(source, "bank:balance", { amount: 4200 });A single argument is sent as-is; two or more are sent as an array. Use
sendAll when the single payload is itself an array.
Taki.NUI is the raw transport. For notifications, menus, dialogs, progress
and the rest, use Taki.UI instead - it is built on the same
transport and adds no new concepts.
A complete widget set - notifications, progress, context menus, dialogs, input forms, text overlays, status panels, keybind hints, loading and error states - with theming, localisation and accessibility already handled.
// Notifications. `duration: 0` keeps one on screen until you dismiss it.
Taki.UI.notify({ type: "success", title: "Success", message: "Vehicle spawned" });
Taki.UI.notify({ type: "error", title: "Failed", message: "You are not the driver." });
// A persistent loading state, replaced in place when you reuse the id.
Taki.UI.loading("save", "Saving…");
Taki.UI.notify({ id: "save", type: "success", title: "Saved" });
// Progress that animates and closes itself.
Taki.UI.progress({ label: "Repairing vehicle", duration: 2500 });
// A context menu resolves the chosen entry, or null if dismissed.
const choice = await Taki.UI.context.open({
id: "vehicle_menu",
title: "Vehicle",
options: [
{ label: "Repair", action: "repair" },
{ label: "Lock", action: "lock" },
{ label: "Delete", action: "delete", tone: "danger" },
],
});
if (choice?.action === "repair") {
// …
}
// A dialog resolves true/false (null on timeout).
if (await Taki.UI.dialog.open({ title: "Delete vehicle?", tone: "danger" })) {
// …
}
// An input form resolves the values, or null if dismissed. Always strings.
const values = await Taki.UI.input.open({
title: "Deposit",
fields: [{ name: "amount", label: "Amount", type: "number", required: true, min: 1 }],
});
// Persistent HUD elements.
Taki.UI.text({ id: "checkpoint", text: "Los Santos", subtext: "Waypoint reached" });
Taki.UI.status({
id: "vitals",
position: "top-right",
entries: [{ id: "fuel", label: "Fuel", value: 68, total: 100, tone: "warn" }],
});
Taki.UI.keybinds([{ id: "trunk", keys: ["E"], label: "Trunk" }]);
// Theme and locale, live.
Taki.UI.setTheme("crimson");
Taki.UI.setLocale("pt");Taki_Kit Core contains no UI framework. Not React, not Vue, not Svelte.
That is enforced by a build gate, not by convention. bun run build fails if
any file under src/ imports one:
[1/8] Framework-independence gate
ok Taki_Kit Core is framework-independent (64 source files scanned, 0 UI framework imports).
React exists only in ui/src/react/, and only the NUI page imports it. The
result is that the client bundle carries zero React - 54.5 KiB for the whole
client - while the page itself is 470 KiB.
The split:
src/client/ui/ui-store.ts all behaviour. no DOM, no React, no FiveM
src/shared/types/ui.ts the wire contract: UiCommand / UiState / UiResult
ui/src/bridge.ts the ONLY file that touches window.postMessage
ui/src/react/ the reference renderer
Because the store is pure, every behaviour is unit-tested under bun test with
no browser and no game. Swapping the renderer means replacing one folder.
bun run dev:ui # preview at http://127.0.0.1:5173, rebuilds on save
bun run build:ui # minified production page into web/
bun run build:ui --dev # unminified, with a source map, for devtoolsdev:ui serves the real page in standalone mode: GetParentResourceName()
does not exist in a browser, so the bridge logs that once and renders anyway. A
dev control panel is injected to drive every widget. It is never bundled into a
real resource - main.tsx does not import it.
web/ is generated and git-ignored. Run bun run build before starting a
server; fxmanifest.lua references web/app.js and the manifest check will
tell you if it is missing.
Full documentation: docs/UI.md.
Taki.Database.use(oxmysqlProvider());
const rows = await Taki.Database.query<PlayerRow>("SELECT * FROM players WHERE id = ?", [id]);
await Taki.Database.insert("players", { identifier, name });
await Taki.Database.update("players", { name }, { id }); // WHERE required
await Taki.Database.delete("players", { id }); // WHERE required
await Taki.Database.transaction(async (tx) => { … });Taki_Kit does not depend on a SQL library. DatabaseProvider is the whole
surface; oxmysql is an adapter, and a MemoryProvider ships for tests.
Helpers never interpolate values — only ? placeholders. Identifiers
(table/column names), which cannot be parameterised, are validated against
^[A-Za-z_][A-Za-z0-9_]*$ and backtick-quoted. Stacked statements are refused,
because that is how a read-only endpoint becomes a write primitive:
await Taki.Database.query("SELECT 1; DROP TABLE users");
// NetworkError: Refusing a statement containing multiple statements.Taki.Database.useMemory() makes the whole subsystem testable with no server.
Taki.useFramework(new EsxAdapter(Taki)); // or explicit via config
Taki.framework()?.name; // "esx"Taki_Kit ships standalone, esx (and ESX-Legacy) and qbcore (and QBox).
config.framework.adapter: "auto" probes for ESX, then QBCore, then falls back
to standalone — deferred and retried, because resource start order is not under
your control.
src/core never imports a concrete adapter, and an adapter never reaches into
core internals. The contract is FrameworkAdapter in
src/shared/types/framework.ts.
The standalone adapter is not a stub. It supplies identity (so commands,
permissions and callbacks work on a frameworkless server) and reports money,
inventory and job as null rather than 0. A fabricated zero balance invites a
resource to write to an account that does not exist.
exportsis read lazily in every adapter. A module-level capture would snapshot it before the framework resource finishes starting, and then every lookup would silently returnundefined.
const off = Taki.Events.onServer("shop:open", handler);
off(); // detach for good
Taki.Resource.onStop(() => saveEverything());
Taki.Resource.exists("es_extended");
Taki.Resource.getState("qb-core");
Taki.Resource.isStarted("oxmysql");
await Taki.Resource.waitForStart("es_extended", 30_000);Every register, on and schedule is tracked in one DisposableStore, which
is drained on onResourceStop. A FiveM resource restart is then genuinely
clean — no duplicate events, no orphaned timers.
bun test223 tests covering validators, utilities, permissions, configuration, event
schemas, callback logic, error handling, commands, the database, and a full
end-to-end pass over the real createCore().
The FiveM runtime is mocked in tests/mocks/fivem.ts, installed via
bunfig.toml so it is present before any test module loads:
[test]
preload = ["./tests/setup.ts"]The mock is deliberately honest: it has a real event bus (so a disposer that
does not detach is caught), a real player table, and GetPlayerName(999)
returns undefined for a slot that does not exist, exactly as the real native
does. A permissive stub would let tests pass for the wrong reason.
Switch the simulated side:
TAKI_TEST_SIDE=client bun testWhat is deliberately not mocked: setTimeout, Promise, JSON, Math,
Date. Those are identical in Bun and in FiveM's V8, so mocking them would only
make the tests less honest.
"the shared bundle did not load before the client bundle"
shared_scripts is missing, or listed after client_script. Check
fxmanifest.lua, then bun run build to confirm all three files exist.
"my code doesn't run" and the console is empty
Almost always a manifest glob that matches nothing. This project uses explicit
paths; bun run check validates every reference against dist/.
ReferenceError: Bun is not defined in production
A Bun API reached runtime code. It compiles and passes bun test (which runs
in Bun). bun run build blocks this; if you hit it, a tsconfig was widened.
"a handler fires twice after restarting my resource"
Something registered outside Taki_Kit, so it is not in the disposable store.
Route it through Taki.Events.on* or register the disposer with
Taki.Resource.onStop.
Taki.Permissions.has is false for a framework admin
The framework's permission list is only merged when it exposes one. Grant
explicitly: Taki.Permissions.grantRole(source, "admin").
measurable: false from a distance check
OneSync is off. Pass { requireMeasurable: false } only for actions that
genuinely do not depend on position.
Too many pending callback requests
A handler is registered but never responds. /taki rate shows the count and the
oldest pending request.
A callback times out only on a busy server
Raise config.callbacks.defaultTimeoutMs, or maxTimeoutMs if you raise it
past the ceiling — the ceiling is clamped silently.
web/— a complete NUI page using the typed bridge in both directions.src/shared/config/config.ts— every knob, documented inline.src/server/commands/index.ts— the/takidiagnostics command tree.src/adapters/standalone/index.ts— the smallest usefulFrameworkAdapter.tests/integration.test.ts— the full guarded request path, end to end.
bun install
bun run check # format, lint, typecheck, test, buildHouse rules, in the project's stated priority order:
Security → Type safety → FiveM compatibility → Maintainability →
Developer experience → Performance → Extensibility
- Never add a runtime dependency. If you need one, it is an adapter.
- Never add an abstraction without a concrete consumer.
import typeacross a side boundary is fine (it is erased). A value import is not.- Every
register/on/schedulereturns a disposer. - No
anyinsrc/. Prefer a discriminated union, a generic, or a type guard. - Prefer a plain
Errorsubclass over a clever factory.
Further reading:
- docs/ARCHITECTURE.md - the three-bundle split, why
import typecrosses a side boundary, and the build gates. - docs/SECURITY.md - the trust boundaries, what is validated where, and why client-side validation is UX rather than security.
- docs/UI.md - the widget layer, the wire protocol, and how to write your own renderer.
Tests are part of the contract, not an afterthought: tests/docs.test.ts fails
if this README and the real API disagree, so a renamed method cannot ship with
documentation still describing the old one.
| Path | Purpose |
|---|---|
src/shared/index.ts |
shared entry; publishes the Taki global |
src/client/index.ts |
client entry; attaches client natives |
src/server/index.ts |
server entry; attaches server natives |
src/core/install.ts |
assembles the core |
src/core/natives/bridge.ts |
the native seam |
src/shared/validators/schema.ts |
the schema engine |
src/core/security/guard.ts |
the security envelope |
src/core/security/rate-limiter.ts |
token bucket |
src/core/callbacks/callback-transport.ts |
request/response |
tools/build.ts |
the 6-gate build |
tools/check-isolation.ts |
side-isolation graph walk |
tools/check-bun-apis.ts |
Bun-API guard |
tools/check-bundle.ts |
emitted-output scanner |
tools/check-manifest.ts |
fxmanifest validator |
types/fivem.d.ts |
minimal FiveM runtime declarations |
types/runtime.d.ts |
timers, console, V8 extras |
Third-party libraries retain their original licenses and attribution. Taki_Kit itself has no runtime dependencies.
Taki_Kit © Taki. All rights reserved.