Skip to main content
How it works

A closed world, compiled.

A game knows every state, command and question before it ships. Conjure treats that as a contract: everything a runtime would otherwise do on the device or on the server happens once, at build time — and what comes out is deterministic enough to run in two places and agree.

Declarative, not imperative

You say what. The compiler decides how.

Imperative code tells the machine the steps: loop, check, cache, invalidate, redraw. Declarative code states the result you want and leaves the steps to a compiler that knows your whole game. The difference is not style — it is who owns the hard decisions, and whether a change costs one line or four files.

interactive · same requirement, two ways

Show the 10 cheapest offers the player can afford. Keep it current.

imperative
public sealed class ShopOffers(Wallet wallet, OfferCatalog catalog, OfferListView view)
{
readonly List<Offer> cache = new();③
bool dirty = true;③
int lastGold = -1;
public void Start()
{
wallet.OnGoldChanged += _ => dirty = true;
catalog.OnPriceChanged += _ => dirty = true;
// (no OnOfferRemoved handler)④
}
public void Update()①
{
if (!dirty && wallet.Gold == lastGold) return;
Rebuild(wallet.Gold);
}
void Rebuild(int gold)
{
var result = new List<Offer>();②
foreach (var o in catalog.All)
if (o.Price <= gold) result.Add(o);
result.Sort(ByPrice);
cache.Clear();
for (int i = 0; i < result.Count && i < 10; i++)
cache.Add(result[i]);
lastGold = gold;
dirty = false;
view.Refresh(cache);
}
static int ByPrice(Offer a, Offer b) => a.Price.CompareTo(b.Price);
}
  1. ①runs every frame
  2. ②allocates a list per run
  3. ③manual cache + dirty flag
  4. ④forgot: offer removed → stale row
declarative
reactive query AffordableOffers(gold: int) -> Offer[] {
from Offer o
| filter o.Price <= @gold
| sort o.Price
| take 10
}
who decidesimperativedeclarative
data structure & indexyoucompiler
when to recomputeyoucompiler
what to allocateyoucompiler
ordering & invalidationyoucompiler
threadingyoucompiler

imperative · ≈ 40 lines · 4 decisions that are yours · 1 bug already indeclarative · 6 lines · 0 of those decisions are yours

The same requirement, written as steps on the left and as a result on the right. The four marked places on the left are decisions you own — and one is already a bug.

Why this is the door for assistants

Small surface

The whole language fits in a few pages. An assistant can hold all of it, and so can you.

The compiler reviews every draft

A draft is bound, typed and planned before it exists. Wrong shapes come back as diagnostics, not as bugs in production.

Humans review intent

You read six lines of what, not forty lines of how.

Same code, both sides

One command. Two places. One answer.

The command you declare is compiled once. The device runs it to move the game forward; the server runs the very same code to check. Here is BuyItem(sword) doing both.

interactive · one command, two places
input state
#91c0
seed
0x5A3C
output state
—
time
— µs
same input, same seed
input state
#91c0
seed
0x5A3C
output state
—
time
— µs

same result verified on x86-64, ARM64 and IL2CPP

Both sides start from the same verified state and the same seed — the only inputs the command can read.

under the hood · what the compiler does
generated C# (abridged)
public static class BuyItemExecutor
{
public static BuyItemOutcome ExecuteCore(
DbContext context, BuyItemCommand command, CommandExecutionFrame frame)
{
if (context is null) throw new global::System.ArgumentNullException(nameof(context));
var db = (GameDbContext)context;
if (!(command.cost > 0))
{
return BuyItemOutcome.Failure(BuyItemErrorCode.InvalidCost);
}
var __mutation1Key = command.playerId;
Wallet __mutation1Row;
var __mutation1Matched = db.Wallets.TryFindById(__mutation1Key, out __mutation1Row);
if (!__mutation1Matched)
{
return BuyItemOutcome.Failure(BuyItemErrorCode.NoWallet);
}
__mutation1Row = new Wallet
{
PlayerId = __mutation1Row.PlayerId,
Gold = __mutation1Row.Gold,
};
__mutation1Row.Gold = __mutation1Row.Gold - command.cost;
db.Wallets.Update(__mutation1Row);
var wallet = new { gold = __mutation1Row.Gold };
// … upsert Inventory: same shape — key, TryQuery on its unique index, Update or Add
return BuyItemOutcome.Success(new BuyItemResult { gold = wallet.gold });
}
// + Execute(...) wrapper and error mappers
}

Not a protocol. Not a second implementation. The same compiled command, with a fixed math library and a seeded random, so both sides can only reach the same state.

Inside the compiler

Five stages, all at build time

Nothing on this path runs on the player's device. The output is plain C# (or a portable artifact) — there is no query parser on the hot path.

game.conjureyour data & queries
bind & typeanalysis
optimizeCascades · cost-based
planphysical plan
emit C#plain C#
The optimizer

A glass box, not a black box

A Cascades-style, cost-based optimizer makes the choices a senior engineer makes by hand — which index, which join strategy, what order, when to stop scanning, whether to eliminate a sort — but deterministically and exhaustively, using your schema, your indexes, cardinality estimates and profile data. The difference: every choice is inspectable. The plan is output you can read; the strategy names are real and finite; nothing is hidden.

The optimizer can also read a profile of real sessions — including deterministic replays — so cardinality estimates, plan choices and buffer sizes come from how the game is actually played, not from guesses. The profile is governed and fail-closed: a stale, unapproved or schema-mismatched profile is rejected, never silently used. Profile-guided optimization →

explain
// illustrative: the plan ConjureDB picks for GetTopScorers
TopN (limit=count, order=Score desc)
└── Scan (Player,
strategy=SortedSetScan,
index=Player_ByScore)

Ask for the plan and read what it picked — an index walk, bounded, no sort, no allocations.

Reactive data

Not an event bus. A maintained result.

Rebuilding every frame pays for the whole table even when nothing changed. Rx moves events and every subscriber re-runs its query. Incremental view maintenance moves the change itself through the query's own operators and keeps the result as a table. Same event, three mechanisms — step through it.

interactive · one event, three mechanisms
Top players1Aria60Ravens2Bo58Wolves3Cyn57Owls4Player 756Ravens5Dax55Owls6Eli53Wolves7Player 4251Wolves8Fen50Ravens9Gus49Owls10Hal49Wolves
  1. sourcePlayers · 10 000 rows1 row changed
  2. maintainersjoin · sort · take 10probe 1 key · insert at #7, evict #10
  3. resultTopPlayers · 10 rows2 rows changed

frame budget · ≈ 3 rows × 5 changes = 15 rows per frame

The level-up travels as one delta: probe one guild, compare with the cut-off, insert at #7. Two rows of the result change; nothing else is touched.

under the hood
schema
reactive query TopPlayers() -> LeaderboardRow[] {
from Players
| join Guilds g (GuildId == g.Id)
| sort { -Level, Name }
| take 10
| select { … }
}

IVM is not a subscription trick. The compiler lowers the query into maintainers — filter, join arrangements, grouped accumulators, bounded Top-K state — and each commit pushes only the delta through them. The result is a table: consistent once per commit, proportional in cost to the change, and indexable like any other table. A reactive query is how a UI view subscribes to that view model. A shape the maintainers cannot keep does not compile.

And it is honest about its limits: a query shape it cannot maintain incrementally is rejected at compile time, never silently degraded to a hidden full rescan.

Honest limits

What the closed world rules out

Queries nobody declared

Designers cannot type new queries that run on players' devices. Changes after the build ship as compiled artifacts through the liveops lane, verified like everything else.

Many writers on one state

Each piece of state has one owner and one writer. Shared state such as clans and leaderboards gets its own owner on the server; there is no multi-writer contention to resolve.

Rendering and feel

Conjure is the logic and the data. Unity still draws the frame, plays the sound and reads the touch.

A schema to maintain

There is a schema and a build step. For a prototype you will delete in two weeks, a dictionary is still the right answer.

No magic

If you don't trust it, read it

The generated code is plain source you can open. There is no reflection, no runtime code generation, nothing dynamic on the query path — which is exactly why it survives IL2CPP and a tight mobile frame. Conjure earns trust by removing mystery, not by asserting quality.