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.
$ 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
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.
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.
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.
/// 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); }
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.
| Service | Free every day or month | What Ocre uses it for |
|---|---|---|
| Workers | 100,000 requests a day, 10 ms CPU each | Your whole app, one Worker |
| D1 | 5 million rows read, 100,000 written a day; 5 GB | Models, migrations, users |
| Queues | 10,000 operations a day | Background jobs, mail sent later |
| R2 | 10 GB stored; 1 million Class A, 10 million Class B operations a month | Attachments, direct and multipart uploads |
| Durable Objects | 100,000 requests a day | Realtime pages over WebSockets |
| Workers KV | 100,000 reads, 1,000 writes a day | The cache |
From idea to production, tonight.
The tutorial builds a live Q&A app with accounts, realtime votes, moderation and tests, then deploys it.