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.
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.
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.
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.