Skip to main content
Your game's model

Not translated yet — showing English.

Schema as Code

Declare the model in code, push it, get types back. The developer's way into the schema: the admin panel and code author the same model, and codegen closes the loop for every engine.

When to use it

  • Your data model should live in code and be reviewed like code: declare, schema diff, schema push.
  • Engine types must never drift from the deployed model: schema codegen regenerates the Unreal C++ and Unity C#.
  • A breaking change must be readable and cancellable before it runs: propose → plan → apply.
  • You reuse one bundle (Stat, Interactable) across projects: declare it once as a preset.
  • Skip it when an operator only tweaks values in the admin panel. The model change still diffs back into code.

Who does what

ActorOn this page
schema-authordeclares entities/parts/enums in code, diffs and pushes
operatorreviews the panel overview, proposes and applies migrations
cithe build pipeline, running under a backend-service key: pushes on a merge and regenerates engine types afterwards

At a glance

Declaring Item with an embedded Stats part and an enum, pushed as one schema
[Entity("item")]
public class Item
{
    public string Name = "";
    public Rarity Rarity;              // an enum declared the same way
    public Stats Stats = new();        // a part — embedded, no lifecycle of its own
}

[Part("stats")]
public class Stats { public int Power; public int Weight; }
@Entity('item')
export class Item {
  name = '';
  rarity!: Rarity;                   // an enum declared the same way
  stats = new Stats();               // a part — embedded, no lifecycle of its own
}

@Part('stats')
export class Stats { power = 0; weight = 0; }
@entity("item")
class Item:
    name: str = ""
    rarity: Rarity                     # an enum declared the same way
    stats: Stats = Stats()             # a part — embedded, no lifecycle of its own

@part("stats")
class Stats:
    power: int = 0
    weight: int = 0
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

USTRUCT(PSPart = "stats")
struct FItemStats
{
    GENERATED_BODY()

    UPROPERTY() int32 Power;
    UPROPERTY() int32 Weight;
};

UCLASS(PSEntity = "item")
class UItem : public UObject
{
    GENERATED_BODY()

    UPROPERTY() FString    Name;
    UPROPERTY() EPSRarity  Rarity;       // an enum declared the same way
    UPROPERTY() FItemStats Stats;        // a part — embedded, no lifecycle of its own
};
// declarations compile into the same pushed model — playserv push from the UE project or CI
Unity C# is the same C# API here — the same attributes compile in Unity, on the 2021.3 baseline — no C# 12 syntax here
[Entity("item")]
public class Item
{
    public string Name = "";
    public Rarity Rarity;              // an enum declared the same way
    public Stats Stats = new();        // a part — embedded, no lifecycle of its own
}

[Part("stats")]
public class Stats { public int Power; public int Weight; }
playserv schema diff       # local declarations vs deployed schema
playserv schema push       # with a revision precondition — no blind overwrites
playserv schema codegen    # regenerate Unreal C++ / Unity C# types

Push is an explicit step

Nothing is uploaded when you save a file. A declaration reaches the deployed model only when playserv push (or schema push) runs, from your machine or from CI, and it carries the revision it was diffed against. A panel edit shows up as drift in schema diff; moving a change in either direction is always a command you run.

The model

What a declaration carries.

CarriesWhat it is
keythe stable name it is addressed by. A rename in code is a rename, not a delete-and-create
kindan entity, a part or an enum, declared in code
ownership modeseed — code creates the record if it is absent and a repeat push leaves the values alone, so the admin console owns them afterwards; or managed — code owns it always, every push brings values to what is declared, and edits from the admin console are refused rather than applied and lost on the next push
presetoptionally: a reusable bundle — an entity preset such as stats or world-objects — declared once and applied like a type. It introduces no new kind of declaration

What a push promises.

AlwaysWhat it is
matchingis by key, never by symbol: a repeat push after renaming the symbol leaves one record rather than two
the reportsays exactly what will change before applying, and what it overwrote afterwards
originis distinguishable: a record created by a push from code is told apart from one created elsewhere
the revisionrides with it, and a push lands whole or not at all

What codegen promises.

AlwaysWhat it is
regenerationhappens after every push, and generated types are never hand-edited: regenerating and then diffing produces no change
namingfollows the declaration wherever it was authored — the Rarity field on Item becomes UPSItem::Rarity in Unreal and Item.Rarity in Unity
the two directionsdo not fork: whatever is declared in code appears in the panel, and whatever an operator authors in the panel diffs cleanly against code, so schema diff shows every difference

What a migration promises.

AlwaysWhat it is
when one is requireda persistent declaration change that rewrites existing values
what it declaresa version, a preview, an ordered apply, a rollback on failure, and an observable completion outcome
coexistencewhile two data versions are live, reads and writes declare which versions they accept — the runtime never infers compatibility from field names

Errors

  • A caller without the role gets forbidden and the deployed model is untouched: a refusal is never a partial push. That is a different refusal from a stale revision, which is precondition_failed and means the diff was computed against a schema that has since moved — re-diff and push again.
  • A value outside a declared bound is refused on write, never clamped, and a string past its declared length likewise. Clamping would produce a value that is valid and wrong.
  • An invalid UTF-8 sequence is rejected on write rather than repaired.
  • A breaking persistent change with no declared migration cannot be published at all.

Limits

Declaration-shaped ceilings are checked at declaration time — on deploy or on publication — rather than at first use, wherever the runtime symptom would not look like a refusal. The numbers arrive with the platform limits chapter.