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

Reference / UI

Visitor

Visitor.Id — a stable opaque id for this browser, for work begun before there is a user

`Visitor.Id` is a stable opaque id for the browser someone is using, minted on their first visit and remembered afterwards. It gives work started before sign-up — a shop's basket, a saved filter — somewhere to belong, and a key to hand a real owner once there is one. It is a name, never a credential: it arrives from the browser, so it proves nothing and must never gate access to anything.

stable4 examples compiled by CIuivisitoranonymousidentity

Summary#

Plenty of what a person does happens before they are anybody. They browse a catalogue, fill a basket, set a filter — and only then, if at all, do they sign up. Session.CurrentUser is null for all of it, which is correct and no help: that work still has to live somewhere, and still has to be there after a refresh.

Visitor.Id is the missing name. It is a stable opaque id for this browser, minted the first time it is read and remembered across visits, so an app can key rows to the visit that created them and hand them a real owner later.

[Page("/cart")]
[AllowAnonymous]
[Render(CSR)]
component CartPage() {
  string token = Visitor.Id;                 // who this browser is, signed in or not
  live var cart = Cart.Include(c => c.Lines).SingleOrDefault(c => c.Token == token);
  render {
    if (cart != null) { foreach (var l in cart.Lines) { Text(l.Product.Name); } }
  }
}

What this needs around it: a Cart entity carrying the visit's Token and a Lines collection, a CartLine, and a security block on each — the token arrives from the browser, so the entity's own grants are the whole protection. The example below declares all of it as one compiled app.

A name, not a credential. Visitor.Id comes from the browser, so it is input, never evidence. It identifies a visit; it authorizes nothing. Rows keyed by it are protected by your entity's security block and by nothing else — see what to assume about the id below.

Signature#

Visitor.Id     // string — a stable opaque id for this browser. Never empty.

Available in every component, with no using. Reading it is what mints it.

Description#

How stable is Visitor.Id? — same browser, same value#

The same value on every page of the app and after a reload, in this browser. A different value in another browser, another profile, or a private window — and a new one if the visitor clears their site data, which is what "forget me" should mean.

It is not derived from anything about the person: not their address, not their device, not their behaviour. It is an opaque id, generated at random, which is exactly why it can be handed out freely — it says only "this is the same browser as before", which is the entire question an app needs answered.

It is a CLIENT value#

Visitor.Id lives in the browser, so an expression reading it runs client-side, like every other ambient. To use it on the server, pass it as an argument:

[Page("/products/{slug}")] [AllowAnonymous] [Render(CSR)]
component ProductPage(string slug) {
  string token = Visitor.Id;
  var product = Product.SingleOrDefault(p => p.Name == slug);
  action Add(Product p) { AddToCart(token, p); }     // the server takes it as input
  render {
    if (product != null) { Pressable(onClick: () => Add(product)) { Text("Add to cart"); } }
  }
}

This is not a limitation to route around — it is the honest shape. A client-supplied id is something the server should receive and treat as input, never something it should quietly trust as identity.

Moving a visitor's rows to their account on sign-in#

The point of a visit-keyed row is that it becomes an owned one. When the visitor signs in, the app moves the rows it cares about from the visit to the user — the handover it exists for:

void ClaimCart(string token, User owner) {
  var cart = Cart.SingleOrDefault(c => c.Token == token && c.Owner == null);
  if (cart != null) { cart.Owner = owner; }
}

Whether to claim, merge, or discard when the user already has rows of their own is the app's decision, and the platform has no opinion: it supplies the id and nothing else.

Can a visitor forge the id? — what to assume#

Rows keyed by Visitor.Id are exactly as protected as your entity says they are. Because the token arrives from the browser, a visit-keyed entity is one where the app should think about the security block rather than reach for the defaults — the same way it would for anything an anonymous caller can reach.

Two rules keep this simple:

  • Never gate anything that matters on a visitor id. It is not a login. It cannot stand in for one.
  • Never put anything in a visit-keyed row that would harm the visitor if another visitor read it. A basket of product references is the right size of thing; a saved address is not, until there is a user to own it.

Pages that a visitor reaches before signing in must also declare [AllowAnonymous] — routed components require an authenticated principal by default, which is what makes reaching for Visitor.Id a deliberate act rather than something an app drifts into.

And so must every FUNCTION those pages call. A page being public settles who may SEE it; who may CALL a function is a separate grant on the function, and the two are easy to conflate — this page's own example did, until 2026-08-25. Without it the server refuses the hand-off, the call does nothing, every statement after it in that body is unwound, and nothing appears on screen to say so: the visitor presses "Add to cart" and the basket stays empty. The compiler now refuses that shape and names both fixes.

When storage is unavailable#

Some browsers refuse site storage (a privacy mode, an embedded webview). There Visitor.Id still returns a stable id for as long as the page is open, so nothing breaks and nothing throws — it simply is not remembered after a reload. An app that wants to notice can: a basket that comes back empty is the visitor's answer.

Examples#

An anonymous basket — the whole shape, from an empty visit to a claimed cart:

[Principal] entity User { string Email; }

entity Product {
  [Required, MaxLength(80)] string Name;
  decimal Price;
  security { allow read when IsAnonymous || IsAuthenticated; }   // a catalogue is public — that is what a shop is
}

entity Cart {
  [Required, MaxLength(64)] string Token;      // the visit this basket belongs to
  User Owner;                                  // null until someone signs in and claims it
  [ForeignKey(Cart)] CartLine[] Lines;
  // ⚠ Reachable by anyone, signed in or not — see Security above. That is the honest cost of a row keyed by
  // something the browser supplies, and the reason a basket holds product references and nothing else.
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

entity CartLine {
  [Required] Cart Cart;
  [Required] Product Product;
  int Quantity = 1;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

// The server takes the token as an ARGUMENT — it is input from the browser, never identity.
[AllowAnonymous]
void AddToCart(string token, Product product) {
  var cart = Cart.SingleOrDefault(c => c.Token == token);
  if (cart == null) { cart = new Cart { Token = token }; }
  var line = new CartLine { Cart = cart, Product = product };
}

[Page("/")]
[AllowAnonymous]
[Render(CSR)]
component Catalog() {
  string token = Visitor.Id;
  live var products = Product.OrderBy(p => p.Name).ToList();

  action Add(Product p) { AddToCart(token, p); }

  render {
    Stack(gap: 3) {
      foreach (var p in products) {
        Row(gap: 2) {
          Text(p.Name);
          Button("Add to bag", onPress: () => Add(p));
        }
      }
    }
  }
}

See also#

  • routes and pages — protected-by-default routing, and the [AllowAnonymous] a visitor-facing page must declare
  • page authorization (policies) — what a real authorization gate looks like, and why a visitor id is not one
  • component — where an ambient is read, and how a component holds it in a field

Related

routes and pages

How a component becomes a page: it declares a route with `[Page("/catalog/{slug}")]`, and navigating to a matching path…

page authorization (policies)

A `policy` names a reusable authorization predicate over the current user — e.g. `policy Admins => UserRole.Any(r =>…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…