Ticket

A Rails-shaped web framework, written in Polar.

Resource routes, controllers, views and filters, all checked by Polar's type checker before the first request comes in. Ticket is under construction: the data milestone (SQLite, migrations, typed queries) is being built right now, and the APIs will change.

Under construction. F2 is in progress, and the APIs will change.

The lineup

What already works in Ticket today. Every example on this page comes from the framework's own example app and tests.

routes zone

Routes the way Rails taught you

root, verb lines, resources with only and except, nesting, member, collection and namespace. The zone generates the router and a typed helper for each named route, like article_path(id).

polar start -- routes prints the table, just like rails routes.

routes
  root         -> PagesController.home
  get  /about  -> PagesController.about  as about

  resources articles  -> ArticlesController  only index show new create

  namespace admin
    get  /stats  -> StatsController.show  as stats
$ polar start -- routes
  root_path         GET   /              PagesController.home
  about_path        GET   /about         PagesController.about
  articles_path     GET   /articles      ArticlesController.index
  new_article_path  GET   /articles/new  ArticlesController.new
                    POST  /articles      ArticlesController.create
  article_path      GET   /articles/:id  ArticlesController.show
  admin_stats_path  GET   /admin/stats   StatsController.show
controllers

Controllers are modules of plain functions

Every action takes a Conn and returns a Response. Its effects are in the signature, so you can see at a glance which actions touch state and which can halt.

redirect_to answers a form post with a 303, so the browser follows it with a GET.

functions
  show(conn: Conn) -> Response / {Mut, Throws<Halt>} {
    let article = find_or_halt(conn)

    Response.render(Layout.app(article.title, Articles.show(article)))
  }

  create(conn: Conn) -> Response / {Mut} {
    let article = Article.create(title_of(conn))

    Response.redirect_to(conn, Paths.article_path(article.id))
  }
views zone

Markup that type-checks

A view is a header plus markup, and it compiles to a function that returns Html. Holes are escaped, a Bool attribute appears or disappears, and a None leaves the attribute out.

Shared components go in a view_component zone and are used as <UI.Card> from any module.

view_component
  Card(title: String, children: Html)
    <section class="card">
      <h2>{title}</h2>
      {children}
    </section>

views
  item(article: Article)
    <li><a href={Paths.article_path(article.id)}>{article.title}</a></li>

  wrapped()
    <Card title="Latest">
      <p>Fresh off the press</p>
    </Card>
filters

Filters without magic

A filter is a function from Conn to Result<Response, Conn>. Ok lets the request through and Err answers it. Action.before runs them in order, like Rails' before_action.

Deep inside a helper, throw Halt(response) answers the request on the spot.

functions
  show(conn: Conn) -> Response / {Mut} {
    Action.before([require_token], stats)(conn)
  }

  require_token(conn: Conn) -> Result<Response, Conn> {
    if Params.string(conn, "token") == Some("secret") {
      Ok(conn)
    } else {
      Err(Response.text(401, "unauthorized"))
    }
  }
compile time

Typos fail the build, not the request

Ticket's zones are Polar plugins, so mistakes in routes and views are reported by polar check on the line you wrote, with a hint for the fix.

routes
  resources posts  -> PostController
$ polar check
error[POLAR0901]: no module `PostController` in `uses`
  --> src/routes.px:12:23
   |
12 |   resources posts  -> PostController
   |                       ^^^^^^^^^^^^^^
   |
   = help: a controller is a module in `uses`, named by
           its last segment or its `as` alias

Tour dates

Ticket is built one milestone at a time. Here's where it's playing.

  1. F0

    Skeleton

    The package builds, and an example app depends on it and serves a page.

    Played
  2. F1

    Request to response

    Conn and params, views, resource routing, controllers and the ticket CLI (new, server, routes).

    Played
  3. F2

    Data

    SQLite through a Db effect, migrations and typed queries are done. Validations and associations are next.

    On now
  4. F3

    App ergonomics

    Forms, sessions, flash and CSRF, static files, generators and test helpers.

    Later

Try it early

Ticket isn't published yet. Clone it next to your app and add it as a path dependency. You'll need the polar toolchain and Node 22.5 or newer.

# polar.toml
[project]
name = "app"
hosts = ["Node"]
main = "src/routes.px"

[dependencies]
ticket = { path = "../ticket" }

[run]
launcher = "ticket"
options = { port = 3000 }
$ polar run
# builds every host and serves on :3000

$ polar start -- routes
# prints the route table

A Rails-style ticket command with ticket new, ticket server and ticket routes is the next thing being built.