# Build Command

Build your project for production. `alepha build` produces **one** `dist/`, carrying one server slice per runtime you asked for.

## Quick Start

```bash
alepha build                          # one node slice
alepha build --runtime node,workerd   # both, from one build
```

Your production-ready app is now in the `dist/` folder.

## One artifact, N runtimes

Releasing an app that runs on both Node and Cloudflare used to mean building twice. Both runs rebuilt the client bundle, re-prerendered and re-compressed the assets, and those are the slow steps: only the server link genuinely differs, and it differs by a handful of export conditions.

So the client bundle, the prerender, the compression, the headers, the PWA manifest and the build manifest all run **once**, and only the server link repeats. Measured on Lore: 15.2s for a workerd-only build against 15.4s for `node,workerd`, of which `build client` is 3.9s.

⚠️ **Declared order is meaningful.** The first runtime is the **primary**: it is `manifest.runtimes[0].runtime`, it is what `dist/package.json`'s `main` points at, and it is what a deployer spawns. `["bun", "node"]` and `["node", "bun"]` produce the same two slices and different behaviour.

Then three commands turn that one `dist/` into the format you need:

| format  | command                                        | runtimes                                             |
| ------- | ---------------------------------------------- | ---------------------------------------------------- |
| binary  | [`alepha compile`](https://latest.alepha.dev/docs/cli-commands-compile.md) | bun                                                  |
| archive | [`alepha pack`](https://latest.alepha.dev/docs/cli-commands-pack.md)       | node, bun, workerd                                   |
| image   | [`alepha image`](https://latest.alepha.dev/docs/cli-commands-image.md)     | node, bun, and bun via `--compile` for a small image |

⚠️ **There is no `--target`.** The build is described by what it produces: declaring a `workerd` slice is what writes the Cloudflare config, `runtime: ["static"]` is what makes a static site, and Docker is its own command.

## What It Does

The build runs a fixed pipeline of tasks:

```mermaid
flowchart TD
  A[Clean dist] --> B[Build client]
  B --> C[Build server, once per runtime]
  C --> D[Copy assets]
  D --> E[PWA manifest]
  E --> F[Prerender]
  F --> G[dist/manifest.json]
  G --> H{Deploy config}
  H -->|workerd slice| I[wrangler.jsonc]
  H -->|static| J[Static site files]
  I --> K[public/_headers]
  J --> K
  H -->|otherwise| K
  K --> L[Brotli sidecars]
```

1. **Cleans the dist folder**: Fresh start, no stale files
2. **Builds the client**: Compiles React, bundles assets, optimizes for browsers
3. **Builds the server**: one slice per declared runtime, and only this step repeats
4. **Copies assets**: Moves static files to the right places
5. **Generates the PWA manifest**: If `pwa` is configured
6. **Prerenders pages**: Sitemap and static pages, when applicable
7. **Writes the build manifest**: `dist/manifest.json`
8. **Generates deployment configs**: `wrangler.jsonc` when a workerd slice was built, the static site files for `runtime: ["static"]`
9. **Writes `dist/public/_headers`**: security and cache headers for hosts that serve files without the app (see [Static File Headers](https://latest.alepha.dev/docs/guides-deployment-headers.md))
10. **Pre-compresses assets**: Writes `.br` (Brotli) copies of client assets. Skipped when `workerd` is the only slice: Cloudflare compresses at the edge and never serves a `.br` sidecar

## Output Structure

After building, your `dist/` folder looks like this:

```txt
dist/
├── index.node.js       # one entry wrapper per slice
├── index.workerd.js
├── server/
│   ├── node/           # each slice's chunks, namespaced
│   └── workerd/
├── public/             # static assets (CSS, JS, images)
│   ├── entry.abc123.js
│   ├── chunk.def456.js
│   └── favicon.svg
├── manifest.json       # what the build declared; `alepha pack` requires it
└── package.json        # `main` points at the primary slice
```

⚠️ **The slices are namespaced, and that is load-bearing.** Two runtimes built into one `server/` do not collide - their content hashes differ - so both sets would simply sit there, and the wrangler `server/*.js` glob would sweep the Node chunks into the Worker upload: a Worker twice the size it needs, or a Node chunk importing a node builtin and a refused deploy.

⚠️ **There is no `index.js` that works out its host.** The manifest is the discovery mechanism, because a second one able to disagree with it is worse than none.

Run your server with:

```bash
node dist                 # resolves the primary slice through `main`
node dist/index.node.js   # or name the slice
```

## Options

| Flag              | Description                                                                                                                                                      |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--runtime`, `-r` | Runtimes to link the server for, comma-separated and in order: `node`, `bun`, `workerd`. The first is the primary. `static` declares an app with no server.      |
| `--stats`         | Generate build statistics report (use `--stats=json` for JSON output)                                                                                            |
| `--inspect`       | Bundle `alepha/inspector` so devtools can inspect the production build. Off until started with `ALEPHA_INSPECT=1`. See [Inspector](https://latest.alepha.dev/docs/guides-core-inspector.md)  |
| `--prebuilt`      | Skip the bundle steps; only regenerate the target-specific deploy config (e.g. `wrangler.jsonc`) when `dist/` is already built                                   |
| `--if-stale`      | Build only when `dist/` is missing or older than its sources (the app's `src/`, `public/`, `migrations/`, `package.json`, and the workspace packages it bundles) |
| `--mode`, `-m`    | Environment mode, `production` by default: loads `.env`, `.env.<mode>` and `.env.<mode>.local`                                                                   |

Declaring a `workerd` slice is what writes the Cloudflare deploy config; `runtime: ["static"]` is what makes a static site. There is no `--target`: the build is described by what it produces.

## Deployment Targets

### Standard Node.js Server

```bash
alepha build
```

Deploy anywhere that runs Node.js:

```bash
# Copy dist/ to your server
scp -r dist/ user@server:/app

# On the server
cd /app && node .
```

### Docker

```bash
alepha build && alepha image
```

`alepha image` generates a `Dockerfile` **in the app directory** when there is none, beside `alepha.config.ts`, and reuses yours when there is: the file is meant to be committed and edited. It is not written into `dist/`, which the build wipes on every run.

```bash
alepha image --tag           # tag:latest
alepha image --tag=1.3.4     # tag:1.3.4
```

The generated image runs as uid `1000`, not root (`image.user` overrides it). `image.env` and `image.volumes` bake `ENV` defaults and `VOLUME` declarations into it, so a self-contained image needs no `docker run` flags - see the [Docker deployment guide](https://latest.alepha.dev/docs/guides-deployment-docker.md).

`alepha image --compile` compiles the bun slice first and ships an image holding the binary and nothing else, on `gcr.io/distroless/cc-debian12` (`image.from` overrides it). That variant stays root: the base has no shell to prepare a volume with.

⚠️ A Bun `--compile` binary is **not static**, whatever triple it is built for: it needs an interpreter, `libstdc++` and `libgcc`. `alepha image` derives the triple from the base for that reason, and refuses a base with no libc (`scratch`, `distroless/static`) rather than producing a container that exits immediately.

⚠️ `alepha image` shells out to `docker build`, so it is a local and CI command. It cannot run in an in-process build path.

### Cloudflare Workers

```bash
alepha build --runtime=workerd    # or --runtime workerd
```

Creates Cloudflare Workers configuration:

```txt
dist/
├── main.cloudflare.js  # Worker entry point (wrangler's `main`)
├── index.workerd.js    # the workerd slice's entry wrapper
├── server/workerd/     # the workerd slice's chunks
├── wrangler.jsonc      # Wrangler configuration
├── manifest.json       # Build manifest (detected resources, declared env keys)
└── public/             # Static assets (if any)
```

> **D1 Database Support**
>
> If your `DATABASE_URL` uses the `d1://` protocol (as injected by the [infra plugin](https://latest.alepha.dev/docs/cli-plugins-infra.md)), the D1 binding is automatically configured in `wrangler.jsonc`.

Then deploy:

```bash
cd dist && wrangler deploy
```

Or let `alepha deploy` drive the whole pipeline - provisioning, build, migrations, deploy, and secrets.

### Static Site

```bash
alepha build --runtime=static
```

Prerenders your pages to plain HTML/CSS/JS for any static host. Not compatible with `--prebuilt` (prerendering needs a live app).

## SEO Features

### Sitemap Generation

Add the [`$sitemap`](https://latest.alepha.dev/docs/packages-alepha-react-sitemap.md) primitive to a router. It
builds `sitemap.xml` from your `$page` primitives and prerenders it to
`dist/public/sitemap.xml`, the file both a static site and a built server serve:

```typescript
import { $sitemap } from "alepha/react/sitemap";

class AppRouter {
  // hostname defaults to PUBLIC_URL, then "" (relative URLs)
  sitemap = $sitemap({ hostname: "https://myapp.com" });
}
```

Produces `dist/public/sitemap.xml` with all your routes:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://myapp.com/</loc>
    <lastmod>2026-06-06</lastmod>
  </url>
  <url>
    <loc>https://myapp.com/about</loc>
    <lastmod>2026-06-06</lastmod>
  </url>
  <!-- ... -->
</urlset>
```

## Build Statistics

```bash
alepha build --stats
```

Generates interactive reports about your bundles:

```bash
# View client bundle analysis
open dist/public/stats.html

# View server bundle analysis (one report per slice)
open dist/server/node/stats.html
```

The reports show:

- File sizes (before and after compression)
- Chunk breakdown
- Dependency analysis

> **Bundle Analysis**
>
> Use this to find large dependencies dragging down your bundle size.

## Environment Variables

The build loads `.env`, `.env.<mode>` and `.env.<mode>.local` (`--mode` is `production` by default) into its own process. That is the environment it analyzes the app in, and the one prerendered pages are rendered with:

```bash filename=.env.production
API_URL=https://api.myapp.com
```

> **Read at runtime, not baked in**
>
> The server bundle does not embed your variables: a server slice reads its environment when it starts, so one `dist/` serves every environment. Only `process.env.NODE_ENV` and the build metadata behind `alepha.meta` are compiled in as constants, in both bundles, along with the `VITE_*` variables Vite inlines into the client bundle. `alepha.env` is empty in the browser. What a prerendered page displays is fixed at build time, like the rest of its HTML.

## Build Configuration

Defaults for the build command live in the `build` section of `alepha.config.ts`. Command-line flags override them:

```typescript filename=alepha.config.ts
import { defineConfig } from "alepha/cli/config";

export default defineConfig({
  build: {
    runtime: ["bun"],
    stats: true,
    pwa: {
      name: "My App",
      themeColor: "#0f172a",
    },
  },
});
```

Build options include `stats`, `inspect` and `runtime`, plus the sections below. Binary compilation and container packaging belong to `alepha compile` and `alepha image`; configure images under top-level `image`, not `build`.

| Section      | Description                                                                                                                    |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `output`     | Override `dist` and `public` directory names                                                                                   |
| `preload`    | `allowUnresolved`: source paths of pages allowed to resolve to no preloaded chunks (refused by default)                        |
| `cloudflare` | Extra `wrangler.jsonc` config merged into the generated file                                                                   |
| `static`     | Surge domain for the `CNAME` file; `source` to adopt a client directory the workspace built itself (must live outside `dist/`) |
| `pwa`        | Web app manifest: name, short name, colors, display mode                                                                       |

## Client-Side Optimization

The build automatically:

- **Minifies JavaScript**: Removes whitespace, shortens variable names
- **Minifies CSS**: Combines and compresses styles
- **Tree shakes**: Removes unused code
- **Code splits**: Creates separate chunks for routes
- **Hashes filenames**: Enables aggressive caching
- **Pre-compresses assets**: Writes Brotli (`.br`) copies of JS, CSS, WASM, SVG, HTML and XML files

## Server-Side Optimization

The server build:

- **Bundles dependencies**: Single file, no `node_modules` needed in production
- **Externalizes Node built-ins**: Uses native `fs`, `path`, etc.
- **Preserves source maps**: Debug production issues when needed

> **Single File Deploy**
>
> Deploy the complete `dist/` artifact: the entry wrappers import chunks under `server/<runtime>/`. Runtime dependencies, if externalized, are declared in `dist/package.json`.

## Backend-Only Projects

If your project has no browser entry, the build only creates the server:

```bash
alepha build
# → dist/index.node.js and server/node/ chunks
```

Perfect for:

- API servers
- CLI tools
- Background workers
- Scheduled jobs

## Build Workflow

A typical deployment workflow:

```bash
# 1. Verify everything works
alepha verify

# 2. Build for production
alepha build --runtime=workerd

# 3. Run the full pipeline with the existing bundle
alepha deploy --env production --prebuilt
# Or only authenticate and upload an appropriate existing artifact:
# alepha infra deploy --env production
```

## Tips

**Run verify first.** The `alepha verify` command catches issues before you build. Don't ship broken code.

**Check bundle sizes.** Run `alepha build --stats` periodically. Large bundles slow down your users.

**Test the production build locally.** After building, run `node dist/index.node.js` locally for a Node slice before deploying. Catch issues early.

**Environment variables matter.** Make sure your production `.env` is correct. Wrong API URLs are a common deployment bug.
