Core Concepts
/
State Transitions
State Transitions
Declare a state machine on a resource and saving through a transition becomes a typesafe, state-machine-safe backend function — declared once in TypeScript, enforced on every surface.
View as Markdown
llms.txt
A resource can declare a state machine over one of its fields. Saving a record through a transition is then a fully typesafe, state-machine-safe backend function: declared once in TypeScript, compiled into the manifest, and enforced by the Rust core identically on the server, on Lambda, and in the in-browser WASM engine — no per-resource server code.
Declare the machine
With the resource() DSL, transitions live in the options:
resources/flights.ts
ts · copy
1
export
const
Flight
=
resource
(
"airports"
,
"flight"
,
{
2
number
:
s
.
string
({
maxLength
:
8
}),
3
status
:
s
.
string
({
maxLength
:
20
}),
4
},
{
5
transitions
:
{
6
field
:
"status"
,
7
states
:
[
"scheduled"
,
"boarding"
,
"departed"
,
"cancelled"
],
8
initial
:
"scheduled"
,
9
on
:
{
10
board
:
{
from
:
"scheduled"
,
to
:
"boarding"
},
11
depart
:
{
from
:
"boarding"
,
to
:
"departed"
,
set
:
{
gate
:
null
}
},
12
cancel
:
{
from
:
[
"scheduled"
,
"boarding"
],
to
:
"cancelled"
},
13
},
14
},
15
});
On a class entity, the same declaration is a static transitions member (statics are never columns, so it rides alongside the fields):
entities/flights.ts
ts · copy
1
export
class
Flight
{
2
number
!:
string
&
MaxLength
<
8
>;
3
status
=
"scheduled"
;
// the state field — the literal doubles as the default
4
5
static
transitions
=
{
6
field
:
"status"
,
7
states
:
[
"scheduled"
,
"boarding"
,
"departed"
,
"cancelled"
],
8
initial
:
"scheduled"
,
9
on
:
{
10
board
:
{
from
:
"scheduled"
,
to
:
"boarding"
},
11
depart
:
{
from
:
"boarding"
,
to
:
"departed"
,
set
:
{
gate
:
null
}
},
12
cancel
:
{
from
:
[
"scheduled"
,
"boarding"
],
to
:
"cancelled"
},
13
},
14
};
15
}
What compiles
states become the field's enum — a select control in forms, and the member gate in the validation machine.
initial becomes the column default and the only state a record may be created in.
The machine lands in schema.rastack.json and rides the OpenAPI contract as x-rastack-transitions — see Manifest & OpenAPI.
Enforced on every surface
An update that moves the state field must follow a declared edge. If it doesn't, every surface rejects it the same way:
the engine's answer, everywhere
text · copy
1
400 Bad Request
2
status: no transition from "scheduled" to "departed"
Forms — the record-level transition gate runs in useForm alongside the per-field gates.
Imports — imported rows are creates, so rastack import rejects rows born mid-machine (any state but the initial one), with a row-located error.
The engine rastack-api-core enforces the machine on the server, on Lambda, and in the browser over WASM. There is no surface that skips it.
The typed form surface
useForm exposes the machine, narrowed to the record's current state:
app/flights.tsx
tsx · copy
1
const
form
=
useForm
(
Flight
,
{
id
});
2
3
form
.
state
// "scheduled"
4
form
.
transitions
// [{ name: "board", to: "boarding", … }]
5
// — only the edges legal from the current state
6
form
.
transition
(
"board"
);
// typed: only names Flight declares compile
7
8
<
AutoForm
form
={
form
}
/>
// renders one action button per available transition
Good to know
Transition names are literal types: form.transition("explode") is a compile error, not a runtime 400.
set patches — a declarative backend function
A matched edge's set patches are applied by the engine, server-side, atomically with the state change — depart above clears gate on every surface, whether the save came from a form, an import, or a raw API call. Declared once in TypeScript, executed by the engine: a backend function with no backend code.
Tip
The transition gate is part of the constraint state machine — see Validation & Data Import for the per-field gates it sits alongside.
Build with AI
Every page is available as clean Markdown for LLMs and coding agents. Point one at the files below to build on top of React API Stack.
React API Stack · alpha · built to scale to millions