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

Samples

doc-vault

doc-vault — the ENTITY INHERITANCE demo. One table, four kinds of document, three levels deep.

7 source files2 test files

The doc-vault sample, running.
Running, signed in, compiled from the source below.

Get it

$ osy init doc-vault
$ osy launch

The app

app.osy7 lines
// doc-vault — the ENTITY INHERITANCE demo. One table, four kinds of document, three levels deep.
app DocVault {
  use Osyrin.Ui;
  model "model/**/*.osy";
  tests "tests/**/*.test.osy";
}
model/identity.osy54 lines
// The identity tier, self-contained — a [Principal] account, a [Role] enum, and a RoleGrant table recognised by its
// SHAPE. Declared here rather than shared so this demo compiles on its own, the way a downloader's copy would.

[Role] enum AppRole { Authenticator, Member, Legal, Finance }

[Principal] entity Account {
  [Required, MaxLength(200)] string Name;
  [Required, MaxLength(200), Unique] string Email;
  [MaxLength(200)] string PasswordHash;
  security {
    allow read when IsAuthenticated;
    allow read, create when IsAuthenticator;
    deny read PasswordHash when !IsAuthenticator;
  }
}

entity RoleGrant {
  [Required] Account Grantee;
  [Required] AppRole Level = AppRole.Member;
  security {
    allow read when IsAuthenticated;
    allow create when IsAuthenticator;
    allow create, update, delete when IsLegal;
  }
}

policy IsAuthenticator => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Authenticator);
policy IsLegal         => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Legal);
policy IsFinance       => RoleGrant.Any(g => g.Grantee == user && g.Level == AppRole.Finance);

[AuthMethod]
string Signup(string email, string password) {
  var a = new Account { Name = email, Email = email, PasswordHash = Security.HashPassword(password) };
  var grant = new RoleGrant { Grantee = a, Level = AppRole.Member };
  return Security.IssueJwt(a.Id, a.Email);
}

[AuthMethod]
string Login(string email, string password) {
  var a = Account.Where(x => x.Email == email).FirstOrDefault();
  if (a == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, a.PasswordHash)) { return Security.IssueJwt(a.Id, a.Email); }
  return "";
}

app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };

app.AuthBootstrap = new AuthBootstrap {
  Role      = AppRole.Authenticator,
  Login     = Login,
  Signup    = Signup,
  LoginPage = LoginPage,
};
model/seed.osy45 lines
// Seeding, split by AUTHORITY — and the split is not incidental, it is the app's subject showing up in its own
// setup. The first version of this file was one `SeedVault()`, and running it as Legal died on the Invoice with
// "Create of 'Invoice' denied … the caller does not satisfy the guard" — correctly, and it rolled the whole thing
// back. Three verbs, run as three people:

// Anyone signed in: Document lets any authenticated caller create, and so does Memo.
void SeedShared() {
  if (Document.Where(d => d.Reference == "DOC-1").ToList().Count == 0) {
    new Document { Reference = "DOC-1", Title = "Vault index", State = DocState.Filed };
  }
  if (Memo.Where(m => m.Reference == "MEMO-1").ToList().Count == 0) {
    var m = new Memo { Reference = "MEMO-1", Title = "Office closed on the 24th", State = DocState.Filed };
    new Note { Document = m, Body = "circulated to all staff" };
  }
  if (Memo.Where(m => m.Reference == "MEMO-2").ToList().Count == 0) {
    new Memo { Reference = "MEMO-2", Title = "Fire drill, Thursday 09:00", State = DocState.Draft };
  }
}

void SeedContracts() {
  if (Contract.Where(c => c.Reference == "CON-1").ToList().Count == 0) {
    var c = new Contract { Reference = "CON-1", Title = "Server supply agreement",
                           Counterparty = "Acme Hardware", Value = 48000m, State = DocState.InReview };
    new Note { Document = c, Body = "legal review in progress" };
  }
  if (SignedContract.Where(c => c.Reference == "CON-2").ToList().Count == 0) {
    var sc = new SignedContract { Reference = "CON-2", Title = "Support retainer 2026",
                                  Counterparty = "Zenith Services", Value = 12500m, State = DocState.Filed,
                                  Signatory = "Ada Lovelace", SignedAt = DateTime.UtcNow };
    new Note { Document = sc, Body = "countersigned copy filed" };
  }
}

void SeedInvoices() {
  if (Invoice.Where(i => i.Reference == "INV-1").ToList().Count == 0) {
    var i = new Invoice { Reference = "INV-1", Title = "March hosting",
                          Counterparty = "Zenith Services", Amount = 940m, State = DocState.Filed };
    new Note { Document = i, Body = "paid 2026-04-02" };
  }
  if (Invoice.Where(i => i.Reference == "INV-2").ToList().Count == 0) {
    new Invoice { Reference = "INV-2", Title = "April hosting",
                  Counterparty = "Zenith Services", Amount = 940m, State = DocState.InReview };
  }
}
model/theme.osy73 lines
// THE SHARED DEMO THEME — canonical copy. Every demo listed in `DemoSharedThemeTests` holds a
// byte-identical `model/theme.osy`, and that test is what keeps them identical.
//
// ⚑ EVERY COLOUR HERE SHADOWS A TOKEN THE KIT ALREADY DECLARES, and that is the whole point.
//   The themes this replaced declared a PARALLEL palette — `Surface0/1/2`, `TextPrimary`,
//   `FillAccent/Success/Warning/Danger` — names `Osyrin.Ui` has never heard of. So the kit's own
//   `Primary`, `Success`, `Warning` and `Danger` stayed at their defaults in six demos: the moment
//   one of them reached for a kit control, that control painted itself indigo next to the demo's
//   blue. `FillAccent` was declared in all six and referenced by NONE — an accent colour that
//   existed only in the theme file.
//
// So the rule for a demo theme is: SHADOW a kit token, never invent a second name for it. A name
// the kit does not have (`Radius.Card`) is a real extension and fine; a second spelling of one it
// does have (`TextPrimary` for `OnBg`) is how an app ends up with two of everything.
//
// Excluded by design — these five own their look and must NOT adopt this file: arcade, ember,
// motion, gestures (each demonstrates a visual world) and Apps/recall-osy.

theme Demo {

  Colors {
    // A deep petrol blue, deliberately not the kit's indigo (#4F46E5) — a demo should look like a
    // considered app rather than an unstyled one, and the two should be distinguishable on sight.
    // It is the one saturated colour on the page; everything else is warm neutral, which is what
    // keeps "distinct" from turning into "loud".
    Primary   = Modes.Of(light: Palette.From("#125E7A"), dark: Palette.From("#57B8D6"));
    OnPrimary = Modes.Of(light: "#FFFFFF", dark: "#06181F");

    Bg      = Modes.Of(light: "#FAF9F7", dark: "#101317");   // the page — a touch warm, so cards read as lifted
    Surface = Modes.Of(light: "#FFFFFF", dark: "#171C22");   // a card, a row, a lane
    Muted   = Modes.Of(light: "#EFEEEA", dark: "#1E242B");   // an inline notice, a disabled field
    Border  = Modes.Of(light: "#E4E2DC", dark: "#28303A");

    OnBg          = Modes.Of(light: "#14181D", dark: "#E7EBF0");
    OnSurface     = Modes.Of(light: "#14181D", dark: "#E7EBF0");
    TextSecondary = Modes.Of(light: "#59626D", dark: "#9BA6B3");   // labels, timestamps, captions
    TextMuted     = Modes.Of(light: "#7C848E", dark: "#7E8894");   // the quietest text on the page

    Success = Modes.Of(light: Palette.From("#1D7A4C"), dark: Palette.From("#4EC98A"));
    Warning = Modes.Of(light: Palette.From("#B26A00"), dark: Palette.From("#E0A343"));
    Danger  = Modes.Of(light: Palette.From("#C1352B"), dark: Palette.From("#F0736A"));
  }

  // Names the kit does not have, so these are extensions rather than second spellings.
  Radius { Control = "8px"; Card = "12px"; }

  // ⚑ THE FONTS ARE WHAT MAKE THIS READ AS A DESIGNED APP RATHER THAN A DEFAULT ONE, and `Sans`
  //   shadows the kit's own token — so every kit control picks it up with no page edit at all.
  //   Both faces are vendored per app under `model/fonts/` and pinned in `osyrin.lock` (`osy font
  //   add`), because a webfont a page merely NAMES is a webfont that silently falls back.
    //   Only the faces the app SHIPS are named here — Mono is left at the kit default because no
  //   demo renders code, and naming an unshipped family is a silent fallback.
  //   Geist for the UI: a neutral grotesque with real character at small sizes, which is where a
  //   dense LOB screen lives. Fraunces for display: a warm variable serif, used ONLY on a page
  //   title. The pairing is the whole look — one voice for reading, one for announcing.
  Font {
    Sans    = "Geist, ui-sans-serif, -apple-system, \"Segoe UI\", Roboto, system-ui, sans-serif";
    Serif   = "Fraunces, \"Iowan Old Style\", Palatino, Georgia, ui-serif, serif";
  }

  FontSize   { Caption = "12px"; Body = "14px"; Subhead = "17px"; Title = "26px"; }
  FontWeight { Regular = 400; Medium = 500; Semibold = 600; }

  // ── APP EXTENSIONS ─────────────────────────────────────────────────────────────────────────────
  // Everything ABOVE this line is the shared theme, held byte-identical across the demos by
  // `DemoSharedThemeTests`. An app may add tokens BELOW it — a name the kit does not have. It may
  // NOT add a second spelling of one the kit already has; that is the defect this file replaced.
  // ── APP EXTENSIONS ─────────────────────────────────────────────────────────────────────────────
  // Everything ABOVE this line is the shared theme, held byte-identical across the demos by
  // `DemoSharedThemeTests`. An app may add tokens BELOW it — a name the kit does not have. It may
  // NOT add a second spelling of one the kit already has; that is the defect this file replaced.
}
model/vault.osy53 lines
// THE VAULT — one table, four kinds of document, three levels deep.

enum DocState { Draft, InReview, Filed }

entity Document {
  [Required, Unique, MaxLength(40)] string Reference;
  [Required, MaxLength(200)] string Title;
  DocState State = DocState.Draft;

  [ForeignKey(Document)] Note[] Notes;

  invariant Title.Length > 2 message "a document needs a real title";

  semantic => $"{Reference} — {Title}";

  security { allow create, read, update when IsAuthenticated; }
}

entity Note {
  [Required] Document Document;
  [Required, MaxLength(400)] string Body;
  security { allow create, read when IsAuthenticated; }
}

entity Contract : Document {
  [Required, MaxLength(120)] string Counterparty;
  [Required] decimal Value;

  invariant Value > 0 message "a contract has a value";

  security { allow create, read, update when IsLegal; }
}

entity Invoice : Document {
  [Required, MaxLength(120)] string Counterparty;
  [Required] decimal Amount;

  security { allow create, read when IsFinance; }
}

entity Memo : Document {
  security { allow create, read when IsAuthenticated; }
}

entity SignedContract : Contract {
  [Required, MaxLength(120)] string Signatory;
  [Required] DateTime SignedAt;

  semantic => $"{Reference} — {Title}, signed by {Signatory}";

  security { allow create, read when IsLegal; }
}
model/pages/login.osy53 lines
// Sign in. So the first thing the demo needs is a way to be somebody else.
using Osyrin.Ui;

[Page("/login")]
[AllowAnonymous]
[Render(CSR)]
[Title("Document vault — sign in")]
component LoginPage() {
  string email = "ada@vault.test";
  string password = "demo1234";
  string problem = "";

  action SignIn() {
    var ticket = Login(email, password);
    if (ticket == "") { problem = "That email and password don't match anyone."; }
    else { Session.SignIn(ticket); }
  }

  render {
    Row(align: Align.Center, justify: Justify.Center, minH: "100vh", p: 4, bg: Colors.Bg, color: Colors.OnBg) {
      Stack(gap: 4, w: "100%", maxW: "440px") {
        Stack(gap: 1) {
          Text("Document vault", fontSize: FontSize.Title, fontFamily: Font.Serif, fontWeight: FontWeight.Semibold, letterSpacing: "-0.02em");
          Text("Four kinds of document, one table, three levels deep.", fontSize: FontSize.Body, color: Colors.TextSecondary);
        }

        Box(bg: Colors.Surface, rounded: Radius.Card, p: 5, borderW: 1, border: Colors.Border) {
          Stack(gap: 3) {
            Input(value: email, label: "Email", placeholder: "you@vault.test");
            Input(value: password, label: "Password", type: "password", placeholder: "password");
            if (problem != "") {
              Box(bg: Colors.Muted, rounded: Radius.Control, px: 3, py: 2) {
                Text(problem, fontSize: FontSize.Caption, color: Colors.Danger);
              }
            }
            Button("Sign in", onPress: SignIn, tone: Tone.Primary);
          }
        }

        Box(bg: Colors.Surface, rounded: Radius.Card, p: 4, borderW: 1, border: Colors.Border) {
          Stack(gap: 2) {
            Text("Sign in as each of these and compare — password demo1234",
                 fontSize: FontSize.Caption, fontWeight: FontWeight.Medium, color: Colors.TextSecondary);
            Text("ada@vault.test — Legal: sees contracts, signed and unsigned", fontSize: FontSize.Caption, color: Colors.TextSecondary);
            Text("finn@vault.test — Finance: sees invoices, and no contract at all", fontSize: FontSize.Caption, color: Colors.TextSecondary);
            Text("mo@vault.test — neither: sees only memos", fontSize: FontSize.Caption, color: Colors.TextSecondary);
          }
        }
      }
    }
  }
}
model/pages/vault.osy209 lines
// THE VAULT PAGE — the hierarchy, rendered.
using Osyrin.Ui;

[Page("/")]
[Render(CSR)]
[Title("Document vault")]
component VaultPage() {
  live var plain     = Document.Include(d => d.Notes).ToList();
  live var contracts = Contract.Include(c => c.Notes).ToList();
  live var signed    = SignedContract.Include(c => c.Notes).ToList();
  live var invoices  = Invoice.Include(i => i.Notes).ToList();
  live var memos     = Memo.Include(m => m.Notes).ToList();

  action SignOut() { Session.SignOut(); }

  int contractsSeen = -1;
  string signedBy = "";
  action CountByType() {
    contractsSeen = 0;
    signedBy = "";
    foreach (var d in plain) {
      if (d is Contract) { contractsSeen = contractsSeen + 1; }
      if (d is SignedContract sc) { signedBy = signedBy + sc.Signatory; }
    }
  }

  action AddShared()    { SeedShared(); }
  action AddContracts() { SeedContracts(); }
  action AddInvoices()  { SeedInvoices(); }

  render {
    Stack(gap: 5, p: 6, bg: Colors.Bg, color: Colors.OnBg, minH: "100vh") {

      Stack(gap: 1) {
        Text("Document vault", fontSize: FontSize.Title, fontFamily: Font.Serif, fontWeight: FontWeight.Semibold, letterSpacing: "-0.02em");
        Text("Four kinds of document. One table. Three levels deep. Every section below is a separate read of a "
             + "separate TYPE — and which of them has anything in it is decided by that type's own security block, "
             + "not by this page.", fontSize: FontSize.Body, color: Colors.TextSecondary, maxW: "70ch");
        Row(gap: 3, align: Align.Center) {
          Text("Signed in as " + Session.CurrentUser.Name, fontSize: FontSize.Caption, color: Colors.TextSecondary);
          Button("Sign out", onPress: SignOut);
        }

        Row(gap: 2, align: Align.Center) {
          Button("Add memos", onPress: AddShared);
          Button("Add contracts (Legal)", onPress: AddContracts);
          Button("Add invoices (Finance)", onPress: AddInvoices);
          Button("Count by type", onPress: CountByType);
        }

        if (contractsSeen >= 0) {
          Text("By type test, in an action: " + contractsSeen + " contract(s), signed by [" + signedBy + "]",
               fontSize: FontSize.Caption, color: Colors.TextSecondary);
        }

        if (Failure.Any) {
          Box(bg: Colors.Muted, rounded: Radius.Control, p: 3, borderW: 1, border: Colors.Danger) {
            Row(justify: Justify.SpaceBetween, align: Align.Start, gap: 3) {
              Stack(gap: 1) {
                Text("That was refused", fontSize: FontSize.Caption, fontWeight: FontWeight.Semibold, color: Colors.Danger);
                Text(Failure.Message, fontSize: FontSize.Caption, color: Colors.TextSecondary, maxW: "90ch");
              }
              Button("Dismiss", onPress: Failure.Dismiss);
            }
          }
        }
      }

      Box(bg: Colors.Surface, rounded: Radius.Card, p: 4, borderW: 1, border: Colors.Border, label: "Every document") {
        Stack(gap: 3) {
          SectionHead(heading: "Document — the root, read polymorphically",
                      count: plain.Count + " of every kind",
                      note: "A base read returns every kind BELOW it, exactly as C# and EF mean it — the contracts, "
                            + "the invoices and the memos are IN this count, not merely in the same table. And each "
                            + "row that comes back is governed by its OWN rules, which is why this one number is "
                            + "different for Ada, for Finn and for an ordinary member.");
          Stack(gap: 2) {
            foreach (var d in plain) {
              Card(d.Reference, d.Title, d.State.Label, d.Notes.Count + " note(s)");
            }
          }
        }
      }

      Box(bg: Colors.Surface, rounded: Radius.Card, p: 4, borderW: 1, border: Colors.Border, label: "Contracts") {
        Stack(gap: 3) {
          SectionHead(heading: "Contract : Document — Legal only",
                      count: contracts.Count + " row(s)",
                      note: "Its own two fields on top of everything Document declares. Empty unless you are signed in as Legal, "
                            + "and nothing on this page checks that.");
          Stack(gap: 2) {
            foreach (var c in contracts) {
              Card(c.Reference, c.Title, c.Counterparty + " · " + c.Value, c.Notes.Count + " note(s)");
            }
            if (contracts.Count == 0) { NothingHere("No contracts you may read. Sign in as Legal to see these."); }
          }
        }
      }

      Box(bg: Colors.Surface, rounded: Radius.Card, p: 4, borderW: 1, border: Colors.Border, label: "Signed contracts") {
        Stack(gap: 3) {
          SectionHead(heading: "SignedContract : Contract : Document — three levels",
                      count: signed.Count + " row(s)",
                      note: "Reference and Title come from Document, Counterparty and Value from Contract, Signatory from itself. "
                            + "The page reads all five off one object.");
          Stack(gap: 2) {
            foreach (var sc in signed) {
              Card(sc.Reference, sc.Title,
                   sc.Counterparty + " · " + sc.Value + " · signed by " + sc.Signatory,
                   sc.Notes.Count + " note(s)");
            }
            if (signed.Count == 0) { NothingHere("No signed contracts you may read. Sign in as Legal to see these."); }
          }
        }
      }

      Box(bg: Colors.Surface, rounded: Radius.Card, p: 4, borderW: 1, border: Colors.Border) {
        Stack(gap: 3) {
          SectionHead(heading: "Invoice : Document — Finance only",
                      count: invoices.Count + " row(s)",
                      note: "A sibling of Contract. Its Counterparty is literally the same physical column as Contract's — one "
                            + "column, two types, two values, because they are different rows.");
          Stack(gap: 2) {
            foreach (var i in invoices) {
              Card(i.Reference, i.Title, i.Counterparty + " · " + i.Amount, i.Notes.Count + " note(s)");
            }
            if (invoices.Count == 0) { NothingHere("No invoices you may read. Sign in as Finance to see these."); }
          }
        }
      }

      Box(bg: Colors.Surface, rounded: Radius.Card, p: 4, borderW: 1, border: Colors.Border) {
        Stack(gap: 3) {
          SectionHead(heading: "Memo : Document — adds nothing",
                      count: memos.Count + " row(s)",
                      note: "A distinct TYPE with no members of its own — still its own reads and its own rules.");
          Stack(gap: 2) {
            foreach (var m in memos) {
              Card(m.Reference, m.Title, m.State.Label, m.Notes.Count + " note(s)");
            }
          }
        }
      }

      Box(bg: Colors.Surface, rounded: Radius.Card, p: 4, borderW: 1, border: Colors.Border) {
        Stack(gap: 3) {
          SectionHead(heading: "One list, every kind — the row decides how it draws",
                      count: plain.Count + " walked",
                      note: "The SAME rows as the first section, in one loop with no type in the query. What each "
                            + "line shows is decided by what the row turned out to be — and a kind you may not read "
                            + "is not here to be asked about, because security already answered.");
          Stack(gap: 2) {
            foreach (var d in plain) {
              if (d is SignedContract sc) {
                Card(d.Reference, d.Title, "SignedContract · signed by " + sc.Signatory, d.Notes.Count + " note(s)");
              } else if (d is Contract con) {
                Card(d.Reference, d.Title, "Contract · " + con.Counterparty, d.Notes.Count + " note(s)");
              } else if (d is Invoice inv) {
                Card(d.Reference, d.Title, "Invoice · " + inv.Counterparty, d.Notes.Count + " note(s)");
              } else if (d is Memo) {
                Card(d.Reference, d.Title, "Memo · adds nothing of its own", d.Notes.Count + " note(s)");
              } else {
                Card(d.Reference, d.Title, "Document · the root itself", d.Notes.Count + " note(s)");
              }
            }
          }
        }
      }
    }
  }
}

[Composable]
component NothingHere(string what) {
  render {
    Box(bg: Colors.Muted, rounded: Radius.Control, px: 3, py: 2) {
      Text(what, fontSize: FontSize.Caption, color: Colors.TextSecondary);
    }
  }
}

[Composable]
component SectionHead(string heading, string count, string note) {
  render {
    Stack(gap: 1) {
      Row(justify: Justify.SpaceBetween, align: Align.Center, gap: 3) {
        Text(heading, fontSize: FontSize.Subhead, fontWeight: FontWeight.Semibold);
        Text(count, fontSize: FontSize.Caption, color: Colors.TextSecondary);
      }
      Text(note, fontSize: FontSize.Caption, color: Colors.TextSecondary, maxW: "80ch");
    }
  }
}

[Composable]
component Card(string reference, string title, string detail, string notes) {
  render {
    Box(bg: Colors.Muted, rounded: Radius.Control, px: 3, py: 2) {
      Row(justify: Justify.SpaceBetween, align: Align.Center, gap: 4) {
        Stack(gap: 1) {
          Text(reference + " — " + title, fontSize: FontSize.Body, fontWeight: FontWeight.Medium);
          Text(detail, fontSize: FontSize.Caption, color: Colors.TextSecondary);
        }
        Text(notes, fontSize: FontSize.Caption, color: Colors.TextSecondary);
      }
    }
  }
}