Getting Started
Typhon is an in-process, real-time ACID database with an ECS (Entity-Component-System) data model. You declare
components (plain structs), group them into archetypes (the shape of an entity), spawn entities,
query them — all from a single .NET library, one engine per process, no server.
There are two ways to get going, both about five minutes: scaffold a working project in one command (fastest), or build one by hand to see every piece. Start with the scaffold; read the by-hand walkthrough when you want to understand what it generated.
The fastest start: typhon new
Install the typhon command-line tool — a .NET global tool that also hosts the Workbench, a local database GUI:
dotnet tool install --global Typhon.Cli --prerelease
Scaffold a project. It emits a runnable starter that models a small world shard — a planet of roaming characters (the SWG Light sample) — with a tick loop and profiling already wired:
typhon new MyApp
cd MyApp
Run it. The first run restores the Typhon package from NuGet, deploys the shard, ticks the runtime, and — because
typhon.telemetry.json turns the profiler on — records a profiler capture into the database's own
world-shard.typhon/profilings/ directory:
dotnet run
== ch.1 — deploy a shard of 20,000 ==
shard deployed: 20,001 characters
...
== ch.5 — the shard lives (200 ticks) ==
ran 200 ticks
...
== ch.6 — durability: close & reopen ==
REOPEN — 13,339 characters survived (the Imperial withdrawal persisted)
probe came back:
Wallet (Versioned) durable → 150 credits
Transform (SingleVersion) durable → (10.1, 30.1)
Ham (SingleVersion) durable → 100/100/100 (H/A/M)
Intent (Transient) RESET → (0, 0) (heap-only; dropped on reopen by design)
OK — ran end to end; profiler trace written: ./world-shard.typhon/profilings/20260814-220312-943.typhon-trace (22,937,035 bytes)
That last block is the point of the whole sample: the database came back, and each storage mode came back differently — which is the modeling decision ch.2 is about.
Notice where the capture landed: inside the database, not beside the app. typhon.telemetry.json names no output
path, and an absent path means "a file in this database's profilings/". A capture therefore travels with the data it
describes — which is what lets the Workbench line up what a system did against where that data actually lives.
Open the database and its newest capture together:
typhon ui --open-latest
That's the whole loop — scaffold → run → profile → inspect. The generated project is small:
| File | What it is |
|---|---|
Character.cs |
The data model — the Character archetype and its components, each in the storage mode its access pattern needs (SingleVersion hot state + spatial + index, one Versioned wallet, Transient scratch). |
Systems.cs |
The tick-loop systems: spawn characters, move + regenerate them lock-free, keep the spatial index coherent, and settle credit trades as atomic Versioned transactions. |
Program.cs |
Opens the engine, walks the API (spawn / read / transact / query / view), then runs the runtime. |
typhon.telemetry.json |
Turns on config-driven profiling (the engine self-wires it — no code needed). |
Edit the components and systems to model your own world. The chapters below explain every piece.
Turn profiling on and off
Profiling is config-driven — a typhon.telemetry.json beside your app, which the engine reads at startup with no
code from you. The CLI authors that file so you never hand-edit nested JSON:
typhon telemetry trace captures/app.typhon-trace # pin captures to an explicit path instead
typhon telemetry enable CpuSampling # add a capture channel
typhon telemetry edit # full-screen interactive flag editor
typhon telemetry trace --clear # back to the default: inside the database bundle
trace <path> is the override, not the norm. With no path set — the scaffold's default — captures go to the
database's own profilings/, which is what keeps a capture findable from the database it describes. Set an explicit
path only when you want captures somewhere else, and remember that --clear is how you get the default back.
Or build it by hand
Prefer to see every piece wired yourself? Here's the same engine, hand-built. Add the package to a .NET 10 project:
dotnet add package Typhon --prerelease
Prerelease packages are opt-in — the --prerelease flag (or checking "Include prerelease" in your IDE) is required.
1. Define a component
A component is just data — a plain, blittable struct with a [Component] attribute. The attribute's name is a
stable schema identity; the number is its revision. StorageMode.Versioned is the default (full ACID) and worth
spelling out. Add [Index] to a field to make it fast to filter on.
using Typhon.Schema.Definition; // [Component], [Field], [Index], [Archetype], Comp<T>
namespace Shard; // component + archetype types must live in a namespace, not the global one
[Component("Shard.Faction", 1, StorageMode = StorageMode.SingleVersion)]
public struct Faction
{
[Index(AllowMultiple = true)] public int Value; // indexed → fast to filter on
}
[Component("Shard.Wallet", 1, StorageMode = StorageMode.Versioned)]
public struct Wallet
{
public long Credits; // Versioned: the one component that earns full ACID
}
An archetype is the fixed shape of an entity — a partial class that names itself and registers its component
slots. The static Comp<T> handles (Character.Wallet) are how you refer to each slot when spawning, reading, and
querying.
[Archetype]
public sealed partial class Character : Archetype<Character>
{
public static readonly Comp<Faction> Faction = Register<Faction>();
public static readonly Comp<Wallet> Wallet = Register<Wallet>();
}
2. Open the engine, spawn, and read
DatabaseEngine.Open is the one-line setup: it names the on-disk database (a shard.typhon directory in the
working folder), registers your components (your archetype self-registers at assembly load), and hands back a
ready-to-use engine. Do this once at startup; using var flushes and releases the file lock at scope end.
Writes go through a short-lived transaction; reads see a consistent point-in-time snapshot without waiting on writers.
using Typhon.Engine; // DatabaseEngine, EntityId, transactions, queries
using var dbe = DatabaseEngine.Open("shard.typhon", o => o
.Register<Faction>()
.Register<Wallet>());
// Spawn an entity (a write — needs a transaction)
EntityId scout;
using (var tx = dbe.CreateQuickTransaction())
{
scout = tx.Spawn<Character>(
Character.Faction.Set(new Faction { Value = 1 }),
Character.Wallet.Set(new Wallet { Credits = 250 }));
tx.Commit();
}
// Read it back (a read — sees a consistent snapshot)
using (var tx = dbe.CreateQuickTransaction())
{
var e = tx.Open(scout);
var faction = e.Read(Character.Faction);
var wallet = e.Read(Character.Wallet);
Console.WriteLine($"faction {faction.Value} with {wallet.Credits} credits");
}
💡 Hosting in a DI app? The same fluent options work through
services.AddTyphon(o => o.DatabaseFile("shard.typhon").Register<Faction>()…), which composes the engine into your service collection.Open()is the standalone equivalent that owns a private container for you.
3. Query
Query<Character>() starts a query over all Character entities; WhereField<Faction>(...) filters on an indexed
field, so the engine drives the scan from the index instead of walking the archetype (use Where for computed or
unindexed conditions — see ch.4); Count() returns how many match (Execute() would instead hand
back the matching EntityIds to iterate).
using (var tx = dbe.CreateQuickTransaction())
{
int rebels = tx.Query<Character>()
.WhereField<Faction>(f => f.Value == 1)
.Count();
Console.WriteLine($"{rebels} rebel(s)");
}
4. Commit and transactions
Every write enters the engine through a transaction, and nothing is visible to anyone else until Commit().
CreateQuickTransaction() is the simplest form — it manages the durability boundary for you. This is the behaviour
of Versioned components (the default): transactional writes, snapshot-isolated reads, crash-safe.
using (var tx = dbe.CreateQuickTransaction())
{
var e = tx.OpenMut(scout); // mutable handle (vs. read-only tx.Open)
e.Write(Character.Wallet).Credits += 500; // pay a reward — an in-place ref write
tx.Commit(); // durable + visible here
// No Commit() → the change is discarded at scope end.
}
Hot per-frame data and throwaway scratch can opt into the faster SingleVersion / Transient storage modes instead —
those relax the transactional model on purpose. See Changing data: transactions & durability.
Next steps
- Start here — your first app — the fuller walkthrough, piece by piece (it is the scaffold's
Program.cs). - Modeling your world — archetypes, indexes, the three storage modes, and spatial queries.
- Changing data: transactions & durability — the real transaction model and what survives a crash.
- Systems & the tick loop — the runtime that ticks your world in parallel (the scaffold's
Systems.cs). - User Guide index — the full reading ladder.
- In-depth overview — the contributor/power-user reference: struct layouts, algorithms, invariants.
- Runnable example — the scaffold's exact sources live in
doc/guide/example;dotnet run --project doc/guide/examplewalks the whole arc.