Skip to content

Concept Report: Flecs Semantics on Top of Koota

Status: draft v2, 2026-10-30

1. Summary

Revision Plan
v0 (original) Custom TS bindings for the flecs C library
v1 Our own dependency-free TS core with flecs semantics, built spec-driven
v2 (current) Use koota as the core. Fill the flecs gaps with several small extension packages, and send upstream PRs where they fit.

Recommendation: Depend on koota from npm, pinned to an exact version. Build the missing flecs features (scheduler, prefabs, relation traits/traversal, serialization, devtools) as small, independent @workspace/ecs-* packages that use only koota's public API. Specify each extension spec-first, using the flecs manuals and tests. Upstream generic improvements to koota when they fit its scope. Demos are React (+ react-three-fiber where useful) apps built on koota + extensions.

Why we switched from v1 to v2:

  • Production-ready today. Koota already covers about 60% of the flecs feature surface we need, with tests, docs, and an active pmndrs maintainer team.
  • React / R3F fit. Koota comes from pmndrs, the same collective as react-three-fiber, drei, and zustand. Its React hooks (useQuery, useTrait, useWorld, …) are its core strength.
  • Focus. Our effort goes into the learning value (flecs concepts) and the demos instead of re-implementing storage and queries.
  • Fallback. The v1 spec-driven process is kept, but applied to extensions instead of a full core.

Updated constraints:

Constraint Answer
Goal Learning/showcase and production use
Flecs fidelity Flecs semantics matter, the implementation does not
Core Koota is acceptable as the core (the v1 requirement for a core of our own is dropped)
Runtimes Browser + Node/Bun/Deno. Multithreading is nice-to-have (dropped for now)
C / WASM toolchain Not wanted
Way of working Depend on koota + multiple small extension packages + optional upstream PRs
Priority gaps Scheduler/phases, prefabs/IsA, transitive/symmetric/traversal, serialization, devtools
First demo demo-dnd
apps/web Template that new demos are copied from

2. Evaluation of the Original Plan ("Custom Bindings")

"Language bindings" for flecs (C#, Rust, Zig, Lua) all wrap the C core through native FFI. TypeScript has no native FFI in the browser, so a TS binding means compiling flecs to WebAssembly (emscripten or wasi-sdk) and writing a TS layer over the exported C API.

Weaknesses

  1. Toolchain cost. C + emscripten builds, debugging across the WASM boundary, and keeping up with upstream flecs releases. The team does not want this.
  2. Boundary overhead. Components live in WASM linear memory. JS systems have two choices: cross the boundary for every entity (slow), or map columns as typed-array views (fast, but limited to POD/numeric data, and the views break when memory grows).
  3. Component data model mismatch. Idiomatic TS components are objects, strings, and arrays. Flecs stores raw bytes described by its meta addon. Strings, nested objects, and references need manual marshalling or a side-table on the JS side, which partly defeats the purpose.
  4. Lifetimes and GC. Closures registered as systems/observers, plus JS-side handles, need explicit cleanup (FinalizationRegistry is not deterministic).
  5. Multithreading. Running the flecs scheduler with threads in WASM requires pthreads + SharedArrayBuffer + COOP/COEP headers, which makes Node/Bun/Deno/browser deployment harder.
  6. Low learning value. Most of the effort goes into FFI plumbing, not ECS concepts.
  7. No existing reference. Flecs lists no maintained JS/TS binding, so there is nothing to build on. We would be first and alone.

When bindings would make sense

  • You need the actual flecs runtime: Explorer/REST compatibility, Flecs Script, native-level performance with millions of entities.
  • Most logic runs in C/WASM and TS mainly orchestrates and renders.

Neither applies here. Conclusion: not recommended.

3. Landscape: TypeScript ECS Libraries vs. flecs

The sources are in reference/ (koota 0.6.6, bitECS 0.4.0, becsy 0.16.1, miniplex 2.0, flecs 4.1).

koota bitECS becsy miniplex
Org / license pmndrs / ISC NateTheGreatt / MPL-2.0 Last Olive Games / MIT hmans / MIT
Core size / tests ~4.7k LOC / ~194 ~4.6k / ~140 ~7.8k / ~176 ~0.6k / ~76
Activity Active, pre-1.0, canary releases 0.4 rewrite, recent hierarchy work Slow patch releases Dormant since 2.0
Storage Bitmask + sparse sets. SoA/AoS traits in plain JS arrays Bitmask + sparse sets. User-owned stores (typed arrays possible) ArrayBuffer/SAB fields Plain objects
Relations Pairs, wildcard, exclusive, pair data, cleanup, target filters Pairs (one bit per pair), wildcard, exclusive, data, auto-remove Refs/backrefs only None
Hierarchy User-defined ChildOf, experimental ordered() Cascade depth-ordered query None None
Prefabs / IsA None IsA by copying None None
Observers onAdd/onRemove/onChange, onQueryAdd/Remove onAdd/onRemove/onSet/onGet Reactive queries instead Bucket events
Change detection Added/Removed/Changed modifiers Removed in 0.4 Best of the four (added/changed/removed, write tracking) Enter/exit only
Systems / scheduler None (user frameloop, directed in benches) None (pipe) Entitlement-based planner None
Serialization None Strong (SoA/AoS/snapshot/observer deltas) None Objects are JSON
Threading No SAB-friendly, DIY Designed, not implemented No
React First-class hooks None None miniplex-react components

Takeaways

  • koota is the only candidate that combines relations, change detection, observers, active maintenance, and React. It is the best foundation for React/R3F apps.
  • bitECS has the closest feature overlap with flecs and the best serializers. But its pair-per-bit design scales poorly with many relation targets, it has no React integration, and MPL-2.0 adds friction to forking. Use it for inspiration only (serializers, Cascade, relation-trait API).
  • becsy has the best scheduler and change tracking, but fixed capacities, no relations, and threading that was never delivered. Use it for inspiration only (entitlements, before/after ordering).
  • miniplex is a dormant project. Use it for inspiration only (declarative <Entity>/<Component> JSX with ref capture for R3F objects).

4. Koota vs. flecs: Gap Analysis

flecs feature koota How we close the gap
Entities, components, tags, singletons Full –
Queries: with / Not / Or Full (multiple Or groups merge into one) Possible upstream fix
Optional terms Missing Use entity.has() inside the loop. Upstream candidate
Cached queries, Added/Removed/Changed Full –
Observers OnAdd/OnRemove/OnSet Partial (no custom events, no multi-term) ecs-events (small) if needed
Pairs, wildcard target, exclusive, pair data Full –
Cleanup traits (OnDelete, OnDeleteTarget) Partial (autoDestroy) Sufficient for the demos
Transitive / symmetric / reflexive Missing ecs-relations
Traversal up / cascade Missing ecs-relations (helpers + depth-ordered iteration)
Query variables $x Missing (target filters cover some cases) Target filters + helpers
Hierarchies (ChildOf, names, paths) Partial ecs-relations (ChildOf, Name, path lookup)
Prefabs / IsA Missing ecs-prefab
Systems, pipeline, phases Missing ecs-schedule (rate/interval and fixed-step later, not v0)
Deferred commands Missing (query results are snapshots, so it's mostly harmless) Not prioritized
Serialization / snapshots Missing ecs-serialize
Reflection / names Missing (traits have no names) Trait registry in ecs-serialize / ecs-devtools
Devtools / Explorer Missing ecs-devtools (React panel built on @workspace/ui)
Multithreading Missing (plain arrays) Dropped for now

Koota's known limits that we accept:

  • At most 16 worlds, ~1M entities per world, and an 8-bit generation (a stale handle can match again after 256 reuses of the same slot).
  • It patches Number.prototype (entity.get() etc.).
  • It uses new Function for accessors, so a strict CSP needs unsafe-eval.
  • Destroying entities and non-exclusive relations scale linearly in places.
  • It is pre-1.0. Deprecations are scheduled for 0.7.0.

5. Options

Option Pros Cons Fit
A flecs → WASM bindings (v0) Real flecs C toolchain, boundary cost, no React story Low
B Own pure-TS core (v1) Full control, max learning, threading possible Large scope, not production-ready for a long time Medium
C Depend on koota + extension packages + upstream PRs Production-ready now, React/R3F native, focus on demos and flecs concepts Bound to koota's design limits and release pace High
D Hard fork of koota Can change internals freely Merge burden, splits the community, loses upstream fixes Low
E Upstream-only (everything goes into koota) No extra packages Blocked by maintainer scope and review pace; scheduler/prefabs likely out of koota's scope Low

Recommendation: Option C.

  • Revisit B only if hard requirements (threading, >16 worlds, CSP without unsafe-eval) come back.
  • Avoid D. If an extension can't be built without internals, open an upstream PR or issue first. Use $internal only as a short-lived, version-pinned shim.

6. Extension Packages

Rules for all extensions:

  • Use public koota API only (hooks, queries, relations, trait.schema). Do not monkey-patch.
  • Each package is small, tree-shakeable, and has koota as a peerDependency.
  • Each package is spec-first. A spec in docs/specs/<pkg>.md references the matching flecs manual and lists in/out of scope. Conformance tests are ported or adapted from flecs tests where possible.
  • Anything generic and in koota's spirit is proposed upstream. Once it's merged upstream, delete our version.
Package flecs reference Approach Upstream candidate?
@workspace/ecs-schedule Systems.md, pipeline phases defineSystem({ name, phase, before, after, sets, runIf, run(world, dt) }). Builtin phases OnStart → OnLoad → PostLoad → PreUpdate → OnUpdate → OnValidate → PostUpdate → PreStore → OnStore, plus custom phases via definePhase(name, { dependsOn, before, manual }). Phases not linked to the builtin chain are manual (only runPhase/runPhases run them, e.g. turn pipelines). createSchedule({ name, plugins, phases, sets, systems, hooks }) fixes the order once and validates it. run(world, dt), runPhase, runPhases. Enable/disable and OnStart state live in a world trait (schedule.state). Introspection (schedule.systems) and hooks for devtools. Intervals, rates, and fixed-step are later, not v0. requestAnimationFrame and R3F useFrame are documented driver examples, not package adapters. Ideas from becsy (before/after) and Bevy (sets, run conditions). No (koota leaves systems to users on purpose)
@workspace/ecs-prefab PrefabsManual.md A prefab is an entity with a Prefab tag. instantiate(prefab) copies traits (with overrides) and adds an IsA(prefab) pair. Prefab variants copy in order along the IsA chain. No shared storage (copy semantics, like bitECS). Queries use Not(Prefab). Maybe (Prefab exclusion)
@workspace/ecs-relations Relationships.md, ComponentTraits.md, HierarchiesManual.md symmetric(rel) mirrors pairs through onAdd/onRemove hooks. transitive(rel) gives isReachable(a, rel, b) plus an optional materialized closure relation kept in sync by hooks. up(entity, rel, trait) walks targets, and cascade(query, rel) iterates depth-first. Also ChildOf, Name, and path lookup. Yes: symmetric/transitive options, Or group fix, optional terms
@workspace/ecs-serialize Flecs JSON format, Remote API A trait registry (name ↔ trait, which also covers the schedule state trait via its name field) plus a JSON world snapshot/restore for schema traits, AoS via codecs, and relation pairs with id remapping. Later: delta serialization driven by Changed. Ideas from bitECS serializers. Maybe (trait names/registry)
@workspace/ecs-devtools Flecs Explorer A React panel (built on @workspace/ui) that shows an entity list, traits, relations graph, queries, and system timings (measured by devtools through ecs-schedule introspection and hooks). Uses ecs-serialize for inspect and export. No

Dependency graph:

flowchart TD
  koota[(koota)] --> schedule[ecs-schedule]
  koota --> prefab[ecs-prefab]
  koota --> relations[ecs-relations]
  koota --> serialize[ecs-serialize]
  serialize --> devtools[ecs-devtools]
  schedule -. introspection, hooks .-> devtools
  ui["@workspace/ui"] --> devtools
  template["apps/web template"] --> demos["demo-*"]
  devtools --> template

7. Spec-Driven Process (for Extensions)

7.1 Sources

  • Flecs manuals (normative): Systems, Prefabs, Relationships, Component Traits, Hierarchies, Remote API.
  • Flecs test suite (reference/flecs/test/): mine it for behaviour cases and port relevant tests.
  • Koota internals and docs (reference/koota): learn the extension surface and what's planned upstream.
  • bitECS / becsy / miniplex: inspiration as listed in section 3.

7.2 Workflow per feature

flowchart LR
  A[Flecs manual + tests] --> B[Spec doc<br/>docs/specs/*.md]
  B --> C[Conformance tests<br/>packages/ecs/tests]
  C --> D[Implementation]
  D --> E[Demo usage]
  E -->|API friction| B
  1. Write a short spec in docs/specs/<pkg>.md. Include the flecs reference, scope (in/out), the TS API sketch, and edge cases.
  2. Turn the spec into Vitest tests. Port flecs tests where useful.
  3. Implement against koota's public API until the tests pass.
  4. Use the feature in the driving demo. Feed any friction back into the spec. If the friction is caused by koota itself, open an upstream issue or PR.

7.3 Non-goals (for now)

Flecs Script, the REST protocol, wire compatibility with the flecs Explorer, multithreading, shared-storage prefab inheritance, and full query variables.

8. Demo → Feature Mapping

Demo Showcases Koota features Extensions driven
demo-dnd (1st) Initiative order, turns, status effects with duration Traits, tags, pairs (HasEffect(Poisoned)) with pair data (duration), observers, Added/Removed ecs-schedule (turn phases), ecs-prefab (monster templates), ecs-devtools v0
demo-anno Production chains, logistics Singletons, exclusive relations (StoredIn), target filters, Changed ecs-schedule (intervals/rates: later, not v0), ecs-serialize (save/load)
demo-ck3 Dynasties, vassalage, opinions Relation data (opinion), cleanup (autoDestroy), ordered children ecs-relations (transitive VassalOf, symmetric MarriedTo/AlliedWith, up, ChildOf names)
demo-totalwar Many units, formations (R3F instanced meshes) SoA traits, cached queries, updateEach, useStores ecs-schedule (fixed step: later, not v0). Benchmarks vs. bitECS. Koota's limits shown honestly (no workers)

9. Risks and Mitigations

Risk Mitigation
Koota API churn (pre-1.0, 0.7.0 removals announced) Pin the exact version. Upgrade deliberately. The extensions' test suites act as an early warning
Extension needs koota internals Upstream issue/PR first. $internal only as a pinned, isolated shim
Upstream PRs rejected or slow The extensions stand alone. Upstreaming is optional
Koota hard limits (16 worlds, ~1M entities, 8-bit generation, Number.prototype, new Function) Document them. Demos stay below the limits. If they become blockers, re-evaluate option B
Performance (demo-totalwar) Use SoA traits + useStores. Benchmark early. Rescope battle size if needed
Learning value lower than writing our own core Extensions cover the advanced flecs concepts (scheduler, prefabs, relation traits). reference/koota is studied as material
Copy-semantics prefabs differ from flecs Document the difference in the spec. The demos don't depend on shared storage

10. Repository Housekeeping

  • reference/ holds the koota, bitECS, becsy, miniplex, and flecs sources for study. Keep it out of the pnpm workspace and the build.
  • packages/ecs (tsdown starter, still named tsdown-starter) can be either:
  • (a) a thin umbrella @workspace/ecs that re-exports koota + extensions, or
  • (b) split into packages/ecs-schedule, packages/ecs-prefab, etc.

Recommended: (b) packages, plus (a) as a convenience re-export.

  • TypeScript version mismatch: the root uses ~6.0.2, packages/ecs uses ^7.0.2. Align them. Koota uses TS 6.
  • apps/web becomes the demo template.
  • Add docs/specs/ for extension specs.