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 codegenregenerates 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
| Actor | On this page |
|---|---|
schema-author | declares entities/parts/enums in code, diffs and pushes |
operator | reviews the panel overview, proposes and applies migrations |
ci | the build pipeline, running under a backend-service key: pushes on a merge and regenerates engine types afterwards |
At a glance
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 = 0Attribute-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[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.
| Carries | What it is |
|---|---|
key | the stable name it is addressed by. A rename in code is a rename, not a delete-and-create |
kind | an entity, a part or an enum, declared in code |
ownership mode | seed — 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 |
preset | optionally: 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.
| Always | What it is |
|---|---|
matching | is by key, never by symbol: a repeat push after renaming the symbol leaves one record rather than two |
the report | says exactly what will change before applying, and what it overwrote afterwards |
origin | is distinguishable: a record created by a push from code is told apart from one created elsewhere |
the revision | rides with it, and a push lands whole or not at all |
What codegen promises.
| Always | What it is |
|---|---|
regeneration | happens after every push, and generated types are never hand-edited: regenerating and then diffing produces no change |
naming | follows the declaration wherever it was authored — the Rarity field on Item becomes UPSItem::Rarity in Unreal and Item.Rarity in Unity |
the two directions | do 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.
| Always | What it is |
|---|---|
when one is required | a persistent declaration change that rewrites existing values |
what it declares | a version, a preview, an ordered apply, a rollback on failure, and an observable completion outcome |
coexistence | while 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
forbiddenand the deployed model is untouched: a refusal is never a partial push. That is a different refusal from a stale revision, which isprecondition_failedand 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.