Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Guides

One program

A page, its live data and the function behind a button — one language, one compile, and nothing in between them to build. This walks a real app end to end and shows where the compiler put each part, and why none of it was yours to decide.

01

There is nothing in this project except the app

Five files, and every one of them is your model, your rules or your screens. No client project. No API project, no DTOs, no serializer, no schema migration, no state-management library — not one file whose job is to carry something from one place to another. That is not because the app is small: the platform's own Admin is 55 files of exactly this and nothing else.

filewhat is in it
app.osythe manifest — the app's name and what it uses
model/auth.osythe user, the login, and the login page
model/chat.osythree entities, their security, and two functions
model/room.osythe page
model/theme.osythe colours, in light and dark

02

One file, and the compiler splits it

This is the page. Some of it becomes browser code and some of it becomes server code, and the markers say which. Nothing in the source says which — that is what the compiler decided, from what each line touches.

  runs in the browser  runs on the server— and nothing in the file says so

demo/chat-room/model/room.osyverbatim — this file compiles
// THE PAGE — and it is small on purpose. Everything interesting about this spike is in what the page does NOT say.

[Page("/")]
[Render(CSR)]
[Title("The room")]
component Room() {
  Guid roomId = Guid.Empty;

1  string draft = "";

2  live var messages = Message
    .Where(m => m.Conversation.Id == roomId)
3    .Include(m => m.Author)
    .OrderBy(m => m.SentAt)
    .ToList();

4  on mount { roomId = JoinTheRoom(); }

5  action Send() {
6    Say(roomId, draft);
    draft = "";
  }

7  render {
    Stack(gap: 4, p: 6, maxW: "720px", bg: Colors.Bg, color: Colors.OnBg, minH: "100vh") {
      Text("The room", fontSize: FontSize.Hero, fontWeight: FontWeight.Medium);
      // The viewer's scalar fields ride the boot bag, so this is CLIENT state and costs no query.
      // Binding the whole entity (`var me = Session.CurrentUser;`) would have been a server read —
      // the row under this app's security — to render one string the browser already had.
8      Text("Signed in as " + Session.CurrentUser.Email, fontSize: FontSize.Caption, color: Colors.TextMuted);

      Stack(gap: 2) {
9        foreach (var m in messages) {
          Row(gap: 2) {
            Text(m.Author.Email + ":", fontWeight: FontWeight.Medium);
            Text(m.Body);
          }
        }
      }

      Row(gap: 2) {
        Input(value: draft, label: "Message", placeholder: "say something");
        Osyrin.Button("Send", onClick: Send);
      }
    }
  }
}

1

Client. A plain field on a component is browser state. Typing updates it at 60fps and no one else ever hears about it.

2

Server, and it is a SUBSCRIPTION. This is an entity query, so it runs on the server, under the security this app declared. live is what makes it stay true: a message somebody else posts lands here without a poll, a refetch or an invalidation key.

3

One statement, not one per row. The author of every message comes back with the messages — the N+1 is not optimised away later, it is not written.

4

Both, and that is the interesting one. The hook is client code; JoinTheRoom() is a server function. The hop is the call. There is no endpoint, no fetch, no request type and no response type.

5

Client. An action is a gesture — it runs where the click happened, sets client state, and calls the server where it needs to.

6

Server. Same shape as any other call you write. The argument is a string on both sides because there is only one declaration of it.

7

Client. Layout is arguments — gap, p, maxW, bg — not classes, and not a stylesheet that has to agree with the markup.

8

Client, and it costs nothing. The viewer’s scalar fields ride the boot descriptor, so reading one in a render is browser state — no round trip. Binding the whole entity is not: var me = Session.CurrentUser; is a QUERY (side: server), because that is the row under this app’s security rather than the display bag. This page used to do exactly that, to print one string the browser already had — a server read for nothing.

9

Client. Ordinary control flow over the rows the server sent. m.Author.Email is already loaded, because of ④.

03

The other half of the same program

The entities the page queried, the rule that decides who may read them, and the two functions it called. Same language, same file extension, same compile.

demo/chat-room/model/chat.osyverbatim — this file compiles
entity Conversation {
1  [Required][MaxLength(120)] string Topic;
  [Required] DateTime StartedAt;
2  security {
    allow read, create when IsAuthenticated;
  }
}

entity Participant {
  [Required] Conversation Conversation;
  [Required] User Person;
  [Required] DateTime JoinedAt;
  security {
3    allow read where Participant.Any(p => p.Conversation == Conversation && p.Person == user);
    allow create where Person == user;
  }
}

entity Message {
  [Required] Conversation Conversation;
  [Required] User Author;
  [Required][MaxLength(2000)] string Body;
  [Required] DateTime SentAt;
  security {
    allow read where Participant.Any(p => p.Conversation == Conversation && p.Person == user);
    allow create where Author == user
                    && Participant.Any(p => p.Conversation == Conversation && p.Person == user);
  }
1

The type and its constraints are one declaration. This is the C# type the page sees, the Postgres column, and the validation the server enforces — written once.

2

⭐ The whole security model of this app is these three blocks. No entity is readable by default; each says who may read and write it, and every query anywhere in the program obeys — including the live var in §2.

3

A row filter, not a check. It becomes part of the SQL: a non-participant's query does not fail, it returns nothing, because the rows were never in scope. There is no call site that could forget it.

demo/chat-room/model/chat.osyverbatim — this file compiles
Guid JoinTheRoom() {
1  var room = Conversation.OrderBy(c => c.StartedAt).FirstOrDefault();
  if (room == null) {
2    room = new Conversation { Topic = "The room", StartedAt = DateTime.UtcNow };
  }

  var me = Session.CurrentUser;
  if (!Participant.Any(p => p.Conversation == room && p.Person == me)) {
    new Participant { Conversation = room, Person = me, JoinedAt = DateTime.UtcNow };
  }

3  UnitOfWork.Commit();
  return room.Id;
}

1

LINQ over your entities, compiled to one statement. This is the same expression language the page used — because it is the same language.

2

Constructing an entity is how a row is created. There is no repository, no Add, and no place to forget one.

3

The unit of work commits once, at the end. Everything above it either happened or did not.

04

Ask the compiler where it put each line

You do not have to take the markers on trust, and you should not have to. The decision is in the resolved model, and the model is a command.

$ osy model --json      # in demo/chat-room

"name": "Room", "route": "/",
"members": [
  { "name": "me",       "kind": "query", "side": "server", "live": false },
  { "name": "roomId",   "kind": "state", "side": "client", "live": false },
  { "name": "draft",    "kind": "state", "side": "client", "live": false },
  { "name": "messages", "kind": "query", "side": "server", "live": true  }
]

"name": "Say", "effects": { "reads": ["Conversation", "User"],
                  "creates": ["Message"], "writesData": true }

That is the whole basis for the decision: Say touches entities, so it is a server function; draft touches nothing, so it is browser state. You never wrote either fact down, and you can always ask.

05

What happens when you change it

The changes that break a two-codebase app, and what they cost here

Rename Message.Body
compiles
One rename. The page reads m.Body, so a stale name is a compile error rather than a field that arrives undefined in a browser.
Add a field the page shows
compiles
Declare it, compile. There is no DTO to widen, no mapper to update and no client type to regenerate.
Tighten the security rule
compiles
The live var in §2 narrows with it, immediately, along with every other read in the program. Nothing at a call site changes, because nothing at a call site was checking.
Read an entity from inside render
refused
Refused, by name. Rendering is synchronous and a server hop is not — so the compiler says so at compile time instead of letting the page await mid-paint.
Bind live var to an ordinary server function
refused
Refused: nothing would keep that value current. Either it is a query, which subscribes, or a stream<T>, which pushes. See the agents guide.
Deploy the two halves separately
refused
There are not two halves to deploy. One compile produces both, so they cannot be at different versions in production — the commonest outage in a split stack is not mitigated here, it is unavailable.

Where to go next

Agents

The same seam, with a model on the far side of it.

chat-room

The app on this page, in full, one command away.

UI reference

Every construct in §2, one page each.