A web framework for Rust

Rust on the web,without the wait.

Soda is Rust and Datastar, written the way SvelteKit is: routes as folders, components in view!, forms that post to typed actions, Tailwind through design tokens. And soda dev patches each change into the running app in under a second.

src/routes/page.rs
use super::actions::Add;
use crate::prelude::*;

pub async fn page(cx: &Cx) -> Result<View> {
    let items = cx.state.items().await?;
    Ok(view! {
        <form action=Add class="flex gap-2">
            <Input name="text" placeholder="Add an item" required />
            <Button primary submit>"Add"</Button>
        </form>
        <ul class="divide-y divide-line">
            {items.iter().map(|item| view! { <ItemRow {item} /> })}
        </ul>
    })
}
src/routes/actions.rs
use crate::prelude::*;

#[derive(Form)]
pub struct NewItem {
    #[form(max = 500)]
    pub text: String,
}

#[action]
pub async fn add(cx: &mut Act, form: NewItem) -> Result {
    cx.state.add(&form.text).await?;
    Ok(()) // and every open page draws itself again
}

soda devpatched src/routes/page.rs in 0.7 severy open page drawn again, nothing lost

01

What you
write

A page is a function of what it reads and its address. An action changes things, and every open page draws itself again. There are no loaders, caches or client state to keep in step.

Routes are folders

A folder is an address, and [item] a typed param. Links are functions (link::items::item(&id)), and a missing page or a param named twice is a compile error that names the file.

src/routes/
  layout.rs             around every page
  page.rs               /
  actions.rs            what its forms post to
  items/[item]/
    page.rs             /items/:item

Views and components

JSX-like markup in view!, everything escaped. A function of its props is a component, and a missing prop is a compile error naming it.

#[component]
pub fn ItemRow(item: &Item) -> View {
    view! {
        <li class="flex gap-3">{&item.text}</li>
    }
}

Typed actions and forms

Forms and buttons post to #[action]s. #[derive(Form)] reads and checks what was sent, and an error goes back to its field, in words.

#[derive(Form)]
pub struct NewItem {
    #[form(max = 500)]
    pub text: String,
}

Tailwind, through tokens

One @theme decides the look, Soda's own screens included. The build refuses a color, font, radius or shadow that isn't a token.

@theme {
  --color-accent: #c8175f;
  --radius-card: 18px;
  --font-display: "Fraunces";
}

Patched in, not rebuilt

soda dev patches a change into the running app, and every open page draws again: the server's sessions, its database and the form you're typing in all stay.

A page, a component or an action
0.7 s
a page, a component or an action, on a server
A style
0.3 s
a style, swapped in place
How it works

Datastar underneath

Pages are HTML, streamed to the browser and morphed in place. Each open page holds one stream, so a change anyone makes shows on every page at once.

For what only a browser can do (times in your zone, deadlines that tick, cards dragged between lists), Soda's components are web components, styled by the same tokens.

02

One app.
Three backends.

Where an app runs is its backend. Its routes, pages, components and layouts are written the same on each, and naming one is a line in src/main.rs.

soda_server::app!(list::List);         // on a server
soda_local::app!(model::Items);        // local-first, in each person's browser
soda_static::app!(handbook::Handbook); // static, every page drawn as it's built

soda-server

On a server

A program you deploy draws every page from what it keeps: SQLite in the template, or anything you like. The first paint is its own HTML, then each open page holds one stream, drawn again after anyone's action.

  • Sessions and streams done for you
  • Your database and your queries
  • soda deploy: a container image, or straight to a Docker host

soda new notes

soda-local

Local-first

No server of its own. Each person's browser runs the app. Every change is signed and sealed on their device, and synced through a store that can't read a word.

  • Accounts, passkeys, sharing, sync, undo and offline, built in
  • Members' browsers trade changes directly, in about 20 ms
  • soda deploy: static files, on any host

soda new --local pantry --noun pantry

soda-static

Built ahead of time

Every page drawn once, as the site is built, into files any host serves: docs, a blog, a landing page. Each page has its own title and description, and there's a 404 page and a sitemap. This site is one, drawn from the SDK's own docs.

  • A page for each, from its route's params
  • Under soda dev, a doc saved shows at once
  • No server to run at all: Vercel, Netlify, GitHub Pages, a bucket

soda new --static handbook

03

Made with Soda

04

Start in
a minute

# the soda command, once, from a checkout of the repository
$ cargo install --path cli
# a new app on a server, called Notes
$ soda new notes
$ cd notes && soda dev
  serving http://localhost:5180, each change patched in
Esc