Table of Contents

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