Small on purpose. ================= .. meta:: :description: Bialet is a small web framework in one executable: an HTTP server, SQLite, and the Wren scripting language. No package manager, no YAML, no build step. :keywords: Bialet, web framework, Wren, SQLite, file-based routing, zero dependencies, server-rendered ---- What you get ------------ One executable does the work: it serves HTTP, opens the SQLite file, and runs your Wren code. There is no stack to assemble around it. .. grid:: 1 2 2 2 :gutter: 2 .. grid-item-card:: :octicon:`package;2em` ^^^ **One executable** Copy the binary to a server and run it. No runtime to install, no ``node_modules``. 3.7 MB on Linux x86_64. .. grid-item-card:: :octicon:`database;2em` ^^^ **SQLite is included** The database is one file. No connection strings, no service to provision, no separate process. WAL mode is one flag. .. grid-item-card:: :octicon:`zap;2em` ^^^ **No build step** Save a ``.wren`` file and refresh the browser. No compiler, no bundler, no watch process. .. grid-item-card:: :octicon:`code;2em` ^^^ **HTML, CSS, and SQL** Server-rendered pages, standard HTML and CSS, and SQL you write yourself. Add JavaScript only if you want it. ---- What it costs ------------- .. admonition:: The tradeoffs :class: note Small means bounded. These are the limits that come with it. - **One worker per process, with instant queueing.** Requests are served sequentially per worker. For parallel concurrency on multi-core servers, spin up multiple local instances behind Caddy or nginx. See :doc:`deployment`. - **HTTP/1.0, no native TLS.** Bialet serves HTTP. Put nginx, Apache, or Caddy in front for HTTPS, HTTP/2, rate limiting, request buffering, and static-asset caching. See :doc:`deployment`. - **Wren, with basic tooling.** Apps are written in Wren. The VS Code and Vim plugins provide syntax highlighting; autocomplete and an LSP are on the roadmap. - **No WebSockets.** Request/response HTTP only. Use polling or an external service for realtime features. ---- Where it fits ------------- .. grid:: 1 2 3 3 :gutter: 2 .. grid-item-card:: :octicon:`beaker;2em` ^^^ **Internal tools and admin panels** One process, one SQLite file, and a few users on one small server. .. grid-item-card:: :octicon:`mortar-board;2em` ^^^ **Prototypes and small sites** One directory to deploy. Add a reverse proxy when the site goes public. .. grid-item-card:: :octicon:`heart;2em` ^^^ **Learning and teaching** HTML, SQL, and one scripting language. Nothing else to install. Not a fit if you need multi-server horizontal scaling, in-process TLS, WebSockets, or a package ecosystem. Bialet does not provide them. See :doc:`why-bialet` for the longer version. .. raw:: html
Read the docs All install options
.. toctree:: :hidden: :maxdepth: 2 :caption: Before You Start self why-bialet html-first installation editor-support wren .. toctree:: :hidden: :maxdepth: 2 :caption: First Steps quickstart getting-started/index .. toctree:: :hidden: :maxdepth: 2 :caption: Data & Storage database file datetime .. toctree:: :hidden: :maxdepth: 2 :caption: User Interfaces template jsx-migration forms markdown errors .. toctree:: :hidden: :maxdepth: 2 :caption: Connecting Things advanced-routing external-modules rest-api http-calls .. toctree:: :hidden: :maxdepth: 2 :caption: Running It configuration usage live-reload security cron tests deployment .. toctree:: :hidden: :maxdepth: 2 :caption: Help & Reference reference examples faq ai-prompt ---- .. raw:: html

Maintained by one developer

Bialet is stable for personal use. There is no commercial support or SLA. Bugs go to GitHub Issues; questions and project showcases go to GitHub Discussions. Planned work is in the roadmap.

Join the discussion →
# Why Bialet? Bialet makes a specific trade-off: **maximum simplicity for a narrow but common set of use cases**. This page explains what that means, when it's a good fit, and how it compares to other tools. ## The Philosophy Most web frameworks optimize for flexibility. They give you middleware stacks, ORM layers, routing DSLs, template engines, and configuration files — all designed to handle any scenario you might encounter. Bialet optimizes for **getting things done on a single server**. It bundles the HTTP server, the database, and the language runtime into one binary. Your app is a directory of `.wren` files and a SQLite database. That's it. If your needs fit within Bialet's scope, you get an experience no other framework can match: zero-config development, single-command deployment, and no dependency management. If your needs grow beyond that scope, you'll want a different tool — and that's okay. ## When to Use Bialet - **Prototypes and MVPs** — Go from idea to working app in minutes. No setup, no boilerplate. - **Internal tools and dashboards** — Admin panels, reporting UIs, data browsers. The database is already there. - **Simple CRUD applications** — Blogs, polls, task trackers, form-based apps. - **Learning web development** — Skip the configuration and focus on HTML, SQL, and basic programming concepts. - **Single-server deployments** — Small to medium traffic sites on a VPS or shared host. ## When NOT to Use Bialet - **High-traffic public sites** — Bialet is a single-process server. It can't distribute load across multiple machines. - **Real-time applications** — No WebSocket or Server-Sent Events support. Polling works for simple cases, but it's not ideal. - **Large teams with existing infrastructure** — If you already have PostgreSQL, Redis, and a CI pipeline, Bialet's simplicity isn't buying you much. - **Microservices** — Bialet is designed for monolithic apps on a single server. - **Complex auth flows** — Bialet supports basic auth and sessions, but not OAuth, SAML, or multi-factor authentication out of the box. ## Comparisons ### Bialet vs. PHP PHP is the closest philosophical relative. Both let you drop files in a directory and serve them immediately. Both embed logic in HTML. | | Bialet | PHP | | --------------- | -------------------------------- | --------------------------------- | | Language | Wren (object-oriented) | PHP | | Database | SQLite built-in, always present | Must install and configure | | Deployment | Single binary + app directory | PHP runtime + web server + DB | | Package manager | None (import from GitHub URLs) | Composer | | Template engine | Inline HTML strings (JSX-like) | Plain PHP or Twig/Blade | | Configuration | None | php.ini, .env, server config | Bialet wins on simplicity. PHP wins on ecosystem and scalability. ### Bialet vs. Flask / Express Flask and Express are minimalist frameworks, but they still need a database driver, a template engine, and often an ORM — all configured separately. | | Bialet | Flask / Express | | --------------- | -------------------------------- | --------------------------------- | | Setup | One binary, zero config | Install packages, configure DB | | Database | SQLite built-in | SQLAlchemy + driver (Flask) | | Routing | File-based | Decorators / router definitions | | Templates | Inline HTML + interpolation | Jinja2 / EJS / Pug | | Production | Single binary | gunicorn + nginx / pm2 + nginx | | Language | Wren | Python / JavaScript | Bialet eliminates the "glue code" phase entirely. Flask/Express give you more control over the stack but require more decisions up front. ### Bialet vs. Rails / Django Rails and Django are full-stack frameworks with ORMs, admin interfaces, migrations, and extensive ecosystems. Bialet is full-stack in a different way: it bundles the essentials, nothing more. | | Bialet | Rails / Django | | --------------- | -------------------------------------------- | ----------------------------------------- | | Learning curve | Hours | Weeks to months | | ORM | Raw SQL with Query objects | ActiveRecord / Django ORM | | Admin panel | Build your own | Built-in (Django admin) / gems | | Migrations | `_migration.wren` file | Versioned migration files | | Scaling | Multiple workers on one host (shared SQLite) | Horizontally via workers / load balancer | | Ecosystem | Minimal | Massive (gems / packages) | If you're building a complex, long-lived application with a team, Rails or Django are better choices. If you're building something simple today, Bialet gets you there faster. ### Bialet vs. Static Site Generators Static site generators (Hugo, Jekyll, Eleventy) produce HTML files at build time. Bialet generates HTML at request time and includes a database. If your site has no dynamic data and no forms, use a static site generator. If you need a database, forms, or user-specific content, Bialet is the lighter alternative to a full backend framework. ## The Trade-off Bialet is intentionally limited. It doesn't have: - Multi-server horizontal scaling - WebSockets - Background job queues (beyond Cron) - A plugin ecosystem - PostgreSQL / MySQL support - An ORM - OAuth or social login What it does have is **everything you need to build a data-driven website in a single binary**. For many projects, that's exactly the right trade-off. # HTML-First Bialet renders on every request. There's no client-side router, no hydration step, no state to keep in sync between server and browser. That model works best when the browser does what the browser already knows how to do, and JavaScript is reserved for the few things it genuinely can't. This isn't a new idea — see [You Might Not Need JS](http://youmightnotneedjs.com/) and [HTML Can Do That](https://chrisburnell.com/html-can-do-that/) for the wider case. This page is the Bialet-flavored version: which native HTML/CSS features replace common JS patterns, and how they look in a `.wren` file. ## Why this fits Bialet specifically Bialet has no bundler and no package manager. Every script tag you add is a file you wrote, by hand, that you now maintain forever — there's no `npm update` to carry it forward. That raises the bar for reaching for JS in the first place: a checkbox hack that ships zero bytes of script beats a hand-rolled toggle function you'll be debugging in a year. It also means the trade Bialet already made — render HTML on the server per request instead of shipping state to the client — pairs naturally with letting the *server* own things like sorting, filtering, and pagination via plain links and query params, instead of re-implementing them in JS on top of a JSON endpoint. None of this is a purity rule. If a feature genuinely needs JS — drag-and-drop, live validation against the server, anything real-time — write the JS. The point is to check whether HTML already does it first. ## Disclosure: `
` / `` FAQ sections, "read more" blocks, and dropdown menus are a disclosure widget. The browser already has one: ```wren return

FAQ

Do I need a database?

No — Bialet works without one, but SQLite is built in if you do.

Can I still use JavaScript?

Yes. Bialet doesn't stop you — it just doesn't make you start there.

``` Style the `` marker and open/closed state with the `::marker` pseudo-element and the `[open]` attribute selector — no click handler required. ## Popovers and menus: the `popover` attribute Tooltips, dropdown menus, and lightweight modals used to be the textbook "you need JS for this" example. The `popover` attribute and `popovertarget` make the browser own the show/hide/light-dismiss/`Esc` behavior: ```wren return ``` Clicking outside, pressing `Esc`, or clicking another `popovertarget` closes it automatically. Check current browser support before relying on it for a feature that must work everywhere. ## Tabs and toggles: the checkbox/radio hack Radio buttons already track "which one is selected" — that's a tab strip. Pair hidden inputs with CSS `:checked` and a sibling or `:has()` selector instead of a JS tab controller: ```wren return
Overview content
Settings content
``` ```css .panel-1, .panel-2 { display: none; } #tab-1:checked ~ .panel-1, #tab-2:checked ~ .panel-2 { display: block; } ``` The same hack covers mobile nav toggles, dark-mode switches (without persistence — that part needs a cookie or a tiny script), and show/hide password fields via `` and `:has()`. ## Closing dialogs without JS Opening a `` as a true modal (`.showModal()`) still needs one line of JS — that's an honest limit, not a workaround to hide. But *closing* it doesn't: ```wren return

Delete this post?

``` `
` closes the nearest `` and sets `returnValue` to the button that submitted — no `close()` call needed on the way out. ## Native form validation `required`, `pattern`, `minlength`, `maxlength`, `min`, `max`, and typed inputs (`type="email"`, `type="url"`, `type="number"`) validate in the browser before the request is even sent, and `:invalid` / `:user-invalid` in CSS style the bad state: ```wren return ``` This is client-side convenience only — always re-validate on the server, the same way you already guard `Request.post()` against `null`. See [Forms](forms.md) for the server-side half of this pattern. ## Autocomplete: `` An autocomplete field backed by your database doesn't need a JS keystroke handler and a fetch call — render the options server-side into a ``: ```wren var tags = `SELECT DISTINCT tag FROM posts`.fetch return
{{ tags.map { |t| } }}
``` ## Sorting, filtering, and pagination: let the server do it This is the pattern Bialet is built for. Instead of shipping a sort function to the client, make sorting a link that re-requests the page with a different query param — the server already re-renders on every request, so there's no extra round trip to justify a JS version: ```wren var sort = Request.get("sort") || "created_at" var allowed = ["created_at", "title", "views"] if (!allowed.contains(sort)) sort = "created_at" var posts = `SELECT * FROM posts ORDER BY {{ sort }} DESC`.fetch return
    {{ posts.map { |p|
  • {{ p["title"] }}
  • } }}
``` Note the sort column is checked against an allow-list before reaching the SQL string — interpolating a raw query param into `ORDER BY` is a SQL injection hole, allow-list or reject it. See [Security](security.md). Pagination and filtering follow the same shape: read query params, adjust the SQL, render links to the next state. It's slower to click through than a client-side re-sort, but it works with JS off, needs no state management, and is indexable by search engines. ## When to actually reach for JS HTML and CSS run out for anything that needs to react to *time* or to the *server* without a full page load: live search-as-you-type, drag-and-drop reordering, optimistic UI, WebSocket-driven updates. Write the JS for those — a single small ` } ``` Then use utility classes directly: ```wren

Dashboard

Hello World

``` The CDN is fine for tinkering, but it renders styles in the browser at runtime and adds an external dependency. For production, self-host the compiled CSS with the Tailwind CLI. #### Self-hosting with the Tailwind CLI This works because Tailwind scans your files as plain text for class names. The classes in your `.wren` files are standard HTML attributes, so no special configuration is needed to detect them. **1. Install the CLI.** In your app directory: ```bash npm install -D tailwindcss @tailwindcss/cli ``` **2. Create an input stylesheet.** This is the source file Tailwind reads. Put it in a `_`-prefixed directory so Bialet never serves it (see the pitfalls below) but Tailwind can still read it from disk. Create `_src/input.css`: ```css @import "tailwindcss"; @source "./**/*.wren"; ``` The `@source` directive tells Tailwind to scan your `.wren` files for classes. Adjust the glob to match your app's layout if you keep pages in subdirectories. **3. Link the compiled output.** Reference the built file in your template's `head`. Bialet serves it as a normal static file: ```wren head { {{ _title }} } ``` #### Development: watch mode Run Tailwind and Bialet side by side, in two terminals: ```bash # Terminal 1 — start Bialet bialet . # Terminal 2 — compile CSS on every change npx @tailwindcss/cli -i _src/input.css -o style.css --watch ``` With [live reload](live-reload.md) enabled, Bialet watches the whole app directory. Every time Tailwind writes a new `style.css`, the browser reloads automatically — a full Tailwind + Bialet dev loop with no extra tooling. #### Production: one-shot build Compile once, minified: ```bash npx @tailwindcss/cli -i _src/input.css -o style.css --minify ``` Commit `style.css` and deploy it alongside your `.wren` files. The server needs no Node.js — the compiled file is just a static asset. For caching and gzip on the compiled CSS, see [Deployment](deployment.md). ### Pitfalls **No CSS preprocessors built in.** Bialet doesn't bundle Sass, Less, or PostCSS. Write vanilla CSS, or use a separate build step (like Tailwind's CLI) for preprocessing. **`_`-prefixed CSS files are not served.** Files starting with `_` or `.` return 403. Name your stylesheets without a leading underscore: ```wren // ✓ Served // ✗ Returns 403 ``` This is why the input file lives in `_src/` — Bialet blocks HTTP access to it while the CLI still reads it from disk. The output file (`style.css`) must not start with `_` or `.`. **`node_modules` is publicly served.** Everything inside the app root is served unless a path component starts with `_` or `.`. After `npm install`, the `node_modules/` directory is reachable over HTTP. Block it at your reverse proxy in production (see [Deployment](deployment.md)), or run the npm project outside the app root and point the CLI at it. **Dynamic class names are not compiled.** Tailwind scans your source as plain text, so it cannot see classes assembled at runtime. This compiles fine: ```wren class="text-red-600" // ✓ ``` This does not — Tailwind never sees the full class name: ```wren class="text-{{ error ? "red" : "green" }}-600" // ✗ ``` Use complete class names in your `.wren` files. **Framework CDNs add an external dependency.** For production, compile and serve the CSS from your app directory instead of loading it from a CDN. --- ## Escaping & Security The `{{ }}` interpolation **escapes HTML by default**. Plain values — strings from user input, database queries, or URL parameters — have `&`, `<`, `>`, `"`, and `'` replaced with their HTML entities before they reach the page. ```wren var userInput = "" // Safe — HTML characters are escaped automatically var safe =

{{ userInput }}

// Renders:

<script>alert('xss')</script>

``` You can also use `Util.htmlEscape()` for manual escaping outside of interpolation: ```wren var escaped = Util.htmlEscape(userInput) ``` Markup that is already safe is left untouched: HTML string literals, the result of a nested `{{ }}` block, `HtmlNode` values, and `String.raw`. See [Security](security.md) for the full rules. > ⚠️ Pitfall: **Do not add `.safe` inside `{{ }}`.** Interpolation already > escapes, so `{{ userInput.safe }}` escapes the text twice (`&lt;`). > Use `HtmlNode` or `.raw` only for markup you intentionally trust. --- ## Common Pitfalls - **Opening-tag nesting:** The outermost tag of an inline HTML string cannot appear again as a child at any level. This means `
...
` fails because `
` is both the outermost and a nested tag. Wrap with a different tag (e.g. `
`) and nest freely below it: `
...
`. - **Mismatched tags are not validated:** Only the outer closing tag is matched; `
Hello
` compiles and is served verbatim (the browser auto-closes the ``). Write well-formed HTML yourself. - **Invalid tag names:** Tag names must be lowercase, start with a letter, and contain only letters, numbers, and hyphens — no underscores or uppercase. Hyphens enable custom elements like ``. Use classes and semantic tags otherwise. - **Interpolation depth:** Maximum 9 nested `{{ }}` levels. - **Forgetting `return`:** Without `return`, the response body is empty. - **`_`/`.`-prefixed files:** Private, return 403 if accessed directly. - **Newlines inside `{{ }}`:** The Wren expression can span lines, but an infix operator must end its line — a leading operator is a parse error. - **Map callback is a single expression:** The body must fit on the line after the `{`, otherwise the block returns `null` and renders empty. - **Empty lists in `map`:** Produce empty output — pair with `&&` for empty states. - **`return` terminates immediately:** Code after `return` never executes. - **DB values are strings:** Convert with `Num.fromString()` before math. - **Forgetting `return` before `Response.redirect()`:** The code after it still runs and attaches a body to the 302 — no error, just a redirect with an unexpected body. Always `return Response.redirect(...)`. - **Instance vs static methods:** Instance methods need `new()`; static methods don't. - **`_`-prefixed CSS is not served:** Name stylesheets without leading `_`. - **CSS preprocessors not built in:** Write vanilla CSS or use a build step. - **Framework CDNs in production:** Self-host CSS files for reliability. - **`{{ }}` escapes by default:** Plain values are auto-escaped; do not add `.safe` inside `{{ }}` (it double-escapes). Use `HtmlNode` or `.raw` for intentionally-raw markup. # JSX to Bialet Migration You are moving a React/JSX codebase to Bialet. The good news: inline HTML strings look a lot like JSX. The bad news: the surface similarity hides hard limits that will bite you on the first nested list. This page is the cheatsheet — the five differences that turn JSX code into Bialet code. The short version: inline HTML strings are **plain strings**, not a component tree. No props, no children, no expressions-in-markup outside `{{ }}`, and no `dangerouslySetInnerHTML`. --- ## 1. Tag Nesting Limitations JSX lets you nest any element inside any element. Bialet has one hard rule: **The outermost tag of an inline HTML string must not appear again as a direct child tag at any nesting level.** The parser checks the whole string, not just the first level, and reports `Cannot nest
inside
` on the offending line. ```wren // WRONG —
is the outermost tag AND a child var bad =
Hello
var alsoBad =
Nested
// CORRECT — open with a different tag, then nest freely var good =
Hello
// CORRECT — once the tree starts with a different tag, the // original tag can appear at any depth below it var fine =
Deeply nested same tags work here
``` **Pitfall:** this bites the common JSX pattern of a wrapper `
` inside a `
` (row, then column). If you need the same tag twice, change the outermost wrapper or extract the inner one into a component method (see section 4). Two more parser quirks to unlearn from JSX: - **Mismatched closing tags are not validated.** Only the outermost closing tag is matched. `
Hello
` compiles and is served verbatim — the browser auto-closes the `` at render time. Write well-formed HTML yourself; there is no compiler checking your balance. - **Tag names are restricted.** Lowercase, start with a letter, only letters, numbers, and hyphens. `` and `` are compiler errors. PascalCase component names are a JSX thing — see section 4. --- ## 2. Map Callback Restrictions In JSX you write `{items.map(item =>
  • {item}
  • )}` and the arrow function can hold any logic. In Bialet the `map` callback is a **single Wren expression** — the inline HTML string itself: ```wren var items = ["Apple", "Banana", "Cherry"] var html =
      {{ items.map { |item|
    • {{ item }}
    • } }}
    // Output:
    • Apple
    • Banana
    • Cherry
    ``` The two restrictions that will trip you up: ### No statements inside the callback You cannot declare a variable or write multiple lines of Wren inside `map`: ```wren // WRONG — multiple statements inside the map callback {{ tasks.map { |task| var icon = task.finished ? "✓" : "○"
  • {{ icon }} {{ task.description }}
  • } }} ``` If you need pre-computed values, compute them **before** `map`, or add a computed property to your model class: ```wren var tasks = Tasks.list().map { |t| t.withIcon } // pre-process the list return
      {{ tasks.map { |t|
    • {{ t.icon }} {{ t.description }}
    • } }}
    ``` ### The body must start on the line after `{` A multi-line block whose body is not a single expression on the `{` line returns `null` silently — the output is empty and nothing tells you why: ```wren // WRONG — the callback body breaks onto its own line, returns null, renders empty return
    {{ items.map { |item|
  • {{ item }}
  • } }}
    // CORRECT — the expression starts on the {{ line; the HTML string // itself may span many lines return
    {{ items.map { |item|
  • {{ item.description }}
  • } }}
    ``` ### Empty lists render empty `map` over an empty list produces nothing. In JSX you'd conditionally render a fallback; in Bialet you pair `map` with `&&`: ```wren return
      {{ tasks.map { |task|
    • {{ task.description }}
    • } }}
    {{ tasks.count == 0 &&

    No tasks yet.

    }}
    ``` --- ## 3. Interpolation Rules JSX interpolates with `{expr}`. Bialet uses `{{ expr }}` — the extra brace is what makes it valid Wren inside an HTML string. The rules differ more than the syntax: ### `{{ }}` is the only interpolation point Interpolation happens only inside `{{ }}`. A bare `{expr}` is not a template expression — inside an HTML string it is literal text, and outside one it is Wren map syntax. ### Values are escaped by default This is the biggest safety win over JSX (where you must remember `dangerouslySetInnerHTML`). Plain values — user input, database rows, URL parameters — are HTML-escaped automatically: ```wren var userInput = "" var safe =

    {{ userInput }}

    // Renders:

    <script>alert('xss')</script>

    ``` ### Line breaks follow Wren, not JSX A `{{ }}` expression can span lines, but an infix operator must end its line. A leading operator is a parse error: ```wren // CORRECT — operators end each line return
    {{ showClear &&
    }}
    // WRONG — the operator starts a line return
    {{ showClear &&
    }}
    ``` ### Interpolation depth is limited Nesting is capped at 9 levels of `{{ }}`. Deeply nested JSX interpolations must be flattened. ### No expressions in string values JSX lets you write `className={`btn ${active ? 'on' : ''}`}` — a template literal inside an attribute. Bialet does not support string interpolation into attribute values. Compute the string first, then interpolate the result: ```wren // WRONG — no template literals ... // CORRECT — compute the class, then interpolate it var cls = "btn " + (active ? "on" : "") ... ``` For simple two-way choices, the ternary inside `{{ }}` is fine: ```wren ``` - `Session.csrf` returns a `` field. Put it inside every form that changes state. - `Session.csrfOk` returns `true` when the submitted token matches the stored one. Check it on every POST (and PUT/DELETE via `Request.method`). > ⚠️ Pitfall: the session cookie is `SameSite=Lax`, which already stops most > cross-site POSTs. Do not treat that as enough. SameSite is advisory > (older browsers ignore it) and it does nothing for same-site subdomain > attacks. Keep the explicit token check on any form that writes data. ## Sessions & Cookies Cookies set through `Cookie.set` get secure defaults: | Attribute | Default | Notes | |-------------|----------- |--------------------------------------------| | `Path` | `/` | | | `HttpOnly` | `true` | Not readable from JavaScript | | `SameSite` | `Lax` | Blocks most cross-site cookie sending | | `Secure` | auto | Added when the request arrives over HTTPS | You can override any of these by passing an options map, for example `Cookie.set("pref", "dark", {"SameSite": "Strict"})`. Be careful overriding `HttpOnly` or `Secure` — both are there for a reason. Sessions are stored in SQLite in the `BIALET_SESSION` table. The session cookie (`BIALETSESSID` by default) holds only a 40-character random ID; all data lives server-side. Clear a session with `Session.destroy()`. The table is keyed on `(id, key)`: `Session.set` replaces the row for a key instead of appending, and `Session.get` always returns the latest value written. Older databases that lack the primary key are rebuilt in place on startup. > ⚠️ Pitfall: `Request.post(name)` returns `null` when the key is missing. > Calling string methods on it crashes the request. Always guard with `|| ""` > or a null check: > ```wren > var msg = Request.post("msg") || "" > ``` Cookie names and values are validated (`Util.cookieToken`, `Util.cookieValue`) and reject characters that could break out of the `Set-Cookie` header, so a user-controlled value cannot inject headers. ## Password Storage Use `Util.hash` and `Util.verify` instead of rolling your own: ```wren var stored = Util.hash(password) // store this in the database var ok = Util.verify(password, stored) // true if the password matches ``` `hash` produces a salted SHA-256 digest in `hash$salt` format. The salt is random per password, so identical passwords produce different stored values, and the stored format is all you need to keep — `verify` parses it back out. Be honest about the limits: this is salted SHA-256, a fast hash. It is fine for internal tools and low-stakes apps. For user-facing accounts that face real attackers, consider a deliberately slow KDF (bcrypt, scrypt, argon2) implemented at the app or proxy layer. ## Secure Configuration & Deployment ### TLS Bialet speaks **HTTP/1.0** and has no native HTTPS. Run it behind a reverse proxy (nginx, Apache, or Caddy) for TLS. This is the recommended production setup, and it also lets you bind Bialet to `127.0.0.1` so only the proxy talks to it. See [Deployment](deployment.md) for configs. When requests arrive through a proxy, Bialet detects HTTPS from the `X-Forwarded-Proto` / `Forwarded` headers. That is how the `Secure` cookie attribute is decided. Only set those headers from a proxy you trust — never forward user-supplied ones unchanged. ### Reverse Proxy Hardening Each Bialet worker serves one connection at a time in a blocking loop, so a single slow or hostile client delays every request behind it. The reverse proxy is the layer that protects it from hostile clients; run several workers behind it when you need more concurrency (see [Deployment](deployment.md#scaling-multiple-workers-on-one-host)). Configure the proxy to: - **Cap the request body.** Bialet rejects bodies over its own cap with `413` before parsing: the cap is the smaller of `-b` (default 128 KB) and a memory-safe ceiling set by `-m` (soft limit / 512). Keep these defaults for forms that send only fields; raise them only when your app genuinely accepts large uploads, and raise `-u` (the per-file upload cap) alongside — the effective upload limit is the smaller of `-u` and the body cap minus ~8 KB of multipart framing. At the proxy, set `client_max_body_size` (nginx), `LimitRequestBody` (Apache), or an equivalent to match your app's real upload limit — a cap *above* it just lets oversized bodies reach the framework's own check, and a cap *below* it breaks legitimate uploads. - **Enforce a total body-read deadline.** Bialet already bounds the header block at 5 seconds and the body at 30 seconds, with each `recv()` capped at 5 seconds, then drops the connection. Set a proxy-side read timeout as well so stalled uploads are rejected before they reach the worker (`client_body_timeout` in nginx, `RequestReadTimeout ... MinRate` in Apache). - **Buffer the full request before forwarding** (`proxy_request_buffering on` in nginx; the default in most proxies). This keeps a slow client from holding the upstream socket. - **Deny private files at the proxy too.** Bialet already returns 403 for `_`/`.`-prefixed paths, but the proxy can return 403 first — a second layer in case an app or a planted symlink ever serves such a file. See [Deployment](deployment.md) for ready-to-paste nginx, Apache, and Caddy configs with these settings. > ⚠️ Pitfall: a proxy body cap that is *higher* than your app's needs > re-opens the CPU-exhaustion and memory-exhaustion risks above. Keep it as > low as your uploads actually require. ### Private Files Files and directories whose name starts with `_` or `.` are forbidden from direct HTTP access — the server returns 403. This is what protects `_app.wren`, `_migration.wren`, `_db.sqlite3`, and your configuration from being downloaded. Name anything private with a leading `_` or `.`. The single exception is the RFC 8615 `.well-known` namespace: a URL whose first segment is exactly `.well-known` is public, and everything below it is served like any other file. That is required by ACME/Let's Encrypt, OAuth 2.0 and OpenID Connect, WebFinger, app links, and `security.txt`. The exemption is case-sensitive, applies only as the first path segment (`/foo/.well-known/x` is still 403), and does not survive path traversal (`/.well-known/../.env` is still 403). See [Advanced Routing](#the-well-known-exception). Never place secrets under `.well-known` — it is world-readable by definition. > ⚠️ Pitfall: `_db.sqlite3` contains your data and your session table. > It is already blocked from HTTP access, but the app directory on disk is > not a sandbox. Keep it out of version control backups you share, and > restrict filesystem access to the user that runs the server. ### Database File Permissions Bialet creates `_db.sqlite3` with SQLite's default mode, `0666 & ~umask`. Under the common `umask 022` that yields a world-readable `0644` file, so any local user on a shared host can read the whole database — uploaded files, password hashes, and cached remote modules included. The server does not tighten the mode after opening the file. Run Bialet with a restrictive umask so the file is created `0600`: ```bash umask 0077 bialet -p 7001 /www/myapp ``` Under systemd, set `UMask=` on the service instead: ```ini [Service] UMask=0077 ``` If the database already exists, tighten it with `chmod`. Redo this after any migration or restore that recreates the file: ```bash chmod 600 /www/myapp/_db.sqlite3 ``` > ⚠️ Pitfall: a world-readable `_db.sqlite3` is as exposed as a leaked > backup — no HTTP request is needed to read it. Restrict the app root to > the user that runs the server and keep the file out of shared backups. ### Resource Limits The server can enforce memory and CPU ceilings per app with CLI flags, which limits the blast radius of a runaway script or a memory-exhaustion attack: ```bash bialet -p 7001 -m 1024 -M 2048 -c 25 -C 50 /www/myapp ``` - `-m` / `-M` — soft / hard memory limit in MB - `-c` / `-C` — soft / hard CPU limit in percent - `-b` — max request body in KB (default 128 KB) Request bodies are rejected with `413 Payload Too Large` before parsing when they exceed the smaller of the `-b` limit and a memory-safe ceiling (`-m` / 512, about 256 KB at the default 128 MB soft limit). This keeps worst-case body parsing (one Wren string per line) inside the enforced `RLIMIT_AS` budget, so a crafted body cannot crash the server child or stall it for minutes. See the resource limits table in [Deployment](deployment.md) for defaults. ### Other Defaults - SQLite `PRAGMA foreign_keys` is **on** by default, so orphaned rows cannot accumulate through a careless delete. - File uploads are stored inside the SQLite database, not on disk — they are never served from a writable directory. See the `File` class in the [Reference](reference.md). - Validate user input before trusting it: check types and ranges, convert with `Num.fromString()`, and reject values you did not expect. Do this in your domain classes before saving, and again in the form handler. ## Security Checklist 1. Interpolate untrusted values with `{{ }}` and rely on the automatic escaping; mark intentional markup with `HtmlNode` or `.raw`. 2. Never build SQL from strings — use `?` placeholders and pass values as parameters. 3. Use `.order()` with an allow-list for sortable columns. 4. Put `{{ session.csrf }}` in every state-changing form and check `session.csrfOk` on POST. 5. Guard `Request.post(...)` with `|| ""` before string operations. 6. Store passwords with `Util.hash` / `Util.verify`, never plaintext. 7. Run behind a reverse proxy with TLS; bind Bialet to `127.0.0.1`. 8. Cap the request body and set a total read deadline at the proxy; buffer the full body before forwarding. 9. Keep private files (`_`-prefixed) out of shared backups and repositories. 10. Use `-m` / `-M` / `-c` / `-C` to cap memory and CPU. 11. Validate and convert input with `Num.fromString()` before numeric math. # Cron Tasks Bialet allows you to run scheduled code by defining a cron job in a Wren file. This is useful for running periodic operations like cleaning old records, sending emails, or syncing external data. The cron file can be in the root and be called `_cron.wren` or be inside the `_app` folder, `_app/cron.wren`. ## Basic Usage To schedule a task, use the `Cron.every` or `Cron.at` helpers methods inside the Wren cron file. These methods receive a block that is called at the specified time. ```wren import "/_app/domain" for Task // Every 2 minutes Cron.every(2) { |date| System.log("Hello, from Cron!") } // At 2:00 AM Cron.at(2, 0) { |date| Task.clearAll() } ``` The `|date|` argument is the current [Date object](datetime) used for evaluation. You can ignore it if not needed. ### Helper methods #### `Cron.every(minutes)` Runs the job when the current minute is divisible by `minutes`. ```wren Cron.every(10) { |d| System.log("Runs every 10 minutes") } ``` > **Important:** `Cron.every(n)` uses divisibility, not elapsed time. > `Cron.every(7)` fires at minutes 0, 7, 14, 21, 28, 35, 42, 49, 56, > then skips 4 minutes to 0 again. `Cron.every(90)` **never** fires because > 90 does not divide 60. Use these safe values: 1, 2, 3, 4, 5, 6, 10, 12, > 15, 20, 30. #### `Cron.at(hour, minute, dayOfWeek)` Runs the job when the current time matches the given `hour` and `minute`. ```wren Cron.at(3, 15) { |d| System.log("Runs at 03:15 every day") } ``` Runs the job when the current time matches the given hour, minute, and day of week. Days of week: 0 (Sunday) to 6 (Saturday) ```wren Cron.at(4, 30, 1) { |d| System.log("Runs at 04:30 every Monday") } ``` ## Custom Rules Each job receives a `Date` object of the current time, which can be used for custom rules. ```wren Cron.every(1) { |d| if (d.day == 1 && d.hour == 0) { System.log("It's the first of the month!") } } ``` # Testing in Bialet Bialet includes a built-in testing framework for HTTP endpoint testing. Tests are written in Wren and can verify responses from your application endpoints. ## Running Tests Use the `-T` flag to run all tests: ```bash bialet -T # Run all tests in _tests/ folder bialet -T docs/examples # Run tests in specific directory bialet -T -q # Quiet mode: no colors, minimal output (for CI) ``` ## Test Directory Structure Tests are located in the `_tests/` folder within your application: ``` myapp/ ├── _app.wren ├── _migration.wren ├── hi.wren ├── _tests/ │ ├── _init.wren # Optional: runs before test files │ ├── hi.wren # Tests for hi.wren │ └── password.wren # Tests for password.wren ``` **Important:** The `_tests/` folder name starts with `_` to prevent direct HTTP access, following Bialet's convention for private files. ## Test Initialization ### `_tests/_init.wren` (Optional) If present, this file runs once at the start of each test run (before any test files execute). Use it for common setup: ```wren // _tests/_init.wren System.log("Setting up test environment") // Insert test data `INSERT INTO users (name, email) VALUES (?, ?)`.query("Test User", "test@example.com") ``` ## Skipping Tests Call `Tests.skip()` as a test file's first statement to mark it skipped instead of run. Skipped tests count separately from passed/failed and never affect the exit code: ```wren // _tests/maintenance.wren Tests.skip() ``` `Tests.skip()` doesn't abort the fiber, so any code after it in the same file still executes -- put it first if the rest of the file should not run. ## Writing Tests Tests use the `Test` class with a fluent API: ```wren // Basic GET request test Test.get("/hi").status(200).contains("Hello World") // POST request test with form data Test.post("/password", {"password":"123", "password-check":"123"}) .status(200) .contains("The passwords are the same") ``` ## Test API Reference ### HTTP Request Methods | Method | Description | |--------|-------------| | `Test.get(route)` | Send GET request | | `Test.post(route, params)` | Send POST request with form data | | `Test.apiGet(route)` | Send GET with JSON Content-Type header | | `Test.apiPost(route, params)` | Send POST with JSON body | | `Test.apiPut(route, params)` | Send PUT with JSON body | | `Test.apiDelete(route)` | Send DELETE request | ### Request Configuration Chain these before assertions: ```wren Test.get("/api/user") .setHeader("Authorization", "Bearer token123") // Add custom header .method("GET") // Explicitly set method .postData({"key": "value"}) // Set POST data ``` ### Assertions Chain these after the request method: | Assertion | Description | |-----------|-------------| | `.status(code)` | Assert HTTP status code | | `.contains(str)` | Assert body contains string | | `.equals(str)` | Assert body exactly equals string | | `.notContains(str)` | Assert body doesn't contain string | | `.header(name)` | Assert header exists | | `.header(name, value)` | Assert header equals value | | `.json()` | Return parsed JSON body (Map or List) | | `.jsonContains(key)` | Assert JSON object has key | | `.jsonEquals(key, value)` | Assert JSON key equals value | | `.jsonNotContains(key)` | Assert JSON object doesn't have key | | `.assert(condition, message)` | Custom assertion with message | **Note:** Assertions are lazy - the request is executed when the first assertion is called. ### Body Assertions ```wren // Exact match Test.get("/health").status(200).equals("OK") // Contains substring Test.get("/page").contains("Welcome") // Negative assertion (security checks) Test.get("/public").notContains("password") // Multiple assertions chained Test.get("/api") .status(200) .contains("success") .notContains("error") ``` ### JSON Assertions For API endpoints that return JSON: ```wren // Parse JSON body for custom validation var data = Test.apiGet("/users").status(200).json() Test.assert(data.count > 0, "Expected at least one user") // Fluent JSON assertions (from _tests/api-user.wren) Test.apiGet("/api-user") .status(200) .jsonContains("id") .jsonContains("name") .jsonContains("email") .jsonEquals("id", 1) .jsonEquals("name", "Test User") .jsonEquals("active", true) .jsonNotContains("password") .jsonNotContains("ssn") ``` ### Two Approaches to JSON Testing **Approach 1: Fluent JSON methods** (best for simple checks) ```wren // From _tests/api-user.wren Test.apiGet("/api-user") .jsonContains("id") .jsonEquals("name", "Test User") .jsonNotContains("password") ``` **Approach 2: Parse JSON + custom logic** (best for complex validation) ```wren // From _tests/json.wren var counters = Test.apiGet("/json").json() Test.assert(counters.count >= 2, "Expected at least 2 counters") for (counter in counters) { if (counter["name"] == "test1") { Test.assert(counter["value"] == "10", "Expected value '10'") } } ``` ### Multiple Assertions Chain multiple assertions on a single request: ```wren Test.get("/api/users") .status(200) .header("Content-Type", "application/json") .contains("user@example.com") ``` ### Multiple Tests in One File Write multiple independent tests in a single file: ```wren // _tests/api.wren // Test 1: List users Test.get("/api/users").status(200).contains("users") // Test 2: Create user Test.apiPost("/api/users", {"name": "John"}) .status(201) .contains("John") // Test 3: Invalid request Test.post("/api/users", {}) .status(400) .contains("name is required") ``` ## API Testing Examples ### JSON Response Testing ```wren // Test JSON endpoint with json() and assert() (from _tests/json.wren) // Shows parsing JSON and custom validation logic // Parse JSON response var data = Test.apiGet("/json").status(200).json() Test.assert(data is List, "Expected JSON array") // Insert test data `INSERT OR REPLACE INTO counter (name, value) VALUES ('test1', 10)`.query() // Complex validation with parsed JSON var counters = Test.apiGet("/json").status(200).json() Test.assert(counters.count >= 2, "Expected at least 2 counters") // Iterate and validate specific values for (counter in counters) { if (counter["name"] == "test1") { Test.assert(counter["value"] == "10", "Expected value '10'") } } ``` ### Multiple Test Patterns ```wren // From _tests/hi.wren - different assertion styles // Simple contains check Test.get("/hi").status(200).contains("Hello World") // Exact match Test.get("/hi") .status(200) .equals("

    👋 Hello World

    ") // Multiple chained assertions Test.get("/hi") .status(200) .contains("Hello") .contains("World") .notContains("Goodbye") ``` ### Security and Negative Assertions ```wren // From _tests/password.wren - test what should NOT be present Test.post("/password", {"password": "abc", "password-check": "xyz"}) .status(200) .notContains("The passwords are the same") // Should not contain success message // From _tests/assertions.wren Test.get("/hi") .status(200) .notContains("Error") .notContains("Failed") ``` ### Form Testing ```wren // From _tests/password.wren Test.post("/password", {"password": "123", "password-check": "123"}) .status(200) .contains("The passwords are the same") Test.post("/password", {"password": "123", "password-check": "321"}) .status(200) .contains("The passwords are different") ``` ### Complete Test Examples ```wren // From _tests/assertions.wren - comprehensive testing Test.get("/hi") .status(200) .equals("

    👋 Hello World

    ") Test.post("/password", {"password": "test123", "password-check": "test123"}) .status(200) .contains("The passwords are the same") .contains("Hash:") ``` ## Best Practices ### Use Appropriate Assertion Methods ```wren // ✅ Good - specific assertions Test.get("/health").equals("OK") Test.apiGet("/users").jsonContains("id") // ❌ Avoid - too generic Test.get("/health").contains("OK") // Could match "NOT OK" ``` ### Security Testing Always check that sensitive data isn't exposed: ```wren // Ensure passwords/keys not in responses Test.get("/public/user") .notContains("password") .jsonNotContains("ssn") .jsonNotContains("api_key") ``` ### Use Custom Assertions for Complex Logic ```wren // Instead of manual Fiber.abort() var data = Test.apiGet("/stats").json() Test.assert(data["active"] > data["inactive"], "More active expected") Test.assert(data["total"] == data["active"] + data["inactive"], "Count mismatch") ``` ### Chain Related Assertions ```wren // Group related checks together Test.apiPost("/users", {"name": "Alice"}) .status(201) .jsonContains("id") .jsonContains("createdAt") .jsonEquals("name", "Alice") .jsonEquals("active", true) ``` ### Test Both Success and Failure Cases ```wren // Success case Test.post("/login", {"user": "admin", "pass": "correct"}) .status(200) .contains("Welcome") // Failure case Test.post("/login", {"user": "admin", "pass": "wrong"}) .status(401) .contains("Invalid credentials") ``` ## Database Isolation Tests run against a **temporary database file** that is created fresh for each test run: 1. A temporary database file is created 2. Migrations are automatically run (`_migration.wren` or `_app/migration.wren`) 3. `_tests/_init.wren` runs (if exists) 4. Test files execute 5. Temporary database is deleted This ensures tests don't pollute your development or production database. ## Complete Example **Application file:** `hi.wren` ```wren return

    👋 Hello World

    ``` **Test file:** `_tests/hi.wren` ```wren Test.get("/hi") .status(200) .contains("Hello World") ``` **Run tests:** ```bash $ bialet -T Running tests in _tests... ✓ hi.wren (5ms) Summary: Total Tests: 1 Passed Tests: 1 Failed Tests: 0 Skipped Tests: 0 Elapsed Time: 5ms ``` A failing assertion reports the file and the `Fiber.abort` message under it: ```bash $ bialet -T Running tests in _tests... ✗ hi.wren Expected code to be 200 but was 500 Summary: Total Tests: 1 Passed Tests: 0 Failed Tests: 1 Skipped Tests: 0 Elapsed Time: 20ms ``` ## Exit Codes - `0`: All tests passed - `1`: One or more tests failed This makes it easy to integrate with CI/CD pipelines. `-q` drops the colors and per-test lines down to a single summary line plus a `### FAIL` block per failure -- friendlier for CI logs: ```bash $ bialet -T -q 1 of 1 tests failed in 20ms ### FAIL _tests/hi.wren:2 - hi Expected code to be 200 but was 500 ``` ```yaml # GitHub Actions example - name: Run tests run: bialet -T -q ``` ## How It Works 1. **Request Building**: The `Test` class builds an HTTP request (method, headers, body) 2. **Internal Routing**: Instead of making actual HTTP calls, the test runner directly invokes the Wren handler 3. **Response Capture**: The response (status, headers, body) is captured and wrapped 4. **Assertions**: Each assertion checks the response and reports failures 5. **Database**: All database operations happen on a temp DB, isolated from your main data This approach is fast (no network overhead), reliable (no port conflicts), and safe (isolated database). # Deployment Bialet is a single binary — copy it to your server alongside your `.wren` files and you're deployed. This guide covers setting up a production-ready environment with a reverse proxy, systemd, and Docker. ## Why a Reverse Proxy? Bialet speaks **HTTP/1.0** and does not support HTTPS natively. Running it behind a reverse proxy (nginx, Apache, or Caddy) gives you: - **TLS termination** — HTTPS for your users - **HTTP/1.1 or HTTP/2** between the client and the proxy - **Static file caching and gzip** — the proxy can compress and cache assets - **Security** — Bialet only needs to listen on `127.0.0.1`, not the public internet You *can* expose Bialet directly with `-h 0.0.0.0`, but this is not recommended for production. Bialet is **single-threaded per worker**: each process accepts and serves one connection at a time, in a blocking loop. A request that arrives while the worker is busy waits in the kernel's listen queue, so its latency is the duration of the request in front of it. That is milliseconds for typical CRUD pages, and the built-in header/body deadlines stop a stalled client from holding the worker forever. For more concurrency, run several workers behind the proxy (see [Scaling](#scaling-multiple-workers-on-one-host)). The worker's listen backlog is 10, so a burst larger than that can be refused before the worker ever accepts it. The proxy holds the burst in its own event loop and feeds the worker only as it frees up — one more reason not to expose Bialet directly. The proxy is therefore not just for TLS — it is the layer that absorbs slow clients, connection churn, and request buffering, and, when you scale, the load balancer. Treat the proxy as part of your security posture, not a convenience (see [Security](security.md) and the hardening section below). ## Scaling: Multiple Workers on One Host One worker serves one request at a time, but that is a queueing limit, not a hard ceiling. If 30 ms of requests are queued in front of yours, you wait 30 ms. For a typical CRUD page that is invisible; for a page that runs a slow report query, every request behind it waits for the duration of that query. When that matters, run several Bialet processes against the same app directory, each on its own port, and let the proxy load-balance between them: ```bash bialet -p 7001 -w /www/example.com bialet -p 7002 -w /www/example.com bialet -p 7003 -w /www/example.com ``` nginx: ```nginx upstream bialet { server 127.0.0.1:7001; server 127.0.0.1:7002; server 127.0.0.1:7003; } location @bialet { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://bialet; } ``` Caddy: ``` example.com { reverse_proxy 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 } ``` This is worker scaling on one machine, not a cluster. The workers share one SQLite file, so the constraints of a single database still apply: - **One writer at a time.** SQLite serializes writes. A worker waits up to 5 seconds for the lock (`sqlite3_busy_timeout`) and then returns an error. Enable WAL mode (`-w`) so readers do not block on a writer, and keep the app and database on a local disk — not NFS or another network filesystem. - **Sessions are shared.** They live in the `BIALET_SESSION` table, so any worker can serve any request. Sticky sessions are not required. - **Migrations run in every process at startup.** Keep `_migration.wren` idempotent (`CREATE TABLE IF NOT EXISTS`, ...), and start the workers one at a time on the first deploy so two of them do not run the same migration concurrently. - **Resource limits are per worker.** `-m` and `-c` apply to each process, so three workers get three times the memory and CPU budget. Size them against the host, not the app. - **Each worker needs its own port.** `-p` is per process; the supervisor and HTTP child for one worker are independent of the others. > **Pitfall: cron runs once per worker.** > `_cron.wren` is evaluated once a minute by every process, so four workers > fire every job four times. Make jobs idempotent, or gate them with a > database flag so only one worker does the work. There is no `--no-cron` > flag yet. Scaling across machines is a different problem: it needs a shared database server, which Bialet does not support. That is the point at which a different stack fits better. ## No Restart Needed on Deploy When you push new code to your server, the Bialet server **does not need to be restarted**. It watches `.wren` files for changes and picks them up automatically. ### Auto-Migration If your app has a `_migration.wren` file (or `/_app/migration.wren`), any pending database migrations run automatically **each time the migration file is created or modified**. Just deploy the new file and Bialet runs the migrations before serving the next request. No manual steps, no downtime. This means your deploy flow is: ```bash scp -r ./src/* user@your-server:/www/your-app/ # Done. Migrations run, code reloads, server keeps running. ``` ## Static Files Bialet serves static files (CSS, JS, images, fonts) directly. For better performance, configure your reverse proxy to: - **Cache** static assets (add `Cache-Control` headers) - **Gzip** or **brotli** compress text-based assets (CSS, JS, HTML, SVG) It's also safe to serve static files from the proxy directly instead of proxying to Bialet — just add a `location` block pointing to your app directory. ## Reverse Proxy Setups ### nginx ```nginx server { listen 80; listen [::]:80; server_name example.com; # Serve static files directly from disk (optional, faster) root /www/example.com; location / { # Try static file first, fall back to Bialet try_files $uri @bialet; } # Gzip static assets location ~* \.(css|js|svg|woff2|png|jpg|jpeg|gif|ico|html)$ { expires 7d; add_header Cache-Control "public, immutable"; try_files $uri =404; } location @bialet { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://127.0.0.1:7001; } } ``` For Let's Encrypt TLS, wrap with: ```nginx server { listen 443 ssl; listen [::]:443 ssl; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # ... same location blocks as above ... } server { listen 80; listen [::]:80; server_name example.com; return 301 https://$host$request_uri; } ``` ### Apache Enable the required modules: ```bash sudo a2enmod proxy proxy_http rewrite headers expires ``` Virtual host configuration: ```apache ServerName example.com # Gzip AddOutputFilterByType DEFLATE text/html text/css text/javascript AddOutputFilterByType DEFLATE application/javascript application/json AddOutputFilterByType DEFLATE image/svg+xml # Cache static assets ExpiresActive On ExpiresByType text/css "access plus 7 days" ExpiresByType text/javascript "access plus 7 days" ExpiresByType image/png "access plus 7 days" ExpiresByType image/jpeg "access plus 7 days" ExpiresByType image/svg+xml "access plus 7 days" ExpiresByType font/woff2 "access plus 30 days" # Serve static files directly DocumentRoot /www/example.com RewriteEngine On # If the file exists, serve it RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f RewriteRule ^ - [L] # Otherwise proxy to Bialet RewriteRule ^/(.*)$ http://127.0.0.1:7001/$1 [P,L] ProxyPreserveHost On ProxyPassReverse / http://127.0.0.1:7001/ ``` ### Caddy Caddy is the simplest option — it handles TLS automatically: ``` example.com { encode gzip zstd @static { file path *.css *.js *.svg *.woff2 *.png *.jpg *.jpeg *.gif *.ico *.html } header @static Cache-Control "public, max-age=604800, immutable" reverse_proxy 127.0.0.1:7001 } ``` That's it. Caddy obtains and renews certificates automatically. ## Hardening the Proxy The proxy is your first line of defense. A Bialet worker handles one request at a time, so a slow or hostile client delays every request behind it. Bialet's own header and body deadlines bound how long that can last, but the proxy should still cap body size, enforce a shorter read deadline, and buffer requests before they reach the worker. Key directives, regardless of which proxy you run: - **Cap the request body.** Bialet rejects request bodies larger than the `-b` limit (default 128 KB) with `413 Payload Too Large` *before* parsing them, so a body can never stall the Wren runtime. The effective cap is the smaller of `-b` and a memory-safe ceiling (`-m` / 512) so a body can never push the fork()ed child past its `RLIMIT_AS` budget — at the default 128 MB soft limit that ceiling is 256 KB, so the default `-b 128` is fully effective. Set the proxy's body limit to the smallest your app needs — 1 MB is a sane default for the proxy. - **Set a body-read deadline.** Bialet already bounds this itself: 5 seconds for the whole header block and 30 seconds for the body, with each `recv()` capped at 5 seconds, after which the connection is dropped. A proxy-side timeout still helps because it rejects a stalled client before it reaches the worker: `client_body_timeout` in nginx, `RequestReadTimeout ... MinRate` in Apache. - **Buffer the body at the proxy.** Read the full request before forwarding it, so the upstream socket is never held open by a slow client. - **Bind Bialet to `127.0.0.1`** and only expose the proxy to the internet. - **Deny private files at the proxy too.** Bialet already returns 403 for `_`/`.`-prefixed files, but blocking them at the proxy adds a second layer in case an application or symlink ever serves one (see [Security](security.md)). ### nginx ```nginx server { listen 80; listen [::]:80; server_name example.com; # Cap body size and enforce a total read deadline. client_max_body_size 1m; client_body_timeout 10s; # Deny private files at the proxy (defense in depth). location ~ (^|/)[_.] { return 403; } # Serve static files directly from disk (optional, faster) root /www/example.com; location / { # Try static file first, fall back to Bialet try_files $uri @bialet; } # Gzip static assets location ~* \.(css|js|svg|woff2|png|jpg|jpeg|gif|ico|html)$ { expires 7d; add_header Cache-Control "public, immutable"; try_files $uri =404; } location @bialet { proxy_request_buffering on; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://127.0.0.1:7001; } } ``` Note: the `location ~ (^|/)[_.]` block matches any path segment that starts with `_` or `.`, which covers `_db.sqlite3` and dotfiles such as `.env`. If your app legitimately serves files with such names, drop this block — but prefer renaming those files. ### Apache ```apache ServerName example.com # Cap body size (bytes) and set a total read timeout (needs mod_reqtimeout). LimitRequestBody 1048576 RequestReadTimeout header=20-40,MinRate=500 body=30,MinRate=500 # Gzip AddOutputFilterByType DEFLATE text/html text/css text/javascript AddOutputFilterByType DEFLATE application/javascript application/json AddOutputFilterByType DEFLATE image/svg+xml # Cache static assets ExpiresActive On ExpiresByType text/css "access plus 7 days" ExpiresByType text/javascript "access plus 7 days" ExpiresByType image/png "access plus 7 days" ExpiresByType image/jpeg "access plus 7 days" ExpiresByType image/svg+xml "access plus 7 days" ExpiresByType font/woff2 "access plus 30 days" # Deny private files at the proxy (defense in depth). RewriteRule ^(.*)(^|/)[_.][^/]*$ - [F,L] # Serve static files directly DocumentRoot /www/example.com RewriteEngine On # If the file exists, serve it RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f RewriteRule ^ - [L] # Otherwise proxy to Bialet RewriteRule ^/(.*)$ http://127.0.0.1:7001/$1 [P,L] ProxyPreserveHost On ProxyPassReverse / http://127.0.0.1:7001/ ``` `RequestReadTimeout` is provided by `mod_reqtimeout`, enabled with `a2enmod reqtimeout`. The `MinRate` clause stops a slow-drip client that would otherwise never trip the absolute timeout. ### Caddy Caddy does not have a per-location body-size directive, but you can cap request bodies with a request matcher: ``` example.com { encode gzip zstd @static { file path *.css *.js *.svg *.woff2 *.png *.jpg *.jpeg *.gif *.ico *.html } header @static Cache-Control "public, max-age=604800, immutable" @private { path_regexp private (^|/)[_.] } respond @private 403 reverse_proxy 127.0.0.1:7001 { flush_interval -1 } } ``` Caddy reads request bodies before proxying by default. For a total body deadline, front Caddy with a timeout-capable proxy or enforce it at the app level. ## Cloudflare Tunnel (Alternative) If you do not control a server or a domain, [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) exposes a local Bialet server over a public HTTPS URL **without opening a port, configuring a proxy, or buying DNS**. `cloudflared` keeps an outbound connection to Cloudflare's edge; requests arrive at your machine through that tunnel. Cloudflare terminates TLS and buffers slow clients, which covers most of what a reverse proxy does. Install `cloudflared` ([official packages](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/)): ```bash sudo apt-get install -y cloudflared ``` Start Bialet bound to localhost, then point a quick tunnel at it: ```bash bialet -p 7001 /www/myapp cloudflared tunnel --url http://127.0.0.1:7001 ``` `cloudflared` prints a `https://.trycloudflare.com` URL. Open it and your app is live. No account, no configuration. > **Pitfall: the URL is temporary.** A quick tunnel dies when `cloudflared` > stops, and the next run gets a **new** random hostname. Fine for demos and > previews; useless as a stable endpoint. For a stable hostname you need a [named tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-remote-tunnel/), which requires a Cloudflare account and a domain on their DNS: ```bash cloudflared tunnel login cloudflared tunnel create myapp cloudflared tunnel route dns myapp app.example.com cloudflared tunnel run --url http://127.0.0.1:7001 myapp ``` A named tunnel persists across restarts and can run as a systemd unit with `cloudflared service install`. Security notes that still apply: - **Bind Bialet to `127.0.0.1`.** The tunnel forwards to localhost; do not add `-h 0.0.0.0`. Only Cloudflare's edge ever talks to your server. - **Cap the request body.** Cloudflare does not enforce a body-size limit for you. Keep Bialet's `-b` default (128 KB) or lower it. - **Private files are still private.** Bialet returns 403 for `_`/`.` prefixed files; a tunnel does not change that. ## Running Bialet as a Service ### systemd (Ubuntu / Debian) Create a unit file to keep Bialet running and auto-start on boot. First, create a group service (optional — useful if you run multiple apps): **`/etc/systemd/system/bialet.service`** ```ini [Unit] Description=Bialet group [Service] Type=oneshot ExecStart=/bin/true RemainAfterExit=yes [Install] WantedBy=multi-user.target ``` Then create one unit per app: **`/etc/systemd/system/example.com.service`** ```ini [Unit] Description=example.com in Bialet PartOf=bialet.service After=bialet.service [Service] User=root WorkingDirectory=/www/example.com ExecStart=bialet -p 7001 -m 1024 -M 2048 -l /var/log/bialet/example.com.log -d /www/example.com.sqlite /www/example.com Restart=on-failure [Install] WantedBy=bialet.service ``` Enable and start: ```bash sudo mkdir -p /var/log/bialet /www/example.com sudo systemctl daemon-reload sudo systemctl enable bialet.service example.com.service sudo systemctl start bialet.service example.com.service ``` ### Multiple Apps on One Server Run each app on a different port and proxy by hostname: ```bash bialet -p 7001 /www/site-a # proxied from site-a.com bialet -p 7002 /www/site-b # proxied from site-b.com ``` Point each nginx `proxy_pass` / Apache `ProxyPass` / Caddy `reverse_proxy` to the matching port. ## Docker Run the published [`bialet/bialet`](https://hub.docker.com/r/bialet/bialet) image directly with [Docker](https://docs.docker.com/): ```bash docker run -t -p 7001:7001 -v "$PWD":/var/www bialet/bialet ``` The `-t` flag allocates a pseudo-TTY so the server's logs are colored. Drop it if you want plain, uncolored output. The image serves the current directory, which is mounted at `/var/www`. ### Customizing the Application Directory To serve a different directory, mount it at `/var/www`: ```bash docker run -t -p 7001:7001 -v /path/to/app:/var/www bialet/bialet ``` ### Changing the Default Port The application listens on port `7001` inside the container. To expose a different host port, use `-p`: ```bash docker run -t -p 8080:7001 -v "$PWD":/var/www bialet/bialet ``` (sqlite-wal-mode-production)= ## SQLite WAL Mode (Production) Enable WAL with the `-w` flag: ```bash bialet -w -p 7001 /www/myapp ``` ### What Is WAL? WAL (Write-Ahead Logging) is an alternative journal mode for SQLite. Instead of writing changes directly to the main database file, SQLite appends them to a separate `-wal` file first, then periodically merges them back (a process called *checkpointing*). ### Why It Matters for a Web Server By default, SQLite uses a *rollback journal*. In that mode, **readers block writers and writers block readers**. On a web server handling concurrent requests, this means: - A page loading data (`SELECT`) blocks a form submission (`INSERT`) - Two simultaneous form submissions queue up behind each other - Under any real traffic, you get `SQLITE_BUSY` errors WAL fixes this by decoupling reads from writes: | | Rollback Journal (default) | WAL mode (`-w`) | |---------------------|---------------------------|---------------------| | Readers vs. writers | Block each other | Work concurrently | | Multiple readers | OK | OK | | Multiple writers | One at a time (queued) | One at a time (queued) | | Write performance | Baseline | Often faster | In practice, this means a user submitting a comment form won't stall another user browsing the page, and your cron tasks (which may write to the DB) won't interfere with normal traffic. ### Disk Footprint WAL mode creates two extra files alongside your database: - `_db.sqlite3-wal` — the write-ahead log (grows during writes) - `_db.sqlite3-shm` — shared memory index (small, fixed size) The WAL file shrinks automatically on checkpoint. For most apps, it stays under a few megabytes. ### When You Can Skip It WAL is not strictly necessary if: - Your app is read-only or single-user - Traffic is extremely low (a handful of requests per minute) - You're prototyping locally For anything serving real users, enable it with `-w`. The overhead is negligible and the concurrency benefit is immediate. ## Server Resource Limits Use CLI flags to constrain resources per app: | Flag | Description | Default | Example | |------|------------------------|---------|------------------| | `-m` | Soft memory limit (MB) | 128 | `-m 1024` | | `-M` | Hard memory limit (MB) | 256 | `-M 2048` | | `-c` | Soft CPU limit (%) | 15 | `-c 25` | | `-C` | Hard CPU limit (%) | 30 | `-C 50` | | `-b` | Max request body (KB) | 128 | `-b 512` | | `-u` | Max per-file upload | 4 MB | `-u 8MB` | | `-w` | SQLite WAL mode | off | `-w` | The request-body cap is the smaller of `-b` and the memory-safe ceiling (`-m` / 512, about 256 KB at the default 128 MB soft limit). Bodies over the cap are rejected with `413` before parsing; raising `-m` raises the ceiling. Uploads are capped by `-u` and must also fit inside the body cap, so the effective per-file limit is the smaller of the two minus ~8 KB of multipart framing — about 120 KB at the defaults. Raise `-b` (and `-m` when the ceiling binds) together with `-u` to allow large files. See [File Size Limits](file.md). SQLite foreign keys (`-f`) and synchronous mode (`-s`) are also startup flags; see [Upload and SQLite Tuning](usage.md). Example for a production app: ```bash bialet -p 7001 -m 1024 -M 2048 -c 25 -C 50 -w -b 1100 -u 1MB \ -l /var/log/bialet/app.log /www/myapp ``` Raises the default body cap so the app accepts uploads up to ~1 MB; drop `-b`/`-u` if it does not handle uploads at all. ## Quick Deploy Checklist 1. **Copy the binary** — `scp bialet user@host:/usr/local/bin/` 2. **Copy your app** — `scp -r *.wren static/ user@host:/www/myapp/` 3. **Set up the reverse proxy** — nginx, Apache, or Caddy (see above), including the body-size cap, read timeout, and private-file deny from the hardening section 4. **Create the systemd unit** — so it starts on boot and restarts on crash 5. **Point your DNS** — A/AAAA record to your server's IP 6. **Add TLS** — Let's Encrypt via certbot (nginx/Apache) or Caddy's auto-TLS # Reference This document provides the API reference for Bialet's built-in classes and external modules. ## Core Classes The following classes are available by default in all Bialet applications without requiring imports: - **Request** - Handle incoming HTTP requests - **Response** - Construct and manage HTTP responses - **Cookie** - Manage cookies - **Session** - Manage session data - **Json** - Handle JSON data - **Util** - Utility helper methods - **Config** - Manage configuration settings - **Db** - Database interactions - **Http** - Perform HTTP requests - **Date** - Date and time operations - **File** - File operations - **Markdown** - Render Markdown content to HTML - **System** - Logging and output utilities - **HtmlNode** - A string of already-safe HTML that `{{ }}` leaves unescaped ## HtmlNode `HtmlNode` wraps a string of already-rendered HTML. HTML string literals (`
    ...
    `), the result of a `{{ }}` block, and `String.raw` all produce `HtmlNode` values. Interpolating an `HtmlNode` renders it verbatim, while plain strings are auto-escaped. ### new(string) Wraps a string as safe HTML. Only use this for markup you control or have already escaped. ### toString Returns the raw HTML string. ### +(other) Concatenates the raw HTML with `other.toString`, returning a new `HtmlNode`. ### raw Returns `this` — the node is already safe. ## External Classes External classes must be imported explicitly using the GitHub shorthand or full URLs. See [External Modules](external-modules.md) for details on how to import, author, and manage external modules. --- ## Request A class to handle incoming HTTP requests, parsing their content, and providing convenient access to their components. ### init(message, route, files, fields) Initializes a new Request object. Bialet calls this for you before your code runs. - `message`: The raw HTTP request message. For a `multipart/form-data` request it holds the request line and headers only (see [body](#body)). - `route`: The route the request is targeting. - `files`: The uploaded files data. - `fields`: The text fields of a `multipart/form-data` body, already extracted. Empty for any other request. ### parseQuery(query) Parses the query string of the request. - `query`: The query string part of the URL. ### method Returns the HTTP method of the request (uppercase, e.g., "GET", "POST", "PUT", "DELETE"). ### uri Returns the URI of the request. ### body Returns the body content of the request. For a `multipart/form-data` request the body is an empty string. The file content never enters Wren, and the text fields are read with [post(name)](#postname). Use `Request.post()` and `Request.file()` instead of parsing the body. ### isPost Checks if the request method is POST. ### isJson Checks if the request's content type is JSON. ### json() Parses and returns the JSON body of the request. This is a convenience method equivalent to `Json.parse(Request.body)`. Example: ```wren if (Request.isJson) { var data = Request.json() var name = data["name"] var email = data["email"] } ``` ### header(name) Returns the value of a specified request header. - `name`: The name of the header. ### get(name) Returns the value of a specified query parameter. - `name`: The name of the query parameter. ### query(name) Alias for `get(name)`. Returns the value of a specified query parameter. - `name`: The name of the query parameter. ### post(name) > ⚠️ **CRITICAL: Returns `Null` when the key is not present.** > The return type is `String | Null`. You **MUST** handle `null` > before calling any string method (`.toString`, `.count`, `.trim`, > concatenation, interpolation). This is the #1 production crash cause. > **NEVER assume the field exists. ALWAYS provide a default or null check.** Returns the value of a specified POST parameter, or `null` if not found. - `name`: The name of the POST parameter. - **Returns**: `String` if the key exists in the POST body, `Null` otherwise. Works for `application/x-www-form-urlencoded` and `multipart/form-data` forms. For a multipart form it returns the text fields only: a file part is read with [file(name)](#filename) and never appears here. Line breaks in a value (a `