# Visitor

> `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.

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

## Summary        {#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.

```osy title="a basket keyed to the visit" test app=ui-visitor
[Page("/cart")]
[AllowAnonymous]
[Render(CSR)]
component CartPage() {
  string visitId = Visitor.Id;                 // who this browser is, signed in or not
  live var cart = Cart.Include(c => c.Lines).SingleOrDefault(c => c.VisitId == visitId);
  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 `VisitId` and a `Lines` collection, a `CartLine`,
and a `security` block on each — the id arrives from the browser, so the entity's own grants are the whole
protection. [The example below](#examples) 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](#security) below.

## Signature      {#signature}
```osy syntax
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    {#description}

### How stable is `Visitor.Id`? — same browser, same value   {#what}
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   {#client-side}
`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**:

```osy title="pass it to the server as an argument" test app=ui-visitor
[Page("/products/{slug}")] [AllowAnonymous] [Render(CSR)]
component ProductPage(string slug) {
  string visitId = Visitor.Id;
  var product = Product.SingleOrDefault(p => p.Name == slug);
  action Add(Product p) { AddToCart(visitId, 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   {#claiming}
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:

```osy title="handing a visit's rows a real owner" test app=ui-visitor
void ClaimCart(string visitId, User owner) {
  var cart = Cart.SingleOrDefault(c => c.VisitId == visitId && 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   {#security}
Rows keyed by `Visitor.Id` are exactly as protected as your entity says they are. Because the id 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]`](#see-also) — 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   {#no-storage}
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       {#examples}
An anonymous basket — the whole shape, from an empty visit to a claimed cart:

```osy title="visitor-cart" test app=ui-visitor
[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 {
  // ⚑ NAMED FOR WHAT IT IS. `Visitor.Id` is a NAME the browser supplies — it identifies a visit and authorizes
  //    nothing (see "A name, not a credential" above), so calling the column `Token` reads as a secret to every
  //    reader, the linter included.
  [Required, MaxLength(64)] string VisitId;    // 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 id as an ARGUMENT — it is input from the browser, never identity.
[AllowAnonymous]
void AddToCart(string visitId, Product product) {
  var cart = Cart.SingleOrDefault(c => c.VisitId == visitId);
  if (cart == null) { cart = new Cart { VisitId = visitId }; }
  var line = new CartLine { Cart = cart, Product = product };
  UnitOfWork.Commit();      // adding to a basket is a complete act — nothing else is going to commit it
}

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

  action Add(Product p) { AddToCart(visitId, 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       {#see-also}
- [routes and pages](https://osysharp.com/reference/ui/routing/) — protected-by-default routing, and the `[AllowAnonymous]` a visitor-facing page must declare
- [page authorization (policies)](https://osysharp.com/reference/ui/authorize/) — what a real authorization gate looks like, and why a visitor id is not one
- [component](https://osysharp.com/reference/ui/component/) — where an ambient is read, and how a component holds it in a field
