# topic

> A topic is a named, addressable realtime destination: a declared place to put a message so that every open page entitled to it receives the value, without re-reading the database. Its parameters are its address, so RoomFeed.For(a) and RoomFeed.For(b) are two topics. Candidates says who may subscribe and is required, and a signed-out visitor is refused unless the topic declares [AllowAnonymous]. Publishing takes no commit and no unit of work.

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

## Summary        {#summary}

A **topic** is a declared destination for messages. Publishing to one delivers the value to every open page that is
entitled to it — no polling, no re-query, and no row has to exist to carry the message.

```osy title="a room feed" test app=realtime-room
[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Room {
  [Required, MaxLength(120)] string Name;
  security { allow read, create when IsAuthenticated; }
}

entity Member {
  [Required] Room Room;
  [Required] User Person;
  security {
    allow read where Member.Any(m => m.Room == Room && m.Person == user);
    allow create where Person == user;
  }
}

class ChatLine { public string Body; }

topic RoomFeed(Guid roomId) {
  Candidates = u => Member.Any(m => m.Room.Id == roomId && m.Person == u);
  Carries    = ChatLine;
}

void Say(Guid roomId, string body) {
  RoomFeed.For(roomId).Publish(new ChatLine { Body = body });
}
```

## Signature      {#signature}

```osy syntax
[AllowAnonymous]                   // omitted ⇒ a caller with no principal is REFUSED before the rule is read
topic Name(<address params>) {
  Candidates = u => <predicate>;   // REQUIRED — who may subscribe
  Publishers = u => <predicate>;   // omitted ⇒ the same as Candidates
  Carries    = <type>;             // omitted ⇒ a signal topic, carrying no value
  Delivery   = Ephemeral;          // the only value today. Omitted ⇒ Ephemeral
  Presence   = true;               // omitted ⇒ false
}

Name.For(<address>).Publish(<payload>);   // from a server function, an action, or an api endpoint
```

This fence is a TEMPLATE — `<address params>` is a placeholder, not something that compiles — so it is illustrative
rather than compiled, and every claim it makes is backed by a compiled example further down: the declaration form by
[the room feed above](#summary), and each of the three publishing positions by [`Publish`](#publish).


## Description    {#description}

### The parameters are the address, not the name   {#address}

`RoomFeed.For(roomA)` and `RoomFeed.For(roomB)` are two different topics. This is the reason topics are declared rather than
named by string: an address made of typed parameters is one the compiler checks, and one a security rule can read.

It also means **a topic's name is never its security boundary**. Knowing the name `RoomFeed` gets you nothing; what
decides whether you receive a room's traffic is `Candidates`, evaluated for you, at that address.

Two spellings of one address are one address — an upper-case and a lower-case spelling of the same `Guid` reach the
same subscribers, because the address is compared by its bound values rather than by the text that arrived.

### `Candidates` — who may subscribe, and it is required   {#candidates}

`Candidates` is a predicate over the principal, evaluated at subscribe time with the topic's address in scope. It is
free to read app data, which is what lets a *user* of the app configure who may join — a membership table, a role
grant — without the developer recompiling.

It is deliberately the same spelling a workflow slot's `Candidates` uses, because it is the same question.

**A topic with no `Candidates` is a compile error.** Entity security takes the opposite posture — say nothing and
nobody may — and that is right there, because forgetting a rule fails loudly: nobody can read the table and it is
reported within the minute. A topic nobody may join fails the other way. The page simply never updates. There is no
error, no refusal, nothing in a log, and it looks exactly like a quiet room. It is the least reportable failure in
the system, so it is the one that must not be silent.

A topic open to every signed-in user says so out loud:

```osy title="every signed-in user may listen" test app=realtime-public
[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

class Tick { public int Count; }

topic Heartbeat() {
  Candidates = _ => true;      // loud, greppable, and a decision somebody made
  Carries    = Tick;
}
```

### A signed-out visitor is refused unless you say otherwise   {#anonymous}

`Candidates = _ => true` means *every signed-in user*. It does **not** mean "and anybody who is not signed in": a
caller with no principal is refused before the rule is read at all.

That is deliberate, and it is the one place a topic's security is decided by something other than `Candidates`.
A rule that never mentions its principal is not a statement about anonymity — `_ => Room.Any(r => r == room)` reads
as a restriction and restricts nothing about *who*, and inferring "public" from it would let an app's security
posture change because of what a predicate does not say.

A signed-out visitor is a real audience — a public status page, a queue display on a wall, a live scoreboard, a
landing page that updates — and when they are, the topic declares it:

```osy title="a public status page — anyone may watch, only staff may speak" test app=realtime-anonymous
[Principal] entity User {
  [MaxLength(200)] string Email;
  bool IsStaff;
  security { allow read when IsAuthenticated; }
}

class Status { public string State; }

[AllowAnonymous]                 // ← a caller with NO principal may join. Without this they are refused.
topic ServiceStatus() {
  Candidates = _ => true;        // …and this still decides who joins, signed in or not
  Publishers = u => u.IsStaff;
  Carries    = Status;
}
```

**It opens the door and decides nothing else** — exactly the contract `[AllowAnonymous]` has on a component or a
function. `Candidates` is still evaluated, with the principal parameter bound to `null`, so a rule that reads its
principal still refuses a caller who has none (`u => u.IsStaff` answers false for nobody, or faults, and a fault
denies). Writing `[AllowAnonymous]` over a rule that asks about a user is therefore harmless and pointless: say
what you mean in the rule.

⚠ **`Publishers` obeys it too.** The declaration is about the topic, not about one verb, so an anonymous caller may
publish exactly when `Publishers` (or `Candidates`, when `Publishers` is omitted) admits them. A page that lets a
signed-out visitor *post* needs the attribute on the topic as well as `[AllowAnonymous]` on the page and on the
function — see [public pages (what a signed-out visitor can see and do)](https://osysharp.com/reference/security/public-reads/), which lays the four declarations out side by side.

A signed-out page opens its live socket **when it first subscribes** — a public page that holds no live data and joins
no topic never holds one. What that socket may hear is bounded: a topic by its `[AllowAnonymous]` and its rule, and a
table's change signals only when some row of it is readable by a caller who is not signed in.

### Which visitor is joining?   {#visitor}
Inside a rule `Visitor.Id` is the joining BROWSER — the visitor its cookie proves, exactly as in an entity rule
([guest rows (a visit's own rows)](https://osysharp.com/reference/security/guest-rows/)). So a topic can admit the visit that owns a row:

```osy title="a support chat's feed, for the browser that started it" syntax
[AllowAnonymous]
topic ChatFeed(Guid chatId) {
  Candidates = u => Chat.Any(c => c.Id == chatId && c.VisitId == Visitor.Id);
  Carries    = Line;
}
```

### Can a rule name a policy?   {#policies}
Yes — and it means the policy **about the one joining**: `Candidates = u => IsStaff || IsCustomerIn(chatId)` asks
whether `u` is staff, or is the customer of that conversation. The policy is inlined with its `user` bound to the
rule's principal, so it is the same rule an entity's `security { }` would apply.

⚑ **Nobody matches nothing.** A policy written `Account == user` admits nobody who is not signed in — even to a row whose
`Account` is empty — and one written `VisitId == Visitor.Id` admits no caller who has no visit. Inside a topic rule the
principal is an ordinary value that may be `null`, so every comparison a policy makes with `user` or `Visitor.Id` is
guarded before it is asked.

### Can a package declare a topic?   {#in-a-package}
Yes. Its address and payload are the package's (written in its own namespace); its rules are resolved in the consuming
app, where the app's principal — and any policy the app supplies ([[security-naming-a-policy#supplied]]) — exist. An app
publishes to and listens on a package's topic by its name, as it calls the package's functions. `Osysharp.Conversations`'
`ConversationTyping` and Collaboration's `RecordPresence` are such topics ([The Conversations kit — one timeline per conversation, across every channel](https://osysharp.com/reference/ui/conversations-kit/)).

### `Publishers` — who may publish, defaulting to `Candidates`   {#publishers}

Omitting `Publishers` means *the same people who may listen may speak*, which is right for nearly every topic. The
two cases that differ are both real and both one line: a broadcast topic (many subscribe, a server function
publishes) and a firehose (many publish, few subscribe).

```osy title="a broadcast topic — every signed-in user may listen, only staff may speak" test app=realtime-broadcast
[Principal] entity User {
  [MaxLength(200)] string Email;
  bool IsStaff;
  security { allow read when IsAuthenticated; }
}

class Notice { public string Text; }

topic Announcements() {
  Candidates = _ => true;
  Publishers = u => u.IsStaff;
  Carries    = Notice;
}
```

**The publisher is stamped by the platform.** Who sent a message is taken from the acting principal, never from the
payload — so a message claiming to be from somebody else is not expressible rather than merely refused. You do not
have to remember to set it, and you cannot get it wrong.

⚠ Write the rule in the declaration, not in the function that publishes. A publish rule written in a function body is
enforced only on the path that remembers to call it, and it is reliably weaker than the one you would have declared:
the natural hand-written check asks whether the caller is *in the room* and forgets to ask whether the message names
them as its author.

### A gate reads COMMITTED data — so commit before you publish   {#gates-read-committed-data}

`Candidates` and `Publishers` are evaluated against the database **as it is committed**, not as your unit of work
currently has it. That is deliberate and it matches every other gate in the platform: authorization is decided
against the data that exists, never against a caller's pending, uncommitted view of it.

It has one consequence worth knowing before you meet it. If a single action *creates the row that authorizes the
publish* and then publishes, the publish is refused — because at the moment the gate runs, that row is still pending:

```osy title="✗ publishing on a membership that is still pending" syntax
void JoinAndGreet(Channel channel, string body) {
  new Membership { Channel = channel, Person = Session.CurrentUser };   // not committed yet…
  ChannelFeed.For(channel.Id).Publish(new PostLine { Body = body });    // …so `Candidates` cannot see it → refused
}
```

Commit first, and it behaves as you would expect:

```osy title="commit the authorizing row first, then publish" syntax
void JoinAndGreet(Channel channel, string body) {
  new Membership { Channel = channel, Person = Session.CurrentUser };
  UnitOfWork.Commit();                                                  // the membership is now a fact
  ChannelFeed.For(channel.Id).Publish(new PostLine { Body = body });    // `Candidates` sees it
}
```

The refusal names your own rule, which reads like a bug in the rule rather than a question of ordering — so when a
publish is refused by a rule you believe should pass, check what the current unit of work is still holding. It will
tell you: the refusal lists the entities your unit of work has pending, so you can see the row that was going to
authorize you sitting there uncommitted.

#### A page action is one unit of work, and so is a `[Test]` body   {#one-unit-of-work}

This catches people because a **page action does not commit either.** Calling a server function from a page
carries your uncommitted edits along with the call — the server runs your query against them, so you read your own
changes — but nothing is written until an explicit `UnitOfWork.Commit()`. So the refusal above is not something that
only happens in an unusual place: it is what a page doing both halves in one action gets.

A `[Test]` body behaves **identically**, and deliberately so — it is modelling that page action. If your test creates
the authorizing row and then subscribes or publishes, commit in between, exactly as the app itself would have to:

```osy title="the same commit, in a test body" syntax
var channel = CreateChannel("design", "");   // stages a Channel and a Membership
UnitOfWork.Commit();                         // …now they are facts the gate can see
ChannelFeed.For(channel.Id).Listen();        // admitted
```

### `Publish` — no commit, no unit of work   {#publish}

Publishing is not a data write. It opens no transaction and needs no row, which is what lets an inbound webhook or
an agent mid-answer reach a page directly.

#### Where you may publish from   {#publish-positions}

From a server function, from an `api` endpoint, and **from a page `action`** — the three positions the signature
names, each with a compiled example here.

A publish written straight into an action is the same publish. You do not have to route it through a server
function to be allowed to write it:

```osy title="publishing from a page action" test app=realtime-action-publish
[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

class Line { public string Body; }

// ⭐ THE PAGE IS `[AllowAnonymous]`, SO THE TOPIC MUST BE TOO. Whoever opens this route has no principal, and the
//    publish below is performed as that caller — without the attribute the topic refuses them and the button does
//    nothing. The two marks are one decision written in the two places it applies (see [public pages (what a signed-out visitor can see and do)](https://osysharp.com/reference/security/public-reads/)).
[AllowAnonymous]
topic Lobby(string room) {
  Candidates = _ => true;
  Carries    = Line;
}

[Route("/lobby")]
[Render(CSR)]
[AllowAnonymous]
component LobbyPage() {
  string draft = "";

  action Send() {
    Lobby.For("main").Publish(new Line { Body = draft });
    draft = "";
  }

  render {
    Stack(gap: 2) {
      Input(value: draft, label: "Message");
      Osysharp.Button("Send", onClick: Send);
    }
  }
}
```

**It still runs on the server, and that is not an implementation detail you can lose.** The browser evaluates the
address and the payload, then hands the publish to the host, which performs it under its own authority: `Publishers`
is evaluated there, and the sender is stamped there from the connection. So the rules in this page hold identically
whether you publish from an action or from a server function — including the committed-data rule below, because
the action's own uncommitted edits are not facts yet when the gate reads them.

That also means a publish is the one thing in an action that **cannot** be made to lie about who sent it. There is no
argument for the sender anywhere in this surface, and the browser is given nothing to say about identity.

```osy title="publishing from an api endpoint" test app=realtime-webhook
[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

class Delivery { public string Status; }

topic OrderTracking(Guid orderId) {
  Candidates = _ => true;
  Carries    = Delivery;
}

void CarrierUpdate(Guid orderId, string status) {
  OrderTracking.For(orderId).Publish(new Delivery { Status = status });
}
```

⚠ **An inbound webhook usually has no principal**, so a topic it publishes to needs `[AllowAnonymous]` for the same
reason the lobby above does — the carrier is not signed in as anybody. The example is written as an ordinary server
function, called by somebody who is; open the same call to an anonymous caller and the topic must say so too.

A refused publish **throws**, like any other refusal of a declared rule. It does not return quietly: a message
nobody received is the hardest failure in this system to notice, so it is not one of the silent ones.

### `Carries` — the payload type, or a signal   {#carries}

`Carries` names what each message holds — an app `class`. A topic that declares none is a **signal**: `Publish()`
takes no argument, and subscribers learn only that something happened.

**A published message may not carry a ROW**, and the refusal says so. A publish is composed once and goes to every
subscriber the join gate admitted, so a row inside it would be one person's view of that row handed to all of them —
and what a reader may see of a row is that reader's own question. Carry the values the readers need instead: a name,
an id, the two fields the line renders. (A **presence** entry is the exception that proves it: it carries the
principal's row, and the platform projects it separately for each recipient, through each one's own read rules.)

Prefer carrying the value. A signal can only be answered by re-asking the whole question, and that is what a topic
exists to avoid: a room of ten people a hundred messages deep costs about a thousand rows to deliver one "ok" when
the message carries nothing, and ten when it does.

### `Delivery` — `Ephemeral`, and only that   {#delivery}

`Ephemeral` (the default) is not persisted, and deliberately does **not** reach someone who was offline — a typing
indicator has no meaning to somebody who was not there.

`Delivery = Durable` is **refused at compile time**: it is not delivered yet, and accepting it would mean a subscriber
who was away silently received nothing.

**Write the durable half as rows and the instant half as the topic.** They are different in kind, and an app that
needs both wants both anyway:

- the **row** is what survives a reload and answers "what was said before I arrived";
- the **publish** carries the same values, so an open page renders them without re-reading anything.

That is two lines in the function that does the work — one `new`, one `Publish` — and it is what every app here does.

### `Presence` — the set of who is here   {#presence}

`Presence = true` makes the topic's membership **derived from who is connected to it**. The platform adds an entry
when a page subscribes and removes it when that page goes away, so a crashed browser cannot stay online for ever —
and no app code maintains it.

```osy title="who is in the room" test app=realtime-presence
[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Room {
  [Required, MaxLength(120)] string Name;
  security { allow read, create when IsAuthenticated; }
}

entity Member {
  [Required] Room Room;
  [Required] User Person;
  security {
    allow read where Member.Any(m => m.Room == Room && m.Person == user);
    allow create where Person == user;
  }
}

topic RoomPresence(Guid roomId) {
  Candidates = u => Member.Any(m => m.Room.Id == roomId && m.Person == u);
  Presence   = true;
}
```

An app can never write somebody else's entry — only decorate its own. **Invisible mode is therefore free and needs
no setting: a page that does not subscribe is not in the set.**

Presence is gated by the same `Candidates` rule as any other join. That matters more than it looks: a presence set
names people, so admitting a stranger to "just the presence" of a private room would disclose its membership without
ever showing them a message.

One person is one entry however many pages they have open — a phone and a desktop are one person in the room.

### Subscribing from a page   {#subscribing}

A page receives with `Listen()` — see [Listen](https://osysharp.com/reference/realtime/listen/). On a `Presence = true` topic it also reads the SET of who
is connected, and announces its own state: see [Here, Announce](https://osysharp.com/reference/realtime/presence/).

## Examples       {#examples}

The compiled examples above are the reference set: a room feed with a membership rule, an explicitly public topic, a
broadcast topic with a separate `Publishers` rule, a page action publishing directly, a webhook publishing with no
row to carry it, and a presence topic.

## See also       {#see-also}
- [security { }](https://osysharp.com/reference/security/entity-security/) — the `allow` rules a topic's `Candidates` predicate reads
- [Candidates (slot)](https://osysharp.com/reference/workflow/candidates/) — the `Candidates` spelling a topic deliberately shares
- [Listen](https://osysharp.com/reference/realtime/listen/) — the subscribe half of the surface
- [Here, Announce](https://osysharp.com/reference/realtime/presence/) — who is here, and what they are doing
- [component](https://osysharp.com/reference/ui/component/) — where a subscription is consumed, as a `live var`
