> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xano.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Realtime Triggers

> Five lifecycle events across two scopes — run logic when a client connects, joins, leaves, disconnects, or receives a message.

<Info>
  This page covers the current version of Realtime. For the original channel triggers — a single trigger per channel handling `join` and `message` — see [Realtime Triggers (Legacy)](/building/logic/triggers/realtime).
</Info>

<Note>
  Realtime triggers require a paid instance.
</Note>

Triggers are function stacks that run at the boundaries of a client's lifecycle. They come in two declarations, one per scope, and each selects which events it handles through an `actions` map.

## Server scope

`realtime_server_trigger` runs for the connection itself, regardless of which channels the client later joins.

```java XanoScript lines icon="code" theme={null}
actions = {connect: true, disconnect: false}
```

### Connect

Runs when a client attempts to open a connection to the realtime server. **A connect trigger is gating — it can veto the entire connection.**

This is the right place for checks that apply to the whole server: is this account active, is this token still valid, is this client plan-eligible.

### Disconnect

Runs when a client's connection ends. Observational — it can't reject anything. Use it for cleanup: releasing a claim, marking a user offline, or writing a session record.

## Channel scope

`channel_trigger` runs for a specific channel.

```java XanoScript lines icon="code" theme={null}
actions = {join: true, leave: false, deliver: false}
```

### Join

Runs when a client attempts to join the channel, **before** the join takes effect. **A join trigger is gating — it can veto the join.**

This is where you authorize against your own data: take the room from the channel path, check it against a membership table, and reject anyone who isn't a member.

### Leave

Runs when a client leaves the channel. Observational. The counterpart to join: update presence records, release a lock, or log the session.

### Deliver

Runs **once per recipient**, as a message is delivered. A deliver trigger can **rewrite that copy or drop it entirely**.

This is what turns one publish into N personalized deliveries. From a single message you can:

* **Redact** — strip fields a particular recipient isn't cleared to see
* **Personalize** — localize, or add recipient-specific context
* **Drop** — suppress delivery to a recipient who has muted or blocked the sender

Each recipient's trigger run is independent, so one recipient being dropped has no effect on the others.

<Warning>
  A deliver trigger runs for every recipient of every delivery, so its cost scales with fan-out. Keep it lean — prefer data you already have on hand over a database query per recipient on a channel with many listeners.
</Warning>

## Summary

| Event        | Declaration               | Runs when                                         | Gating?                     |
| ------------ | ------------------------- | ------------------------------------------------- | --------------------------- |
| `connect`    | `realtime_server_trigger` | A client opens a connection                       | Vetoes the connection       |
| `disconnect` | `realtime_server_trigger` | A client's connection ends                        | No                          |
| `join`       | `channel_trigger`         | A client attempts to join, before it takes effect | Vetoes the join             |
| `leave`      | `channel_trigger`         | A client leaves the channel                       | No                          |
| `deliver`    | `channel_trigger`         | Each recipient receives a message                 | Rewrites or drops that copy |

## Declaration clauses

| Declaration               | Required                                                   | Optional                                              |
| ------------------------- | ---------------------------------------------------------- | ----------------------------------------------------- |
| `realtime_server_trigger` | `realtime_server`, `input`, `stack`, `response`            | `actions`, `active`, `description`, `history`, `tags` |
| `channel_trigger`         | `realtime_server`, `channel`, `input`, `stack`, `response` | `actions`, `active`, `description`, `history`, `tags` |

As with every trigger type in Xano, the `input` block is **predefined by the system** — it's provided automatically and isn't yours to modify. Open a trigger in the builder to see the fields available for the events it handles.

## Complete join authorization example

This example assumes the `chat` server from [Servers & Channels](/realtime/realtime-servers-and-channels) exists. Create each declaration in its own file.

First, create a channel whose join decision will be made by the trigger:

```java XanoScript lines icon="code" theme={null}
channel members {
  realtime_server = "chat"
  access = {anonymous: true, presence: false}
  publish = {who: "authenticated", direct: false}
  input {
  }
}
```

Then create its join trigger. This uses the system input schema returned by a workspace pull:

```java XanoScript lines icon="code" theme={null}
channel_trigger member_gate {
  realtime_server = "chat"
  channel = "members"

  input {
    enum action {
      values = ["join", "leave", "deliver"]
    }

    text channel
    json payload
    object client {
      schema {
        json extras
        object permissions {
          schema {
            int dbo_id
            text row_id
          }
        }
      }
    }
  }

  stack {
    realtime.get_session as $session
  }

  response = {
    allowed: $session.authenticated
    reason : "Sign in to join members"
  }

  actions = {join: true}
}
```

An anonymous client is refused with `Sign in to join members`; a client connected with a valid application user JWT is admitted. Pass the JWT as the [WebSocket subprotocol](/realtime/connecting-a-client#authentication). Return a real boolean in `allowed`.

This example checks authentication only. For room membership or tenant isolation, replace the decision with authorization against your own application data. A matching channel path alone is not an access check.

The platform normalizes the trigger inputs on import. Keep the schema returned by pull instead of adding custom fields. `realtime.get_session` supplies the current connection context; its `params` can contain string path values, as described in [Typed path parameters](/realtime/realtime-servers-and-channels#typed-path-parameters).

## Next steps

<CardGroup cols={2}>
  <Card title="Access Control" icon="lock-keyhole" href="/realtime/access-control">
    How triggers fit with channel and message settings.
  </Card>

  <Card title="Messages" icon="message" href="/realtime/messages">
    Delivery targeting and guarantees.
  </Card>
</CardGroup>


## Related topics

- [XanoScript for Triggers](/xanoscript/triggers.md)
- [Realtime](/realtime/overview.md)
- [Triggers](/building/logic/triggers.md)
