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
kootafrom 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¶
- Toolchain cost. C + emscripten builds, debugging across the WASM boundary, and keeping up with upstream flecs releases. The team does not want this.
- 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).
- Component data model mismatch. Idiomatic TS components are objects, strings, and arrays. Flecs stores raw bytes described by its
metaaddon. Strings, nested objects, and references need manual marshalling or a side-table on the JS side, which partly defeats the purpose. - Lifetimes and GC. Closures registered as systems/observers, plus JS-side handles, need explicit cleanup (
FinalizationRegistryis not deterministic). - Multithreading. Running the flecs scheduler with threads in WASM requires pthreads +
SharedArrayBuffer+ COOP/COEP headers, which makes Node/Bun/Deno/browser deployment harder. - Low learning value. Most of the effort goes into FFI plumbing, not ECS concepts.
- 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/afterordering). - 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 Functionfor accessors, so a strict CSP needsunsafe-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
$internalonly 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>.mdreferences 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
- Write a short spec in
docs/specs/<pkg>.md. Include the flecs reference, scope (in/out), the TS API sketch, and edge cases. - Turn the spec into Vitest tests. Port flecs tests where useful.
- Implement against koota's public API until the tests pass.
- 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 namedtsdown-starter) can be either:- (a) a thin umbrella
@workspace/ecsthat 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/ecsuses^7.0.2. Align them. Koota uses TS 6. apps/webbecomes the demo template.- Add
docs/specs/for extension specs.