ocre
Rust · Workers · v0.2written with AI agents ↘

The Rails way, on Cloudflare's free plan.

Models, controllers, jobs, mailers, auth, file uploads and realtime pages, generated as plain Rust you can read, compiled to WebAssembly, and deployed to Cloudflare Workers with one command.

Build a live Q&A app →
$ cargo install ocre-cli
$0 / monthWorkers, D1, R2, Queues
1 commandto provision and deploy
426 KBthe whole Q&A app, gzipped WebAssembly
~/code
$ ocre new qa --starter qa --yes
  create  qa/src/events.rs
  create  qa/src/questions.rs  …
$ cd qa && ocre migrate && ocre dev
  Ready on http://localhost:8787
  # two windows on one event: ask, vote,
  # and watch the list reorder in both
$ ocre deploy
  Created the SECRET_KEY_BASE secret on Cloudflare
  Created D1 database qa on Cloudflare

  https://qa.you.workers.dev
AxumaskamahtmxD1 · SQLiteR2QueuesDurable ObjectsWebAssembly
// why ocre

Everything a product needs.
Nothing to pay for.

Ocre follows Rails' conventions, rebuilt around what Cloudflare gives every account for free: one Worker, a SQLite database, a queue, object storage.

◆

Code you can read

Generators write ordinary Rust into your app: models with plain SQL, axum handlers, askama templates. No hidden runtime magic: open the file, change it.

$0

Built for the free plan

10 ms of CPU per request, 50 subrequests per invocation, 100,000 requests a day: every feature is designed and measured against those limits.

⌁

Made for AI agents

Every app has an AGENTS.md, every command speaks --json, and these docs come as /llms.txt. Agents write the code; you read it.

▦

Batteries included

Auth and OAuth, mail in and out, background jobs and crons, R2 uploads, WebSockets, i18n, webhooks, web push, a cache.

✓

Test what you ship

Unit tests natively, request tests against the real runtime (workerd), browser tests with Playwright: ocre test --e2e runs them all.

▲

One command to production

ocre deploy creates the database, queues, buckets and secrets it needs, migrates, then ships. No dashboard visit.

// a feature, end to end

Live votes, a few small files.

How the tutorial's Q&A app counts a vote and pushes the new ranking to every open screen, trimmed: you own every line.

src/models/question.rs
/// Open questions first, most voted first, then answered ones.
pub async fn for_event(ctx: &Ctx, event_id: i64) -> Result<Vec<Question>> {
    query()
        .eq("event_id", event_id)
        .order_asc("answered")
        .order_desc("votes")
        .limit(200)
        .all(&ctx.db()?)
        .await
}

/// One UPDATE, so votes arriving at the same moment all count.
pub async fn vote(ctx: &Ctx, id: i64) -> Result<Option<Question>> {
    let sql = "UPDATE questions SET votes = votes + 1 WHERE id = ?1 RETURNING *";
    ctx.db()?.first(sql, params![id]).await
}
/// One vote per question and browser; voting again changes nothing.
async fn vote(State(ctx): State<Ctx>, session: Session, Htmx(is_htmx): Htmx, Path(id): Path<i64>) -> Result<Response> {
    let record = question::find(&ctx, id).await?.or_404()?;
    let event = record.event(&ctx).await?.or_404()?;
    let mut voted: Vec<i64> = session.get("voted")?.unwrap_or_default();
    if !voted.contains(&id) {
        question::vote(&ctx, id).await?;
        voted.push(id);
        session.insert("voted", &voted)?;
        broadcast(&ctx, &event).await?;
    }
    Ok(done(is_htmx, &event)) // htmx: 204, the broadcast updates the page
}
/// The whole list, rendered once per audience, swapped into every open room.
async fn broadcast(ctx: &Ctx, event: &Event) -> Result<()> {
    let questions = question::for_event(ctx, event.id).await?;
    let audience = render(&ListView { questions: &questions, host: false })?.0;
    let host = render(&ListView { questions: &questions, host: true })?.0;
    realtime::broadcast(ctx, &channel(event), &realtime::update("questions", &audience)).await.ok();
    realtime::broadcast(ctx, &host_channel(event), &realtime::update("questions", &host)).await.ok();
    Ok(())
}

<!-- templates/events/show.html: htmx connects and swaps, no custom JavaScript -->
<div hx-ext="ws" ws-connect="/realtime/event:{{ event.public_id }}">
#[test]
#[ignore = "request test: run with `ocre test --e2e`"]
fn each_browser_votes_once_per_question() {
    let (_, event) = host_with_event();
    let mut first = Client::new().htmx();     // its own cookies, like a browser
    let id = ask(&mut first, &event, "Can we get the slides?");
    first.post(&format!("/questions/{id}/vote"), &()).assert_status(204);
    first.post(&format!("/questions/{id}/vote"), &()).assert_status(204);
    assert_eq!(votes(id), 1);
    Client::new().htmx().post(&format!("/questions/{id}/vote"), &()).assert_status(204);
    assert_eq!(votes(id), 2);
}
// the free plan, measured

What $0 gets you.

The limits Ocre is built around (Cloudflare free plan, autumn 2026). The limits reference says what each feature costs against them.

ServiceFree every day or monthWhat Ocre uses it for
Workers100,000 requests a day, 10 ms CPU eachYour whole app, one Worker
D15 million rows read, 100,000 written a day; 5 GBModels, migrations, users
Queues10,000 operations a dayBackground jobs, mail sent later
R210 GB stored; 1 million Class A, 10 million Class B operations a monthAttachments, direct and multipart uploads
Durable Objects100,000 requests a dayRealtime pages over WebSockets
Workers KV100,000 reads, 1,000 writes a dayThe cache

From idea to production, tonight.

The tutorial builds a live Q&A app with accounts, realtime votes, moderation and tests, then deploys it.