Server messaging

Rally pages communicate with the server in two ways: RPC and stateful server pages. Start with RPC. It’s simpler, and most pages only need it. Reach for the stateful server model when the server needs to remember per-connection state between messages.

RPC

RPC lets a page call a server function and get a result back. Define a message type and a handler function in your page module:

pub type ServerLoadTitle {
  ServerLoadTitle(slug: String)
}

pub fn server_load_title(
  msg msg: ServerLoadTitle,
  server_context _server_context: ServerContext,
) -> Result(String, List(String)) {
  Ok("Article: " <> msg.slug)
}

Then call it from the client:

effect.rpc(ServerLoadTitle(slug: "hello"), on_response: GotTitle)

Libero (Rally’s code generation layer) reads the handler’s function signature and derives the wire contract from it. The message type becomes the request shape. The return type becomes the response shape. You don’t write a separate API definition, route, or serializer. The Gleam types are the contract.

If you need to load data, validate a form, or run any one-shot server operation, RPC is the right choice.

Stateful server pages

Some pages need the server to hold onto state between messages. An example: after loading an article, you want to toggle favorites and add comments without re-fetching the article ID each time. The server keeps the article_id in its own model so subsequent messages can use it for authorization and queries.

Define three types and two lifecycle functions:

pub type ToServer {
  ToggleFavorite
  AddComment(body: String)
}

pub type ToClient {
  FavoriteToggled(article_id: Int)
  CommentSaved(body: String)
}

pub type ServerModel {
  ServerModel(article_id: Int)
}

pub fn server_init(
  slug: String,
  server_context _server_context: ServerContext,
) -> #(ServerModel, Effect(ToClient)) {
  let article_id = string.length(slug)
  #(ServerModel(article_id:), effect.none())
}

pub fn server_update(
  model: ServerModel,
  msg: ToServer,
  server_context _server_context: ServerContext,
) -> #(ServerModel, Effect(ToClient)) {
  case msg {
    ToggleFavorite ->
      #(model, effect.send_to_client(FavoriteToggled(model.article_id)))

    AddComment(body) ->
      #(model, effect.send_to_client(CommentSaved(body)))
  }
}

The client sends messages with effect.send_to_server:

effect.send_to_server(ToggleFavorite)

The server processes the message in server_update and pushes ToClient messages back. The page’s client-side update function receives those messages and updates the UI.

When to use stateful vs. RPC

Use RPC when each call is independent. Use the stateful model when the server needs to remember something between calls. A common case: server_init loads a resource and stores its ID in ServerModel. Then server_update can authorize and act on subsequent messages using that stored ID without the client having to send it again.

If you find yourself passing the same context (an entity ID, a session token) on every RPC call, that’s a sign the page wants server state.

Broadcast

Stateful server pages can send messages beyond the originating connection:

EffectWho receives it
send_to_client(msg)One specific connection
broadcast_to_session(msg)Every tab in the same browser session
broadcast_to_page(msg)Every connection viewing the same page
broadcast_to_app(msg)Every connection to the app

Connections auto-subscribe to their relevant topics when the WebSocket connects. Under the hood, broadcast uses OTP pg (process groups) for topic management, so it works across nodes in a cluster without extra configuration.

Search Document