Rally

Rally

Package Version Hex Docs

Rally is a Gleam package for building Lustre apps that render on the server and hydrate in the browser. You write page modules and server_* handler functions. Rally generates routing, server-side rendering, WebSocket transport, and typed client-server messaging.

The page file is the contract. Client state, server calls, and the message types that cross the wire all live together until you choose to extract shared code.

Rally apps use SQLite by default: embedded database, migrations, and type-safe SQL codegen, with no separate database server for development.

What Rally Generates

Rally reads page modules and writes the routing, SSR, WebSocket transport, request and response encoding, and dispatch code around them.

You still write the UI, SQL, auth policy, and server handlers.

Create an app

gleam new my_app
cd my_app
gleam add rally libero
gleam run -m rally init
./bin/dev

rally init writes the starter app into the current Gleam project, including bin/dev. After that, ./bin/dev runs codegen, builds the JS client, and starts the server on port 8080. Open http://localhost:8080 to see the app. The starter app uses SQLite, so development does not need a database daemon.

Writing a page

A page file in src/<namespace>/pages/ is a Lustre component with server calls:

import gleam/int
import lustre/element.{type Element, text}
import lustre/element/html
import rally_runtime/effect.{type Effect}
import rally_runtime/effect
import server_context.{type ServerContext}

// MODEL -- client state for this page.

pub type Model {
  Model(count: Int)
}

pub fn init() -> #(Model, Effect(Msg)) {
  #(Model(count: 0), effect.none())
}

// UPDATE -- client messages and how they change the model.

pub type Msg {
  Increment
  GotIncrement(Result(Int, List(String)))
}

pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
  case msg {
    Increment ->
      // effect.rpc sends ServerIncrement to server_increment and
      // routes the response back through GotIncrement.
      #(model, effect.rpc(ServerIncrement(amount: 1), on_response: GotIncrement))

    GotIncrement(Ok(amount)) ->
      #(Model(count: model.count + amount), effect.none())

    GotIncrement(Error(_)) ->
      #(model, effect.none())
  }
}

// VIEW -- shared by server (SSR) and client (SPA).

pub fn view(model: Model) -> Element(Msg) {
  html.button([], [text("Count: " <> int.to_string(model.count))])
}

// SERVER -- message type and handler.
// Libero scans the handler signature to generate the wire contract.

pub type ServerIncrement {
  ServerIncrement(amount: Int)
}

pub fn server_increment(
  msg msg: ServerIncrement,
  server_context _server_context: ServerContext,
) -> Result(Int, List(String)) {
  Ok(msg.amount)
}

Model, Msg, init, update, and view are normal Lustre TEA. ServerIncrement and server_increment define the server call. The client sends the typed message with effect.rpc.

There is no separate API schema. Libero scans the handler signature and Rally wires it into the generated client and server code.

File-based routing

The filename determines the URL:

FileURLRoute variant
home_.gleam or index.gleam/Home
about.gleam/aboutAbout
products/id_.gleam/products/:idProductsId(id: Int)
settings/profile.gleam/settings/profileSettingsProfile

A trailing _ makes the segment dynamic. Params named id or ending in _id parse as Int; others parse as String.

What to import

Most Rally apps use only a few modules directly:

ModuleUse it for
rally_runtime/effectPage effects: RPC, server messages, navigation, broadcast, client context updates
rally_runtime/dbSQLite open, timed queries, nested transactions, SQL value helpers
rally_runtime/systemApp startup and background jobs
rally_runtime/sessionSession cookie generation, parsing, response headers
rally_runtime/authAuth policy and load result types
rally_runtime/envAPP_ENV parsing and production cookie policy
rally_runtime/migrateNumbered SQLite migrations
rally_runtime/test_dbFast in-memory SQLite for tests

The rally/internal/... modules are codegen implementation. App code should treat them as private. The generated files under src/generated/ are the boundary Rally presents to your app.

Generated files

Running gleam run -m rally reads [[tools.rally.clients]] from gleam.toml and produces:

Server-side (in src/generated/<namespace>/): router, page dispatch, RPC dispatch, SSR handler, WebSocket handler, HTTP handler, protocol wire facade.

Client-side (in .generated_clients/<namespace>/): Lustre SPA entry, WebSocket transport, tree-shaken page modules, codec, effect shim.

The client package is a standalone Gleam project. The server project is the input to codegen.

Examples

More docs

Contributing

Rally is a Gleam project targeting Erlang. You need Gleam (v1.x), Erlang/OTP 26+, SQLite3, and Node.js.

git clone <repo-url>
cd rally
gleam build
gleam test

Rally depends on Libero. App projects should add both packages with gleam add rally libero.

Influences

License

MIT. See LICENSE.

Search Document