Skip to main content

Crate ocre

Crate ocre 

Source
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 ?N placeholders and params!, askama templates compiled at build time, htmx for interactivity, a single Error type that knows its HTTP status.
  • Errors name the fix. A missing binding answers 500 and logs which cloudflare.config.ts entry 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

ModuleContents
crate rootserve, 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, …)
bulkMany rows in one D1 statement: bulk::insert, upsert and update (one JSON parameter, json_each)
cacheRead-through values in Workers KV, Cache-Control, ETag and 304 Not Modified
configTyped app settings from Worker variables and secrets (ctx.config::<Settings>()), the environment (development or production)
encryptionEncrypted model columns (AES-256-GCM keyed from SECRET_KEY_BASE), deterministic for lookups
errorsError reporting (Rails’ Rails.error): ctx.errors().report / handle / record, subscribers such as Sentry
eventsStructured events (Rails’ Rails.event): ctx.events().notify(name, payload), tags, context, subscribers
filtersOcre’s view helpers as askama filters: {{ price|number_to_currency("$") }} (feature html)
graphql/graphql endpoint and GraphiQL for an async-graphql schema (feature graphql)
helpersRails’ view helpers: numbers (number_to_currency…), times (time_ago_in_words, strftime), text (excerpt, highlight)
i18nTranslations from locales/*.yml, plurals, the request’s locale
jobsBackground jobs on Cloudflare Queues, scheduled tasks on Cron Triggers
jwtHS256 JSON Web Tokens for API clients
logStructured logging to Workers Logs: levels, request-scoped fields (ctx.log()), JSON lines
mailSending email (log, Resend, Cloudflare adapters) and receiving it from Email Routing
oauth“Sign in with GitHub / Google”: OAuth 2.0 code flow with PKCE
passwordPBKDF2-HMAC-SHA256 password digests
pushWeb push notifications: VAPID keys, RFC 8291 encryption, sending (feature push)
realtimeWebSocket channels on a Durable Object, htmx broadcasts (feature realtime)
replicasD1 read replicas: reads from a nearby copy, each visitor still reading their own writes (D1_REPLICAS=on)
securityContent-Security-Policy (nonces), Permissions-Policy, rate limits, safe redirects, sanitize / strip_tags, log filtering, HTTP Basic auth
seoJSON-LD (seo::json_ld), /sitemap.xml (Sitemap, with hreflang alternates) and /llms.txt (LlmsTxt)
sseServer-Sent Events: stream events to the browser as they happen
storageFiles in Cloudflare R2: multipart uploads, attachments, streamed downloads
tokenRandom tokens for emailed links and API keys, stored as SHA-256 digests
webhooksSigned 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

FeatureDefaultEnables
htmlyesaskama templates (render), HTML error pages, the Htmx extractor. API-only apps (ocre new --api) turn it off
graphqlnoThe graphql module (async-graphql). About 1.1 MB more WebAssembly and 20-60 ms of CPU when a Worker instance starts
realtimenoThe 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 an update_all with a value per row).
cache
Caching: read-through values in Workers KV, HTTP Cache-Control/ETag and 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 (feature html): {{ 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.logger and tagged logging).
mail
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 serve always 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::Live with SSE).
storage
File storage in Cloudflare R2: multipart uploads, attachments, streamed downloads.
testing
Test helpers for Ocre apps, like Rails’ ActionDispatch::IntegrationTest and ActiveSupport::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 Locales static, compiling locales/<code>.yml into the binary.
params
Builds the parameter list of a Db query: params![title, id].

Structs§

ApiError
Handler error for JSON endpoints: an Error rendered as a JSON body.
Batches
Batches of rows by increasing id, from Query::batches: Rails’ find_in_batches without holding a cursor open.
Cookies
The request’s cookies; what handlers set goes out with the response.
Created
201 Created response 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.
ErrorPage
What an HTML error page may show: the status, a message safe for users, and validation errors.
FieldError
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 has HX-Request: true.
HxRedirect
Response telling htmx to load another page: 200 OK with the HX-Redirect header.
Json
JSON request body extractor and response, whose failures are JSON too.
Markdown
A Markdown response: text/markdown; charset=utf-8 (Rails’ render markdown:).
NestedForm
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.
PageLinks
The Link header built by Page::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 ?N placeholder of a D1 query.
Query
A SELECT on one table, built step by step, whose values are always bound parameters.
RemoteIp
Extractor for the client’s IP address, from Cloudflare’s CF-Connecting-IP header.
RequestId
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; finish fails with all of them.

Enums§

Direction
Sort direction for Query::order_by, read from a query string as asc or desc.
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 Accept header, 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_BASE values, during a rotation.
SESSION_COOKIE
Name of the session cookie: _ocre_session.

Traits§

IntoParam
Types that can be bound as D1 query parameters.
OptionExt
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.html and 500.html).
escape_like
Escapes %, _ and \ so text matches literally in a LIKE ... 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 is None.
optional_json_from_sql
Like json_from_sql for a NULL-able JSON column: NULL is None.
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-IP header, 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 fetch entry point.
sleep
Waits duration without using CPU (JavaScript’s setTimeout): pacing for sse streams and polling.

Type Aliases§

ApiResult
Result for JSON handlers: errors become ApiError JSON responses.
Result
Result with Error as the default error type, returned by Ocre APIs and handlers.