The server: routes and responses, auth, CORS, sessions, templates, cache, rate limits, uploads, SSE, sockets, pages, static files, mail, queues, API docs, and the HTTP client.
use web, ui
use views.home
serve[3000]
page['/', home.render]
Crow manages the application instance implicitly — no app variable needed. page[path, view] registers a LiveView page rendered by a view; event handlers are bound inside the view (click = inc), so there is no separate handler argument. serve[port] starts the server (serve[port, refresh = N, budget = M] adds periodic re-rendering and a latency budget — see Background Work). page[path, view, url = ['day', 'sym']] puts those facts in the page's address: a session starts from the values in the query string (/?day=2026-09-04&sym=IMRN), each read in its fact's shape, and the address follows them as they change (history.replaceState), so a reload or a shared link opens the same view — a fact of a part is named by its path, url = ['session.day'], and appears by its last name. A :name segment of a page's path is a fact in its address too: page['/card/:card', card_page] opens a session with the fact card read from the path (/card/7 is 7, in the fact's shape; a value the fact cannot hold, /card/x, is no page — a 404, where a query value that does not fit is passed over), and the address follows the fact as it changes (/card/8) — the path holds the facts that say which thing the page shows, the query string (url = […]) the facts that tune how. A path's name, like a url name, must name a fact (a part's by its last name, :day), or the program is refused before it runs, in both modes. A program serves every page it registers, and a page is a session of its own: a link to another page (a['Card 7', href = '/card/7'], or a nav of them) opens that page's session, and what pages share is the database, the signed-in reader, and the address. (crow run -i had served only the last page a program registered.) The look of the whole app is set there too: serve[port, theme = 'dark', density = 'compact', accent = '#3b82f6', wide = true] — theme is 'light', 'dark', or 'system' (the default, following the reader's setting), density = 'compact' tightens the element defaults for dense screens, accent overrides the accent token with a hex colour, and wide = true lets the page use the full window instead of the reading column. A wrong value is a startup error naming the choices. The order in source doesn't matter: the server starts only after every route is registered.
If no routes are registered, serve[] shows a default welcome page so the user always sees something; once a route or a page is, an address none of them answers is a 404, no page at /cards (the welcome page had answered it).
If the port is already in use, the server prints a friendly error: error: port 3000 is already in use.
| Function | Description | Example |
|---|---|---|
serve[port] | Start the server on the given port (implicit app) | Views: the whole element vocabulary · Web: responses and submodules · Web: a JSON route · View Blocks · Reactive State and Events · Components · Slow Work and the Live Loop · Testing a Live App · Charts and Styling · The Element Vocabulary · Derivations · Views and Live Updates |
page[path, view] | Add a LiveView page rendered by view | Views: the whole element vocabulary · View Blocks · Reactive State and Events · Components · Slow Work and the Live Loop · Testing a Live App · Charts and Styling · The Element Vocabulary · Derivations · Views and Live Updates |
route[path, handler] | Add a route; handler is a named fn handler[req] that receives the request and returns a response (implicit app; route[path, method, handler] for other methods; limit = 60 answers each client at most 60 times a minute) | Web: responses and submodules · Web: a JSON route · Derivations |
A route's handler is handed the request, and nothing else: it is a function, so no module variable is visible inside it (Function Scope), and no parameter can carry one in, since the route calls it with the request alone. What it needs it opens itself — conn = connect[env['DATABASE_URL']] — and every connect to one URL in a program is one handle, so the handlers that connect share one pool (### db). The request's req.method, req.path and req.body are text, and req.headers (their names in lower case), req.query and req.params are maps of text, so /hello/:name reads its name as req.params.name and /find?q=crow its query as req.query.q — decoded, so ?q=hello%20world is hello world (a query had reached the program still encoded). A form's fields are req.form, a map of text, from an urlencoded body or a multipart one, and a multipart body's files are req.files, a list of maps of text as a page's picked file is — field (the form's name for it), name, type, size and path, where the server keeps a copy under .crow/uploads/ while the handler runs and removes it once the handler has answered (cp[f.path, …] keeps one). A body bigger than the app takes — 10 MB, or serve[…, uploads = N] megabytes, the limit a page's uploads keep — is answered 413 Payload Too Large before the handler sees it (a body had been read whatever its size), and a multipart body that does not parse 400 Bad Request.
serve[port, cors = ['https://app.example.com'], files = 'public'] sets the server's options. cors names the sites whose pages may call the app — '*' any — and answers their preflights; a page on any other site is told nothing, so its browser refuses the answer. files is a directory whose files answer the addresses no route or page does — /logo.png is public/logo.png, / its index.html — and a file it does not hold is the app's 404, as is one whose name, or a directory's on its path, starts with a dot: .env, .git/, .crow/, however the address spells the dot (files = '.' had handed out .crow/keys/, whose key seals the sign-in cookies); .well-known, where a certificate authority looks, is served. A route's limit = n answers each client at most n times a minute, a client being its address (a proxy's on this machine, the first address its X-Forwarded-For names); a request over it is 429 Too Many Requests, its Retry-After the seconds to wait. All three are the server's, the same in both modes. A route that names a method answers only it, and another method at its path is 405 Method Not Allowed. Both modes serve routes and make every response below through the same functions, so they answer alike, to the byte of a msgpack or an arrow body. A handler whose value fails — int[req.query.n] of a query that is no number, a frame that did not load — answers 500 and an id, the request could not be answered — the server's log has why, as 3f9a1c07, and the server's log names the handler and the reason beside it, crow: flow stopped [3f9a1c07]: cannot convert 'x' to int; so does one that stops (boom stopped [91c0d2e4]: line 5: index 9 is out of range for an array of 2 items). The client is told no more, since a reason names what the program reaches — a database's address, a file's path (it had been told the reason; Vijay, 2026-10-01) — in both modes, and the server goes on. A response HTTP cannot carry — a status outside 100 to 999, a header whose value holds a line break or another control character, as one a handler built from what a request sent may — answers 500 and an id as a failed handler does, the log saying why beside it (a response could not be sent [5e07aa1b]: the header 'x-name' holds a line break or another control character); text beyond ASCII in a header's value is sent as its bytes. (It had answered internal error, saying nothing, and refused text beyond ASCII the same way.) A route's handler that cannot wait on anything — no query, no read, no log, no loop, nothing it calls that does one — is answered on the server's own thread as its request is read (a hop to another thread and back had cost more than answering text['Hello, World!']); any other runs off the server's threads, as a page's does, so a slow one delays only its own request, on at most four threads a core (64 at least) at once: a request past them waits its turn, as one past a database's pool waits for a connection. Both modes decide which handlers cannot wait by the same rule. A route and its responses are web's: a program that calls one without use web is refused before it runs. (The interpreter had served no route at all, and then none of msgpack, arrow and stream; a compiled route that read a param or named a method had not built, nor one whose handler's value can fail, and the compiled stream had not built either; a compiled handler that stopped had dropped the connection, saying nothing.)
| Function | Description | Example |
|---|---|---|
text[s] | Plain text response | The standard library's layer: use sys · Web: responses and submodules · Derivations |
html[s] | HTML response | Web: responses and submodules · Reactive State and Events · Testing a Live App · The Element Vocabulary · Derivations |
json[map] | JSON response from a map; from a record, an object of its fields; from a list, an array — json[rows] of a list of maps or of records answers an array of objects. Each value is written as encode writes it: a value that failed is null, an int past 64 bits the text of its digits | JSON · A language model as a connection: use llm · The standard library's layer: use sys · Web: responses and submodules · Web: a JSON route · Derivations |
json[frame] | JSON response from a frame: its records, a list of objects (a missing cell null, a moment its ISO text) — what a page's fetch of the route reads back as a frame | JSON · A language model as a connection: use llm · The standard library's layer: use sys · Web: responses and submodules · Web: a JSON route · Derivations |
respond[status, body] | Response with custom status code | Web: responses and submodules |
redirect[url] | HTTP redirect response | Web: responses and submodules |
msgpack[value] | MessagePack binary response: the value as JSON holds it, packed (application/msgpack) — written in Crow (src/modules/web.co), each piece in its smallest form | Web: responses and submodules |
arrow[frame] | The frame as an Arrow IPC stream (application/vnd.apache.arrow.stream), which pandas, polars and every Arrow reader take | Web: responses and submodules |
bytes[data] | Raw binary response (application/octet-stream) | The standard library's layer: use sys · Web: responses and submodules |
stream[feed] | A text response sent as feed puts it, chunk by chunk; stream[feed, a, …] hands feed the rest | Web: responses and submodules |
A stream is sent as its feed puts it. stream[feed, a, …] answers at once, and when the response is sent it calls feed[put, a, …] on a thread of its own. Each put[text] sends that chunk to the reader as it is put, and answers whether the reader still listens: false once they have gone, so a long feed can stop early. The arguments after the feed are the values they have when the handler answers, which is how a feed reads the request. A feed puts text — a value is written into text first, put['`row`\n'] — and a put handed anything else is refused before the program runs; the chunks go out in the order they are put, a loop's included. A feed that fails or stops ends the body where it is, and the log says why. The body is text/plain, sent chunked.
use web
fn numbered[put, n]
for i in range[n]
if not put['line `i`\n']
return
fn flow[req]
return stream[numbered, int[req.query.n]]
route['/flow', flow]
serve[3000]
Design Note: a stream's feed. Considered: a feed that returns a list the runtime sends (it holds the whole body, which a stream exists not to); a channel value a handler writes to (a new kind of value for one builtin); send[out, chunk] on a handle (WebSocket's send would name two things); and chunks of any value, sent as JSON lines. Decided: the feed is a named function, as Crow has no lambdas, handed put, a function value the program already knows how to call; stream[feed, a, …] hands it what it needs from the request, as later[f, a, …] does. Text chunks keep one way to write a value (interpolation), and put answering whether the reader listens lets a feed stop with the return it already has.
A response is what a route returns. A page shows elements and text, so a view that places one — row[text['3 items']] — is refused in both modes: place the text itself, or p[…] or span[…].
use web.auth)| Function | Description | Example |
|---|---|---|
sign[claims, secret] | A token — a JSON Web Token signed with HS256 — carrying the claims (a map, or a record), issued now (iat) and expiring in an hour (exp) unless the claims say when | Math: tensors · Web: responses and submodules |
verify[token, secret] | The claims a token carries, as a parsed value (str[claims.user]), when it was signed with this secret and has not expired | Web: responses and submodules |
A token is how an API, which has no page session, says who calls it: sign gives one to a client that signed in, and each request carries it back for verify to read. A refusal is an error value of kind 'auth', its reason saying why: the token has expired, the token's signature does not match, not a token. A secret shorter than 32 characters signs nothing (a token's secret is 32 characters or more), as the sign-in cookie's CROW_SECRET keeps the same floor; keep it out of the source, in the environment (env['TOKEN_SECRET']). A page keeps sign_in, which needs no token. Both modes sign and verify through the same functions. (Neither had built them: the compiled program named a type the web runtime did not have, and the interpreter had no such builtins.) They are written in Crow (src/modules/auth.co) on the sys layer: the header and the claims as base64url, the signature an HMAC-SHA256 of the two, compared in constant time; the claims reach sign as the JSON text they encode to (Design Note "a builtin written in Crow keeps its row"). Their tests are the module's own (crow test src/modules/auth.co), one of them a token another library signed, read with its secret.
use web.ws)| Function | Description | Example |
|---|---|---|
socket[url] | A connection to a WebSocket server, ws:// or wss:// | Web: responses and submodules |
send[s, text] | Send a text message; answers empty text | The standard library's layer: use sys · Web: responses and submodules |
recv[s] / recv[s, secs] | The next message, as text — waiting for it, or at most secs seconds | Web: responses and submodules |
close[s] | Close the connection, for every value that holds it | Web: responses and submodules |
A socket is a live feed a program reads — a market's trades, a service's events — in a script, a job, or a handler's background work. Each failure is the call's error value, of kind 'socket': a server that does not answer is socket's (cannot open a socket to …), a wait that passes its seconds is recv's (no message in 5 s), and a server that closed answers closed, after which every call says the socket is closed. A binary message reads as text, and a ping is answered and passed over. A socket is one connection however many values hold it — a function handed it, a loop — and close closes it for all of them. Both modes talk through the same client, which is written in Crow (src/modules/ws.co) on the sys layer: the handshake is the protocol's (sys.upgrade), and the framing — a message sent masked, the next read whole across its fragments, a ping answered, a close answered and the connection let go — a formula over send, recv, pack and mask. Its rows keep the contract above in both modes; its tests are the module's own (crow test src/modules/ws.co, the RFC's masked example among them), and tests/socket.rs runs it against a server in both modes.
use web, web.ws
feed = socket['wss://feed.example.com/stocks']
send[feed, '{"action": "subscribe", "params": "T.AAPL"}']
while true
m = recv[feed, 30]
if not m.ok
log['the feed stopped: `m.reason`']
break
log[m]
Design Note: a socket's name. Considered: connect[url], the client's first name, which the database's connect already is — one name two ways, told apart by the URL's scheme; and dial, open and ws, which name a mechanism, a verb many things do, or the protocol's initials. Decided: socket, the thing a program holds, as smtp names a mailer and connect a database; send, recv and close act on it.
| Function | Description | Example |
|---|---|---|
page[path, view] | Add a LiveView page (server-rendered with live updates via WebSocket); :name in path is a fact in its address (Core, above) | Views: the whole element vocabulary · View Blocks · Reactive State and Events · Components · Slow Work and the Live Loop · Testing a Live App · Charts and Styling · The Element Vocabulary · Derivations · Views and Live Updates |
download[value, name] | Hand the reader's browser a file when the handler commits: a frame as CSV, a string as text, a picked file's bytes | Reactive State and Events |
title[text] | The page's title, which the browser's tab and history show: a view places it anywhere in the page, reading the page's state like any element (title['Card card']); a render that changes it changes the tab | Documentation: use doc · Reactive State and Events |
meta[name = 'description', content = text] | A tag in the page's head: its attributes pass through as HTML (property = 'og:title' for a link preview) | Reactive State and Events |
A page's head is what its render places. title and meta take no room on the page: a served page carries them in its head from its first render, and a later render that changes them changes the head — the last title the render places is the page's (a layout's placed before the page's own gives way to it), and a page that places none is titled Crow App. In a test they are elements of the render, where elements[a, 'title'] finds them; the layout engine gives them no box.
Design Note: a page's address and its head. Considered: a path's names as the view's parameters (view card[id]), as Express and Rails hand a route its parameters; and a page's title as an argument of page (page['/card/:card', card, title = 'Card']). Decided: a path's name is a fact. A page's state is its facts and the address already carried facts (url = […]), so one rule holds the path and the query string, a handler that changes the fact moves the address, and nothing new reaches a view. A title is an element the render places, so it reads the page's state (a card's own name) and changes with it, where an argument of page would be text fixed when the page is registered. A route keeps req.params: a route's handler is handed its request, and a page is handed none.
A page and its session. The page request renders the reader's session once, and the page's socket adopts that session — its facts, its derivations and the render already on screen — so a page loads with one render. A session outlives its connection. When the socket closes — a laptop sleeps, the network drops, a rebuild restarts nothing but the page — the server keeps the session for ten minutes, and the page, which holds the session's token for its tab, resumes it on reconnect: the same facts, a replay still where it was, sent its render at once. A waiting session keeps its facts and its derivations' last values, so it comes back without a flash of loading; at most a thousand sessions wait at once. The token is a credential and is treated as one: it travels only over the socket, it is good once, and every resume replaces it, so a copied token resumes nothing after its tab has. Reloading the tab resumes it too, and so does coming back to it with the browser's back button; a second tab is a session of its own. Following a link opens the session of the address it names, even to the page the tab is on: /?day=2 from /?day=1 starts from day = 2 (the tab's session had been resumed, and the address followed it back to day = 1). A socket that brings no live token — a session past its ten minutes, a server that restarted — starts a fresh session from the facts its address carries and is sent its render at once: the page never keeps showing a session the server no longer holds. serve[…, keep = true] keeps sessions across a restart as well: each session's facts are written to .crow/sessions/ as they change, and a tab that comes back to a restarted program finds its session there for the same ten minutes (a kept session had been removed before it was read, and the tab started afresh); a fact whose name or type changed since starts from its declared value, which the log says. The directory ignores itself in version control. While the socket is down the page dims, says reconnecting… and takes no input, so a click is never silently lost. Events that arrive while a render runs are applied together and answered by one render: a slider dragged across its range is one render, not one per step. A render sends what changed, not the page: the server reads the render as the browser builds it — a chart's marks and a table's rows included — compares it with the render the page holds, matching elements by id and then by position as the page does, and sends the changes (a text, an attribute, an element added, gone or replaced) when they are smaller than the page, the whole page when they are not. A moving play-head on a chart is a few attributes, not the drawing again, and a row added to a list whose items carry ids is one row. Each change names the element it expects to find; a page that meets one it cannot place — its shape changed under it — asks for the whole page back, and a field being typed in keeps the reader's draft. Markup the browser would have to repair (a block inside a paragraph, a row outside a table, a tag closed by its parent's end tag) is sent as whole pages, and the server log says which repair, once. Handlers and renders run on worker threads, never on the threads that carry the sockets, so a session's slow work delays only its own page. A clicked element is marked data-crow-busy the moment it is clicked, before the server answers, and the mark clears when the answer to that click arrives — the reader sees the click taken in the same frame, with no program code in the browser.
A page sends only what it was offered. A session accepts a click only when a render it sent carried that handler with exactly those arguments (click = set_speed[10.0] offers set_speed with 10.0, a table row its key), a bound input or select only when a render drew it, and a form only when a render drew it. A helper no element names, a fact no input binds, and the button of a closed modal or a hidden element are refused — a confirmation's second step is accepted only after its first opened it. A refusal changes nothing and is logged: crow live: refused 'name' — the page did not offer it. The socket opens only from the app's own page (a page on another site is refused before it can send anything), a message on it is at most 4 MB — an event is a click or a field's text, and a file comes by a request of its own, under the uploads limit — so a bigger one closes the socket, which the page opens again (the bound had been 64 MB), and a served app listens on this machine only; set CROW_HOST=0.0.0.0 where the network must reach it — a container, a server behind a proxy.
Both modes serve pages. crow run file.co compiles and serves; crow run -i file.co serves from the interpreter at once, with no Rust compile between an edit and the page — the live runtime keeps the sessions (their facts, the commit, the notice, the questions, the address, the reconnects) in both, and only who renders the views and runs the handlers differs. One difference is of timing, not of meaning: the interpreter computes a derivation where it is read, so a page served by it waits for its queries, where the compiled page shows the last value and catches up. crow test --served drives every page in tests/served/, and every app under examples/ with a main.events script beside its main.co (served from a fresh copy, so its database starts empty in each mode), with the same events in both modes and compares the pages they settle on.
use web.mail)| Function | Description | Example |
|---|---|---|
smtp[host, port, user, pass] | Create an SMTP mailer; its messages come from user | The standard library's layer: use sys · Web: responses and submodules · Reactive State and Events |
mail[mailer, to, subject, body] | Send a plain-text message: held to a handler's commit, so a handler that stops sends nothing; r = mail[…] sends at once and r.ok says whether the server took it | Reactive State and Events |
mail is an effect like notice and download: in a handler, or a function a handler calls, a message whose answer is not used waits for the commit, and a stop drops it; used (sent = mail[…]), it is sent when asked, and a failure is the answer's reason. Outside a handler it is sent at once. A view or a derivation sends none, itself or through a function it calls, and is refused if it tries: it runs whenever the page needs it. A mailer is made once for the whole program, as a connection is, and prints as its host, never its password. In a test nothing is sent: mails[a] is the list of what the session sent, each a map of to, subject and body, and crow test --compiled records the compiled program's the same way.
The sending is written in Crow (src/modules/mail.co) on the sys layer, as RFC 5321 says: the greeting, EHLO, the connection secured with STARTTLS where the server offers it (port 465 is TLS from the start; a password travels only encrypted, so a remote server that offers neither is refused — a local relay on the loopback address is not), AUTH PLAIN, the envelope, the message's head (From, To, a Subject beyond ASCII as an encoded word, Date) and its body with its dots stuffed (a body beyond ASCII as base64), QUIT. A recipient the server refuses, an address that is none (name@host), a server that does not answer or answers anything but what the step wants are the reasons (mail to bob@example.com failed: the server answered 550 no such user at RCPT TO). The session layer holds a handler's message to its commit in both modes and hands it to the row's function to send; a script sends it at once. Its tests are the module's own (crow test src/modules/mail.co), and tests/smtp_server.rs runs it against a server in both modes.
use web.client)A program reads a URL with read and load (Files, URLs and S3), sends any other request with request (Requests), and a page pulls remote data with fetch[url] / poll[url, seconds] (reactive, see above). web.client is no module: the client is the language's own, written in Crow.
Design Note: one process. Considered: sessions any process can resume from a shared store, and a table's version shared across processes, so that an app could run on several machines as a Phoenix cluster does. Decided (session-layer-design.md §6; measured 2026-10-02): an app is one process — its sessions, its derivations' values and its tables' versions live in it — and how far one goes is measured, not guessed: a connected tab costs about 140 KB while it is open, a parked session a few kilobytes, and a trivial route answers tens of thousands of requests a second on a laptop (the book, Polish and Deployment, "How far one process goes"; tests/capacity.rs measures it on any machine). An app that outgrows one process runs several behind a proxy that keeps each tab on its process, sharing the database; a write one process makes reaches another's derivations by their fresh window, as an external source's does. Sessions across processes wait for an app that needs them.
Design Note: what web is. Considered: the modules the web framework had gathered — a task queue (web.queue, push), templates (templates, render), an embedded SQLite handle (web.db's sqlite), session stores (sessions[]), Server-Sent Events and WebSocket servers (sse, ws), an LRU cache (cache), generated API docs (web.docs), and CORS, static files, rate limits and uploads as calls on an app value the language no longer has (cors[app], static_dir[app, …], limit[max, window], uploads[req]). Decided (Vijay, 2026-09-30): each was a second way to do what the language already does, or a stand-in the interpreter answered with a placeholder (render gave [rendered: name]), and each is gone. Background work is later[f, …]; a page is a view, not a template; a database is db.connect; a reader's session is a page's session, with sign_in for who they are; a server reaches a page through its live session and a client through stream; what a program computes once and reads often is a derivation. use web serves pages and routes, and its modules are web.auth, web.mail and web.ws; another is refused by name.
Generated by crow test --docs --write from the language specification, section 11 and 13. Edit the spec, not this page.