LILYLILYDOCS
Docs/Concepts/Bricks

Bricks

A brick is a portable WASM bundle: one binary, one hash, one path to ship it.

A brick is the unit of deployment in LILY. On disk it is a directory; on the wire it is a tarball; in a registry it is a sha256. Whatever shape it is in, the contents never drift: one or more .wasm modules, a manifest.json, and an optional assets/ tree for CSS, fonts, images, and anything else the runtime should serve verbatim.

Bricks are content-addressed. The sha256 of the canonical tarball is the brick's identity. Two builds of the same source on two different machines produce the same digest, because the toolchain pins compiler versions, sorts inputs, and strips non-deterministic metadata. If the hash changes, the bytes changed. There is no other reason.

Brick layout
page.tsxSOURCEfrontendPLUGINBIRIRbackendWASM*.brickBUNDLEONE BINARY PER PROJECT — INSPECTABLE, SIGNED, REPRODUCIBLE
One manifest, one or more modules, a tree of static assets, a sidecar signature.

What is inside

The manifest declares the entry module, the runtime ABI version, exported routes (for HTTP-serving bricks), and the hashes of every other file in the bundle. The runtime refuses to load a brick whose internal hashes disagree with the manifest, so partial uploads and bit-rot fail loudly. Static assets sit under assets/ and are addressed by path; the runtime serves them directly without re-entering WASM.

How bricks are produced

Single-source compiles go through lly compile. The extension picks a frontend plugin (frontend-c, frontend-cpp, frontend-js, and so on), the frontend lowers to BIR, and the runtime backend emits a single-module brick. Multi-route or multi-file projects are produced by util plugins: lly nextjs compiles an App Router project, lly nextjs void packs an entire Next.js app plus its tiny HTTP server into one brick, and other plugins follow the same shape.

compile
$ lly compile hello.c -o hello.brick → frontend-c v0.8.2 · 1 TU · 42 LOC → runtime-backend v0.12.0 · single-brick ✓ wrote hello.brick (38 KB, sha256 9c2a…e71f)

Single-brick versus multi-brick

By default lly compile emits a single-brick bundle: one .wasm module, one linear memory, one instantiation. This is the fastest path and the easiest to reason about. When a project spans many translation units and you want them to share linear memory, opt in with --multi. The compiler then emits several modules linked through a shared memory export and an indirect function table. Wasmtime instantiates them together and the runtime threads BIR calls across module boundaries.

Multi-brick is required for anything that depends on shared mutable state between translation units, such as a C++ program built from many .cc files with cross-TU globals, or a Next.js project whose routes share a cache. Multi-brick is opt-in because it costs a small amount of startup latency and rules out some single-module optimisations.

Inspecting a brick

Bricks are designed to be readable without the runtime. lly bundle inspect prints the manifest, lists modules with their sizes and hashes, dumps declared exports, and surfaces the signature status. Use it before a deploy to confirm that what you built is what you think you built.

inspect
$ lly bundle inspect ./void.brick brick void.brick sha256 4f8a1c92e0b73d6a8e1c5fda4731b908c2a7e71f6d54bb09e3a47c91d8e5601a abi brick.v1 mode multi-brick (3 modules, shared memory) modules entry.wasm 612 KB sha256 1a2b…9c0d routes.wasm 1.4 MB sha256 7e44…f201 runtime.wasm 284 KB sha256 0c8f…ab17 assets 14 files, 312 KB routes / · /blog · /blog/[slug] · /api/health signature void.brick.sig · ed25519:lily-store-2026 ✓ manifest hashes match contents ✓ signature verified

Signatures

Every brick distributed through rc.lilylabs.io ships with a .sigsidecar: an ed25519 signature over the brick's sha256, keyed by the LILY store. lly plugin verify <name> checks the sidecar before installing a plugin, and the runtime refuses to load bricks that fail signature checks when strict mode is enabled. You can sign your own bricks for private distribution; the format is documented at plugin verify.

Why content-addressing matters

Because the digest is the identity, the deploy path can be cache-aggressive without ever risking a stale binary. The platform keeps a CAS keyed by sha256; if a CAP already has the bytes, the upload skips. Rollbacks are a hash swap. Audits are a hash comparison. There is no version drift between "the brick on my laptop" and "the brick in production" — they either share a digest or they are different artefacts.

Once you have a brick, the next step is to run it. Locally with lly run, on the fleet with lly void deploy. The brick itself does not care which.