Expand description
Ocre: a Rails-like Rust web framework for Cloudflare Workers, built to run on the Workers free plan and to be written by AI agents.
An Ocre app is one Worker compiled to WebAssembly. Requests go through
serve, which runs a plain axum router with
sessions, CSRF protection, CORS and security headers. Handlers reach the
Worker’s bindings (D1, KV, R2, Queues, Durable Objects, email) through the
per-request Ctx. The ocre command-line tool (crate ocre-cli)
generates the app, its models, scaffolds and migrations, and deploys it.
Guides and the generated app’s conventions live in the repository
README; a one-page list of
every public item is in docs/api-index.md.
§Design rules
- Plain axum handlers. Every Ocre type is
Send, so handlers never need#[worker::send]. Extractors (Session,Flash,Json,Page,storage::Multipart,i18n::I18n,cache::Conditional) and responses (Created,cache::CacheControl,cache::ETag) are ordinary axum types. - One way to do each thing. SQL with
?Nplaceholders andparams!, askama templates compiled at build time, htmx for interactivity, a singleErrortype that knows its HTTP status. - Errors name the fix. A missing binding answers 500 and logs which
cloudflare.config.tsentry to add; internal details are logged, never shown to users. - Free plan first. Nothing costs a request, a KV write or a database row unless the app asks for it: sessions live in an encrypted cookie (no storage), static files are served by Workers Static Assets before the Worker runs, R2 downloads stream without passing through WebAssembly, and features that add binary size or startup CPU (GraphQL, realtime) are opt-in cargo features. The free plan allows 10 ms of CPU per request; functions note their cost in D1 rows, KV operations, Queue operations, R2 operations or CPU where it matters.
§Modules
| Module | Contents |
|---|---|
| crate root | serve, Ctx, Db, params! and Query / Paginated (D1), Error / Result, Json / ApiError / Page (JSON APIs), Session / Flash / Cookies, Validator, NestedForm (bracketed form names), request helpers (Format, RemoteIp, RequestId, redirect_back), render / error_page / Htmx / HxRedirect (feature html), serde helpers (optional, patch, bool_from_sql, …) |
bulk | Many rows in one D1 statement: bulk::insert, upsert and update (one JSON parameter, json_each) |
cache | Read-through values in Workers KV, Cache-Control, ETag and 304 Not Modified |
config | Typed app settings from Worker variables and secrets (ctx.config::<Settings>()), the environment (development or production) |
encryption | Encrypted model columns (AES-256-GCM keyed from SECRET_KEY_BASE), deterministic for lookups |
errors | Error reporting (Rails’ Rails.error): ctx.errors().report / handle / record, subscribers such as Sentry |
events | Structured events (Rails’ Rails.event): ctx.events().notify(name, payload), tags, context, subscribers |
filters | Ocre’s view helpers as askama filters: {{ price|number_to_currency("$") }} (feature html) |
graphql | /graphql endpoint and GraphiQL for an async-graphql schema (feature graphql) |
helpers | Rails’ view helpers: numbers (number_to_currency…), times (time_ago_in_words, strftime), text (excerpt, highlight) |
i18n | Translations from locales/*.yml, plurals, the request’s locale |
jobs | Background jobs on Cloudflare Queues, scheduled tasks on Cron Triggers |
jwt | HS256 JSON Web Tokens for API clients |
log | Structured logging to Workers Logs: levels, request-scoped fields (ctx.log()), JSON lines |
mail | Sending email (log, Resend, Cloudflare adapters) and receiving it from Email Routing |
oauth | “Sign in with GitHub / Google”: OAuth 2.0 code flow with PKCE |
password | PBKDF2-HMAC-SHA256 password digests |
push | Web push notifications: VAPID keys, RFC 8291 encryption, sending (feature push) |
realtime | WebSocket channels on a Durable Object, htmx broadcasts (feature realtime) |
replicas | D1 read replicas: reads from a nearby copy, each visitor still reading their own writes (D1_REPLICAS=on) |
security | Content-Security-Policy (nonces), Permissions-Policy, rate limits, safe redirects, sanitize / strip_tags, log filtering, HTTP Basic auth |
seo | JSON-LD (seo::json_ld), /sitemap.xml (Sitemap, with hreflang alternates) and /llms.txt (LlmsTxt) |
sse | Server-Sent Events: stream events to the browser as they happen |
storage | Files in Cloudflare R2: multipart uploads, attachments, streamed downloads |
token | Random tokens for emailed links and API keys, stored as SHA-256 digests |
webhooks | Signed webhooks: HMAC-SHA256 and Standard Webhooks verification, signing outgoing calls, each event processed once |
§A complete app
A generated app’s src/lib.rs (crate type cdylib) is the Worker entry
point plus an axum router whose state is Ctx:
use axum::{Router, extract::{Path, State}, routing::get};
use ocre::{ApiResult, Ctx, Json, OptionExt, params};
use serde::{Deserialize, Serialize};
use worker::{Context, Env, HttpRequest, event};
#[event(fetch)]
async fn fetch(req: HttpRequest, env: Env, _ctx: Context) -> worker::Result<worker::web_sys::Response> {
ocre::serve(routes(), req, env).await
}
fn routes() -> Router<Ctx> {
Router::new().route("/up", get(up)).route("/posts/{id}", get(show))
}
async fn up() -> &'static str {
"OK"
}
#[derive(Serialize, Deserialize)]
struct Post {
id: i64,
title: String,
}
// GET /posts/1: the row as JSON, or a JSON 404 when there is none. `ApiResult`
// answers errors as JSON; HTML pages return `ocre::Result` (feature `html`).
async fn show(State(ctx): State<Ctx>, Path(id): Path<i64>) -> ApiResult<Json<Post>> {
let post = ctx.db()?.first::<Post>("SELECT id, title FROM posts WHERE id = ?1", params![id]).await?;
Ok(Json(post.or_404()?))
}cloudflare.config.ts binds the D1 database as DB; ocre new writes it, and
ocre dev / ocre deploy run it.
§Cargo features
| Feature | Default | Enables |
|---|---|---|
html | yes | askama templates (render), HTML error pages, the Htmx extractor. API-only apps (ocre new --api) turn it off |
graphql | no | The graphql module (async-graphql). About 1.1 MB more WebAssembly and 20-60 ms of CPU when a Worker instance starts |
realtime | no | The realtime module and the exported OcreChannel Durable Object class (WebSocket Hibernation) |
Re-exports§
pub use serde_json;
Modules§
- bulk
- Many rows in one D1 statement (Rails’
insert_all,upsert_all, and anupdate_allwith a value per row). - cache
- Caching: read-through values in Workers KV, HTTP
Cache-Control/ETagand 304 responses. - config
- Typed app configuration from Worker variables and secrets, and the environment (development or production).
- encryption
- Attribute encryption for model columns: values are encrypted in Rust
before they reach D1 and decrypted when rows are read, like Rails’
encrypts. - errors
- Error reporting (Rails’
Rails.error): report errors with context to the logs and to services such as Sentry. - events
- Structured events (Rails 8.1’s
Rails.event): named facts about what the app did, with a payload, for analytics, audit trails or a data warehouse. - filters
- askama filters for Ocre’s
helpers(featurehtml):{{ post.price|number_to_currency("$") }}. - graphql
- GraphQL support (feature
graphql). - helpers
- View helpers: numbers, dates and text formatted like Rails’
number_to_currency,time_ago_in_words,excerpt… - i18n
- Translations:
locales/*.yml,%{name}interpolation, plurals, locale per request. - jobs
- Background jobs (Cloudflare Queues) and scheduled tasks (Cron Triggers).
- jwt
- JSON Web Tokens (HS256) for API clients.
- log
- Structured logging to Workers Logs: levels, request-scoped fields, JSON lines (Rails’
Rails.loggerand tagged logging). - Email: send with adapters (log, Resend, Cloudflare), receive from Email Routing.
- oauth
- “Sign in with GitHub / Google”: the OAuth 2.0 authorization code flow with PKCE.
- password
- Password hashing (PBKDF2-HMAC-SHA256).
- push
- Web push notifications: messages a browser shows even when the app’s
page is closed (the Push API, with the service worker of
ocre g pwa). - realtime
- Realtime updates: WebSocket channels on a Durable Object, HTML broadcasts for htmx (feature
realtime). - replicas
- D1 read replicas: reads go to a nearby copy of the database, writes to the primary, and each visitor still reads what they wrote (Rails’ automatic role switching, without a second database to configure).
- security
- Security helpers beyond what
servealways does: policies, rate limits, safe redirects, HTML cleaning. - seo
- Search engines and language models: JSON-LD, sitemaps and
llms.txt. - sse
- Server-Sent Events: a response that sends events as they happen (Rails’
ActionController::LivewithSSE). - storage
- File storage in Cloudflare R2: multipart uploads, attachments, streamed downloads.
- testing
- Test helpers for Ocre apps, like Rails’
ActionDispatch::IntegrationTestandActiveSupport::Testing: a request client for the running app, the test database, the server log, time travel and assertions. - token
- Random tokens for emailed links and API keys, stored as digests.
- webhooks
- Webhooks: signatures for calls in both directions, and processing each event once.
Macros§
- locales
- Declares the app’s locales as a
Localesstatic, compilinglocales/<code>.ymlinto the binary. - params
- Builds the parameter list of a
Dbquery:params![title, id].
Structs§
- ApiError
- Handler error for JSON endpoints: an
Errorrendered as a JSON body. - Batches
- Batches of rows by increasing id, from
Query::batches: Rails’find_in_batcheswithout holding a cursor open. - Cookies
- The request’s cookies; what handlers set goes out with the response.
- Created
201 Createdresponse with a JSON body, for create endpoints.- Ctx
- Per-request application context: the Worker environment with typed access to its bindings.
- Db
- Handle to the application’s D1 (SQLite) database, from
Ctx::db. - Error
Page - What an HTML error page may show: the status, a message safe for users, and validation errors.
- Field
Error - One failed validation: the field name plus the message without it, as in Rails’
errors. - Flash
- Flash messages set by the previous request, as an extractor.
- Htmx
- Extractor telling whether htmx sent the request:
Htmx(true)when it hasHX-Request: true. - HxRedirect
- Response telling htmx to load another page:
200 OKwith theHX-Redirectheader. - Json
- JSON request body extractor and response, whose failures are JSON too.
- Markdown
- A Markdown response:
text/markdown; charset=utf-8(Rails’render markdown:). - Nested
Form - Form extractor for bracketed field names, like Rails’
params(post[title],tag_ids[],lines[0][qty]). - Page
?limit=&offset=pagination for list endpoints, as an extractor.- Page
Links - The
Linkheader built byPage::links, as a response part (nothing when there is no other page). - Paginated
- A page of rows plus the total, for pagination links and JSON envelopes.
- Param
- A value bound to a
?Nplaceholder of a D1 query. - Query
- A
SELECTon one table, built step by step, whose values are always bound parameters. - Remote
Ip - Extractor for the client’s IP address, from Cloudflare’s
CF-Connecting-IPheader. - Request
Id - Extractor for an identifier of the request, to correlate log lines and error reports.
- Session
- The current request’s session, stored in an encrypted cookie, as an extractor.
- Statement
- One SQL statement with its parameters, for
Db::batch. - Validator
- Collects field errors with Rails-style messages;
finishfails with all of them.
Enums§
- Direction
- Sort direction for
Query::order_by, read from a query string asascordesc. - Error
- Handler error: an HTTP status plus, for client errors, a message the user may see.
- Format
- Response format a client asks for in its
Acceptheader, for actions that answer HTML or JSON (Rails’respond_to).
Constants§
- ALLOWED_
HOSTS - Name of the Worker variable listing the host names the app answers to (Rails’
config.hosts). - ALLOWED_
ORIGINS - Name of the Worker variable listing extra origins that may call the app from a browser.
- MAX_
SAFE_ INTEGER - Largest integer a JavaScript number (and so D1) represents exactly: 2^53 - 1.
- SECRET_
KEY_ BASE - Name of the Worker secret the session encryption key is derived from:
SECRET_KEY_BASE. - SECRET_
KEY_ BASE_ PREVIOUS - Name of the Worker secret listing the previous
SECRET_KEY_BASEvalues, during a rotation. - SESSION_
COOKIE - Name of the session cookie:
_ocre_session.
Traits§
- Into
Param - Types that can be bound as D1 query parameters.
- Option
Ext - Extension for
Option:option.or_404()?turns a missing record into a 404 response.
Functions§
- bool_
from_ sql - Deserializes a SQLite boolean column (INTEGER 0/1) into
bool. - encode_
path - A route path with its non-ASCII characters percent-encoded, so Unicode routes match (Rails’ Unicode routes).
- error_
page - Renders error responses with the app’s own template (Rails’
public/404.htmland500.html). - escape_
like - Escapes
%,_and\sotextmatches literally in aLIKE ... ESCAPE '\'pattern (Rails’sanitize_sql_like). - json_
from_ sql - Deserializes a JSON column (the JSON text D1 returns) into
serde_json::Value. - now
- Current Unix time in seconds, on Workers and in native tests.
- optional
- Deserializes an optional field where
null, a missing value or an empty string isNone. - optional_
json_ from_ sql - Like
json_from_sqlfor aNULL-able JSON column:NULLisNone. - patch
- Deserializes a field of a partial update (PATCH) into keep / clear / set.
- patch_
json - Deserializes an optional JSON field of a partial update into keep / clear / set.
- redirect_
back - Redirects to the page the request came from, or to
fallback(Rails’redirect_back_or_to). - remote_
ip - The client’s IP address from the
CF-Connecting-IPheader, for code that has the headers but no extractor. - render
- Renders an askama template (compiled at build time) into an HTML response.
- serve
- Runs one request through the application router: the Worker
fetchentry point. - sleep
- Waits
durationwithout using CPU (JavaScript’ssetTimeout): pacing forssestreams and polling.