Status: DRAFT. The side vocabulary and one inference are probed. The correctness of inference at runtime, and what crosses the wire, are UNVERIFIED here by construction — see §6.6.
This is the language's most distinctive rule, and the one §11.1 and §11.2 both depend on.
6.1 One unit, more than one runtime
A single compilation unit produces code that runs in a browser and code that runs on a server. There is no wire protocol to declare, no endpoint to route, and no data-transfer type to keep in step, because there is no boundary in the source for those things to sit on.
Normative. A program MUST NOT be required to state where a member runs. An implementation MUST determine it.
6.2 The three sides
⚠ The model is three-valued, not two. A reader who assumes "client or server" will mispredict the third and most interesting case.
| side | meaning |
|---|---|
| Server | the authority for data reads, security, and secrets — the server interpreter runs it |
| Client | dialogs, local UI, interaction — the client runtime runs it |
| Either | pure compute and local data mutation (new, assignment) — runs wherever the cursor already is, and may span the boundary |
Source: ExecutionSide, OsySharpExpressionEnums.cs:539.
⭐ Either is where the model earns itself. A pure helper does not need a home, so it does not get one — it runs
on whichever side called it, and the same function called from a render slot and from a server function is not two
functions. Local mutation is Either for the same reason: creating a row or assigning a field acts on the unit of
work the cursor already holds, so it is meaningful on both sides and identical on both.
6.3 Inference
The side follows from what the code touches, not from what it is called or where it is written.
- an entity read materialises rows under the declared row filter (§8) → Server
- a render expression paints and performs no I/O → Client
- pure compute and local mutation → Either
⚑ Probe (established, and run by CI). A live var holding an entity read is reported side=server:
using Osyrin.Ui;
entity Order { [MaxLength(40)] string Ref; int Total; }
[Page("/")] [AllowAnonymous] component Board() {
live var orders = Order.Where(o => o.Total > 0);
render { foreach (var o in orders) { Text(o.Ref); } }
}osy model --json reports, for that component member:
component Board
members: orders side=server6.4 Asking the compiler
Normative. An implementation MUST make the resolved side inspectable. A rule the author cannot observe is folklore.
⚠ Two different answers, and they are easy to confuse:
| you have | osy model --json gives you |
|---|---|
| a component member | a side — server / client / either |
| a top-level function | an effects record — readsData, writesData, pure, durability, plus the entities read/created/modified — and its transitive closure |
⚑ Probed. A top-level function has no side field; its keys are allowAnonymous · effects · file · isSystem · name · parameters · returnNullable · returns · scopedName · transitive · visibility. The effects record
is the answer for a function — OpenOrders reports reads: [Order], readsData: true, pure: false.
Informative. The distinction is not an oversight. A component member is bound to one side because it is part of a
rendered tree; a top-level function is frequently Either and takes its side from the caller, so "what does it
touch" is the useful question and "where does it run" is not always answerable in isolation.
6.5 Forcing the side
[Server] and [Client] exist. They force a member onto a side.
Reaching for one is almost always the wrong instinct. They were de-advertised across 718 mentions in 174
files on 2026-08-27, and removed from osy init's scaffold — the first Osy# a downloader reads — precisely
because their presence in examples taught readers to write them. The scaffold's explanatory comment was also
false: it claimed a plain function runs on the server and a render expression cannot call one; measured, a plain
pure top-level function is callable from a render.
Normative. An implementation MUST NOT require these attributes for ordinary programs, and diagnostics MUST NOT suggest adding one as the remedy for an inference the author has not been shown.
6.6 What this section does NOT establish
UNVERIFIED, deliberately. osy validate — the instrument behind every probe in this document — establishes
what the compiler accepts and what it reports. It cannot establish:
- that an inferred side is correct at runtime (that the server half really runs on the server);
- what crosses the wire at a hand-off, and in which direction;
- that a value which cannot cross is refused rather than silently dropped.
Those are runtime semantics and need a test class, per §1.5. ExecutionSidesHandlerTests is the closest existing
guard and is named here so the gap is addressable rather than merely admitted. A future revision of this section
MUST cite tests for the three claims above, or drop them.
Next: §9 Durability semantics — the other half of the model §11 depends on. Drafted.