# How an Osy# app works (the execution model)

> Read this before you write anything. An Osy# app is ONE model — you do not build an API for it, and you do not write the layers you are probably expecting: no controllers, no routes, no DTOs, no serializers, no fetch layer, no client store, no ORM, no auth middleware. A page reads data by naming the entity; a button calls a server function by name. What is left for you to write is the business problem. What you must still reason about is small, and this page says exactly what it is.

<!-- id: project-index · area: project · stability: stable · html: https://osysharp.com/reference/project/index/ -->

## Summary        {#summary}
**You do not write an API for your own app.**

A page reads data by **naming the entity**. A button calls a server function **by name**. The engine carries the flow
across the client/server boundary for you — durably — and your security rules are already inside the query when it
runs.

```osy title="a whole feature: the data, the rule, the server, the screen" test app=project-index
[Role] enum AppRole { Authenticator, Member }

[Principal]
entity User {
  [Required, MaxLength(255)] string Email;
  [MaxLength(200)] string PasswordHash;
  security {
    allow read where Id == user.Id;                   // the rule lives HERE — not in an endpoint
    deny read PasswordHash when !IsAuthenticator;     // …and the credential is masked from everyone but sign-in
  }
}

policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Authenticator);

entity RoleGrant {
  [Required] User User;
  [Required] AppRole Role = AppRole.Member;
  security { allow read where User == user; }
}

entity Note {
  [Required, MaxLength(200)] string Title;
  security {
    allow read, update, delete where CreatedBy == user.Id;   // …and it is compiled INTO every query
    allow create when IsAuthenticated;
  }
}

// An ordinary server function. No route, no controller, no request/response type.
Note Publish(string title) {
  return new Note { Title = title };   // committed when the function returns. Note what is ABSENT:
}                                      // nobody sets an owner — `CreatedBy` is stamped for you.

[Page("/notes")]
[Render(CSR)]
component Notes() {
  var notes = Note.ToList();          // a SERVER read. No fetch, no endpoint, no DTO.
  string draft = "";                  // a client value — inferred from the initializer, not declared

  action Add() { Publish(draft); }    // calls the server BY NAME. The engine hands off.

  render {
    Input(value: draft, placeholder: "Title");
    Button("Publish", onPress: Add);
    foreach (var n in notes) { Text(n.Title); }
  }
}
```

That is the entire feature. **There is no other file.** No `NotesController`, no `NoteDto`, no `notesApi.ts`, no
`useNotes()` hook, no repository, no migration, no `[Authorize]` on an endpoint — and `Note.ToList()` returns *your*
notes rather than everyone's, because the `security { }` block is part of the query rather than a check someone
remembered to write.

## Description    {#description}

### What you are NOT building   {#not-building}
If you have built a SaaS app before, you are carrying a mental checklist. Most of it does not apply here, and the time
you would spend on it is the time you should spend on the actual problem:

| What you would normally build | What replaces it |
|---|---|
| REST controllers, routes, an API surface for your own UI | **Nothing.** The UI calls functions by name. |
| DTOs, view models, mappers | **Nothing.** The entity is the wire type. |
| A serialization layer | **Nothing.** One shared codec, for every app. |
| `fetch`/axios, URLs, CORS config | **Nothing.** There is no URL to call. |
| A client store, reducers, cache invalidation | **Nothing.** Reads land in a shared store; a `live` read refetches on a change signal. |
| Change tracking — dirty flags, diffing, a save pipeline | **Nothing.** The platform tracks what you created and changed. You only say **when** to persist (`UnitOfWork.Commit()`), and on the server not even that. |
| An ORM, a repository, a `DbContext` | **Nothing.** You query the entity type directly ([Querying data](https://osysharp.com/reference/query/index/)). |
| Migrations, DDL | **`osy compile`.** Additive change (a new entity, a new field) just applies. See the caveat below. |
| Auth middleware, per-endpoint guards | **`security { }` on the entity** ([The security model](https://osysharp.com/reference/security/index/)) + `[Authorize]` on the page. Written once, enforced everywhere. |
| Session/JWT plumbing | **`Security.IssueJwt`** + `Session.SignIn` ([auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/)). |
| `async` / `await` / `Task<T>` | **Nothing.** There is no `async` in Osy# ([async / await — why Osy# has neither](https://osysharp.com/reference/function/async-await/)). |
| Background jobs, queues, retry logic | **A `workflow`** — durable states that survive a restart. |

The platform's own Admin — a complete app with authentication, grids, tabs and forms — declares
**no API, no DTO, no fetch layer, no store and no migration.** Its entire authentication surface is one file of about
forty lines, most of which are comments.

### It is one model — but it is not one machine   {#the-boundary}
This is where an honest guide has to stop selling. **One language, one call syntax, and a boundary the engine crosses
for you — but three things stay yours to reason about.** Get these three right and you can forget the rest.

**1. Where a statement runs — and, just as importantly, where it does *not*.** Two things reach the server from a UI
action, and only two:

- **A query** — naming an entity (`Note.ToList()`, `Note.Single(…)`). That is the fetch.
- **A call to a top-level function** — `Publish(draft)`. That is a hand-off, and therefore a round trip.

**Everything else is local.** Once rows are in hand they live in the client's store, so reading their properties —
`n.Title`, `n.Owner`, a field on a row you are editing — costs **nothing**. It is not a request; it is memory. So do
not contort a component to "avoid extra reads" of data it already has: there are no extra reads. Pure computation,
control flow and looping over rows you fetched all run where the cursor already is.

What *is* worth noticing is a loop that calls a **function** ten times — that is ten hand-offs. The syntax hides the
round trip; the latency does not.

**2. When data persists.** You never write change tracking. Osy# tracks it all — which rows you created, which fields
you touched, what the new values are — with no dirty flags, no diffing, no "is this modified" bookkeeping, and no save
pipeline of your own. **The only thing you ever say is *when* it should persist**, and even that is only on one side:

- **In a server function**, `new Note { … }` is **persisted when the function returns.** There is nothing to say — no
  `Save()`, no `UnitOfWork.Commit()` ([function](https://osysharp.com/reference/function/declaration/)).
- **In a UI action**, the edit is **staged optimistically** — it appears on screen at once, and persists when a
  **`UnitOfWork.Commit()`** runs, which belongs in a **Save** action.

So the same statement means "write it" on the server and "stage it, and show it immediately" on the client. That is
not an inconsistency to memorise — it is the whole reason a form can be typed into, previewed and abandoned without a
round trip per keystroke. The tracking is done for you either way. You are only ever choosing the moment.

**3. How a page is delivered.** `[Render(CSR)]` is the default and is always correct. `[Render(SSR)]` pre-renders for a
faster first paint — but **only for `[AllowAnonymous]` pages**, so a signed-in page is delivered client-side either
way. Choosing it never changes what a page *does*.

### The flow is durable, which is why the boundary can be invisible   {#durable}
When an action hits a server call, it does not fire a request and wait. It **suspends** — its whole continuation is
persisted — the server runs the function, and the action **resumes mid-statement**. That is why the boundary needs no
syntax: the engine can stop and restart your flow anywhere, so it does not need you to mark where the seams are.

The consequence worth knowing: a suspended flow survives a **client reload and a server restart**. The thing that would
be a distributed-systems problem in the architecture you were about to build is a property of the engine here.

### When you DO write an API   {#when-an-api}
There is a REST surface — `app.Apis` — and an MCP surface — `app.McpServer`. Both exist for exactly one purpose:
**publishing your app to someone else.** A third party's integration. An agent. A partner's webhook.

**Never for your own UI to talk to your own server.** And note what they are: you *expose an existing function*; you do
not write an endpoint. The function stays transport-agnostic — it does not know or care that it is reachable over HTTP.

Calling *someone else's* API is the other direction entirely, and has its own surface ([Http.*](https://osysharp.com/reference/http/facade/)).

### What is left to build?   {#the-work}
What is left, once the scaffolding is gone, is the part that was always the point:

- **The model** — the entities, and what is true about them ([entity](https://osysharp.com/reference/entity/declaration/), [invariant](https://osysharp.com/reference/entity/invariants/)).
- **The rules** — who may see and change what ([The security model](https://osysharp.com/reference/security/index/)). One block per entity, enforced in every query, on
  every path, forever.
- **The logic** — ordinary functions.
- **The screens** — components that name the data they need.

An agent given a business problem should start at the entities and the security rules, and will find that most of what
it expected to build does not exist. That is the intended experience.

### Two caveats: schema evolution, and the UI surface   {#caveats}
- **Schema evolution is not unconditional.** Additive change applies on `osy compile`. A change that would **remove or
  reshape existing data** (a drop, a rename, a type change) is *held* until you authorise it with a generated migration
  — which is the platform refusing to destroy data behind your back, not a gap.
- **The UI surface is still moving.** The `ui/` pages are marked `preview` for that reason. The execution model on this
  page — the hand-off, the security-in-the-query, the absence of an API — is settled; the exact spelling of a component
  member is the part still being sharpened.

## See also       {#see-also}
- [app.osy](https://osysharp.com/reference/project/manifest/) — the `app { }` block: the one file that says what your app is
- [project layout](https://osysharp.com/reference/project/layout/) — where the `.osy` files live
- [The security model](https://osysharp.com/reference/security/index/) — the rules that replace your auth middleware, and why they are in the query
- [Querying data](https://osysharp.com/reference/query/index/) — how you read data, and what it costs
- [function](https://osysharp.com/reference/function/declaration/) — a server function (and why it has no `Save()`)
- [async / await — why Osy# has neither](https://osysharp.com/reference/function/async-await/) — why there is no `async`, and where the one `await` lives
- [publishing a REST API (app.Apis)](https://osysharp.com/reference/api/rest/) — `app.Apis`: publishing your functions to someone else
