# The `surf` command

- **Status:** current on 2026-11-06, for version 0.
- **Related documents:** [getting started](https://surface.solenovis.com/docs/getting-started.md), [build a full app](https://surface.solenovis.com/docs/full-app.md), [ADR-0101](https://git.solenovis.com/henning.liemant/surface/src/branch/main/docs/adr/0101-one-install-for-every-target.md)

`surf` is the only tool that a developer installs. This page describes the install, each command and each option.

---

## 1. Install

```bash
curl -fsSL https://git.solenovis.com/henning.liemant/surface/raw/branch/main/scripts/install.sh | sh
```

The script finds the system, downloads the newest release, checks its SHA-256, and puts `surf` into `~/.surf/bin`. Add this folder to the `PATH`. The releases cover Linux x86-64 and macOS arm64.

A web build needs nothing else. The first native build downloads two parts into `~/.surf`, and this takes some minutes:

- A pinned Rust toolchain from `static.rust-lang.org`: about 150 MB on macOS and 210 MB on Linux.
- The runtime pack of this version of `surf`: about 20 MB.

`surf` checks the SHA-256 of each download before it unpacks the file. After these downloads, a build needs no network.

All projects share one target folder in `~/.surf`, so a second project does not compile the runtime again. On a Mac with an M-series chip, the first build of the hello-world app took 50 to 200 seconds with the downloads. A second project took 10 to 30 seconds. The load of the machine gave these ranges. The folder `~/.surf` then held about 1.1 GB.

A native build needs a linker, and Rust does not bring one:

- On macOS, run `xcode-select --install`.
- On Linux, install the package `gcc`.

`surf doctor` shows what the machine has.

The Linux binary comes from Debian 12 (glibc 2.36). An older system can refuse to start it.

### Build `surf` from a checkout

Only framework developers do this. It needs Rust 1.99 or later:

```bash
cargo build -p surf-cli
```

This binary uses the `cargo` on the `PATH` and the crates of its checkout, so it must stay in that checkout. It does not use `~/.surf`.

---

## 2. Projects

### `surf new <folder>`

Makes a new project from the template. The folder must be empty, or must not exist. The project gets `src/App.surf`, `.gitignore`, and `surf.toml` with the newest edition, for example `edition = "2026"` ([stability](https://surface.solenovis.com/docs/stability.md)).

Each command reads the edition of `surf.toml`, and the edition of each package in `src/pkg`. An edition that this `surf` does not know stops the command.

### `surf check <folder>`

Checks all `.surf` files in the folder, or in its `src` folder. The command shows all errors, and it runs no code generator.

### `surf lint [folder]`

Finds code that compiles but is probably wrong (ADR-0070). A finding gives a non-zero exit code. The default folder is the current folder. The rule `edition` finds a project that names no edition in `surf.toml`. The rule `field-select` finds `select-none` on an `Input` or a `TextArea`, because the utility does not turn off the selection of the text of a field (ADR-0131). The rule `media-format` finds the sources of a `Video` or an `Audio` that do not play on each target of ADR-0155. It reads the `type` of a `Source`, or the first bytes of a file under `assets/`. The rules `no-captions` and `autoplay-stop` find a `Video` with sound and no captions, and a `Video` that plays and loops by itself with no way to stop (ADR-0157, ADR-0158).

### `surf test [folder] [--filter <word>]`

Runs each `pub fn test...` in the folder `tests` of the project, with no build, no browser and no window (ADR-0069). Section 8 of the [guide](https://surface.solenovis.com/docs/full-app.md) lists the functions of `surf:test`. A test has a clock of its own: `Test.wait(ms)` moves it, and no real time passes.

| Option | Meaning |
|---|---|
| `--filter <word>` | Runs only the tests with this word in the name. |

### `surf fmt [paths...] [--check]`

Formats `.surf` files. A folder means its `.surf` files and the files in its folders. The default path is the current folder.

Each level of indent has 4 spaces. A project can set another number of spaces, from 1 to 8, in `surf.toml` (ADR-0162):

```toml
[fmt]
indent = 2
```

A file uses the `surf.toml` of the nearest folder above it. A wrong value stops the command before it changes a file.

| Option | Meaning |
|---|---|
| `--check` | Changes no file. The command fails when a file would change. |

### `surf routes [folder]`

Shows the routes of a project with `src/routes` (ADR-0055).

---

## 3. Builds

### `surf build [folder]`

Builds the project. The default folder is the current folder.

| Option | Meaning |
|---|---|
| `--target web` | The default. Writes plain JavaScript and CSS into `build/web`. |
| `--target desktop` | Writes a release binary for each system of the list `[desktop] systems` of `surf.toml` into `build/desktop/<system>`, and an app bundle for macOS. Without the list, it builds for the system of this machine (ADR-0137). |
| `--os macos\|linux\|windows` | Desktop target: builds one system for this run, in place of the list. Without `--arch`, the system has the processor of this machine (ADR-0137). |
| `--arch x86_64\|aarch64` | Desktop target: builds one system for this run, in place of the list. Without `--os`, the system has the `os` of this machine (ADR-0137). |
| `--target server` | Writes a server binary and its web files into `build/server`, for a project with `src/routes` (ADR-0057). The binary listens on `HOST` and `PORT`. Without `HOST`, it binds to `127.0.0.1` (ADR-0109). |
| `--target static` | Writes each page as a file into `build/static`, for a host with no server. |
| `-o <out>` | Writes into the folder `out`. A desktop build writes each system into `out/<system>`. |
| `--base <path>` | The base path: the prefix of each address of the site, for example `/a/` (ADR-0139). The web target, the static target, the server target and the adapters take it. The page, the router, each `Link`, each address of a file and each request to the server code get the prefix. Without it, the folder of a web build can go anywhere. With the server target, the static target and an adapter, the path starts and ends with `/`. Its parts have only letters, digits, `-`, `.`, `_` and `~`. The server answers a request outside the base path with status 404. The code of the server sees each path without the prefix (wave spec, section 5). The desktop target, `--embed` and `--single-file` give an error with this option. |
| `--split auto\|on\|off` | Web, server and static targets: splits the script into chunks (ADR-0047). `auto` is the default. It makes chunks when they save more than the round trip that they cost. |
| `--single-file` | Web target: writes one file, `index.html`, with the assets inside. Use it for a host that loads one HTML text, such as the webview of a plugin. The file has the text of `src/head.html`, as a normal build has. |
| `--x11` | Desktop target on Linux: the binary can also open an X11 window (ADR-0088). |
| `--embed` | Desktop target: writes a library crate into `build/embed` for a host app in Rust (ADR-0041). |
| `--adapter <host>` | Writes the files of a host beside the build, and chooses the target (ADR-0064). `docker`, `fly` and `systemd` use the server target. `netlify`, `vercel`, `cloudflare` and `github-pages` use the static target. The `Dockerfile` of `docker` and `fly` sets `HOST=0.0.0.0`, because a container needs it. The unit of `systemd` keeps the default `127.0.0.1` (ADR-0109). With `--base`, the files of `netlify`, `vercel` and `cloudflare` name the prefix (ADR-0139). |

#### The systems of a desktop build

`surf.toml` lists the systems that `surf build --target desktop` and `surf package` build (ADR-0137):

```toml
[desktop]
systems = ["macos-aarch64", "windows-x86_64", "windows-aarch64"]
```

- A system has the form `<os>-<arch>`. `<os>` is `macos`, `linux` or `windows`. `<arch>` is `x86_64` or `aarch64`.
- A name that is not a system stops the command with an error that lists the systems. An empty list and a system that is two times in the list are errors too.
- Without the key, the list has only the system of this machine.
- The binary of each system goes into `build/desktop/<system>`, for example `build/desktop/windows-x86_64/hello.exe`. This is also the folder of a build with one system.
- `--os` and `--arch` build one system for one run. They do not change `surf.toml`. They are only for the desktop target, and not for `--embed`.
- Each machine can build Windows (ADR-0136). Only a Mac can build macOS, for `x86_64` and for `aarch64`. A build for Linux needs a Linux machine with the processor of the system. Another system stops the command before the first build, with an error that names the system and the reason.
- The private toolchain downloads the parts of each system when they are missing: `rust-std`, and `rust-mingw` for Windows. Each part has a SHA-256 in `surf`.
- A binary from a checkout uses the Rust on the `PATH`. When that Rust has no parts for a system, the error gives the command that adds them, for example `rustup target add x86_64-pc-windows-gnullvm`. `rustup` adds `rust-mingw` only on Windows. So on macOS and on Linux, a binary from a checkout needs a manual copy of `rust-mingw` for a Windows build. A release binary does not need it.
- `surf dev --target desktop` runs only on the system of this machine.

### `surf dev [folder]`

Serves the project and reloads the page after each change. With `--target desktop`, it runs the project in a window and reloads it after each change, with the interpreter of the IR.

A project with routes and server code (for example a `load.server.surfs` file, an `actions.surfs` file or a remote function) also needs its server. The command builds the binary of `surf build --target server` and runs it on a free port of the loopback address. Each request that is not for a page or for a file of `surf dev` goes to that binary: the data of a page, a form, a remote function and an endpoint. A request is for a page when it is a `GET` that accepts `text/html`. After each change, the binary builds again in the background, and the old binary answers until the new one starts. The first build needs the tools of a native build (see `surf doctor`). Later builds take some seconds.

The command prints the address of the [inspector](https://surface.solenovis.com/docs/inspector.md), the dev tools of the running app. The inspector listens only on this machine.

| Option | Meaning |
|---|---|
| `--port <number>` | The port of the server. The default is 8000. Port 0 takes any free port. With `--target desktop`, the inspector uses this port. Without this option, the inspector takes a free port when another program has port 8000. |
| `--target web\|desktop` | `web` is the default. |

### `surf package [folder] [-o <out>]`

Builds an installer (ADR-0086). macOS gets a signed disk image, Linux gets a Debian package and a tar file, and Windows gets an MSIX package (ADR-0141). The default output folder is `build/package` in the project. It builds the systems of the list `[desktop] systems`, or the one system of `--os` and `--arch` (ADR-0137). The files of each system go into `build/package/<system>`, for example `build/package/macos-aarch64/hello-0.1.0.dmg` or `build/package/windows-x86_64/hello-0.1.0.msix`.

`surf` writes the MSIX package and its signature itself, with no other tool. So each machine that can build Windows can make the package. The package needs a code-signing certificate, because Windows installs only a signed package. A test can use a self-signed certificate, and the test machine must trust it. Without a certificate, the command stops with an error for a system of Windows.

| Option | Meaning |
|---|---|
| `-o <out>` | Writes into the folder `out`. The command then needs the name of the project folder too. |
| `--os macos\|linux\|windows` | Builds one system for this run, in place of the list. Without `--arch`, the system has the processor of this machine. |
| `--arch x86_64\|aarch64` | Builds one system for this run, in place of the list. Without `--os`, the system has the `os` of this machine. |
| `--pfx <file>` | Windows: the `.pfx` file with the code-signing certificate and its RSA key. Without it, the command reads `SURF_PFX`. |
| `--pfx-password <password>` | Windows: the password of the `.pfx` file. Without it, the command reads `SURF_PFX_PASSWORD`, and then uses an empty password. Other users of the machine can see an option in the list of processes, so prefer the variable. |

#### The MSIX package of Windows

- The package has the binary, the assets and the fonts of the build, and three gray pictures for the Start menu. It also has the files of the format (ADR-0141): `AppxManifest.xml`, `AppxBlockMap.xml`, `[Content_Types].xml` and the signature `AppxSignature.p7x`.
- The name of the package is the name of the binary, for example `hello`. Windows needs 3 to 50 letters, digits, `-` or `.`.
- The publisher of the package is the subject of the certificate, in the form of Windows, for example `CN=Surface Test, O=Solenovis`. The name of the publisher that Windows shows is the common name (`CN`) of the subject.
- The version has four numbers from 0 to 65535. `version = "0.1.0"` of `surf.toml` becomes `0.1.0.0`. Another form, for example `1.0.0-beta`, stops the command.
- The command reads the certificate before the first build. A wrong password stops the command with an error that asks for the correct password.
- `surf` reads a `.pfx` file with PBES2 and AES, which OpenSSL 3 writes by default, or with 3DES, which `Export-PfxCertificate` of Windows writes by default. It does not read a file with RC2, which LibreSSL writes by default for the certificates. Export such a file again with `openssl pkcs12 -export -keypbe AES-256-CBC -certpbe AES-256-CBC`.
- The signature has no timestamp, so Windows accepts it only while the certificate is valid.

A `.pfx` file can sign a test package, but not a release for the public: a public code-signing certificate keeps its key on hardware. The [Windows plan](https://git.solenovis.com/henning.liemant/surface/src/branch/main/docs/plan/windows.md) (W11) compares the ways to sign a release.

Make a test certificate and install a test package in Windows PowerShell. `Add-AppxPackage` installs a package only when a store of the machine trusts its certificate. The store of the user is not enough. So the import into `LocalMachine\TrustedPeople` needs PowerShell as administrator, and it changes the trust of the whole machine. Remove the certificate from that store after the test.

```powershell
$cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject 'CN=My Test' -CertStoreLocation Cert:\CurrentUser\My
$password = Read-Host -AsSecureString 'Password of the .pfx file'
Export-PfxCertificate -Cert $cert -FilePath test.pfx -Password $password
Export-Certificate -Cert $cert -FilePath test.cer
Import-Certificate -FilePath test.cer -CertStoreLocation Cert:\LocalMachine\TrustedPeople
$env:SURF_PFX = 'test.pfx'
$env:SURF_PFX_PASSWORD = '<the password>'
surf package --os windows
Add-AppxPackage build\package\windows-x86_64\hello-0.1.0.msix
```

### `surf extern stubs [folder]`

Writes the stubs of the implementation files of the extern modules (language spec, section 13.2): `<file>.native.rs` and `<file>.web.js` when they do not exist, and `<file>.web.d.ts`.

### `surf emit-js <folder>`

Writes the generated JavaScript module to stdout.

| Option | Meaning |
|---|---|
| `--values ref\|clone\|path` | How the code copies a value. `path` is the default (path copying). |
| `--runtime <path>` | The import path of the web runtime. The default is `./surf.js`. |
| `-o <out>` | Writes the module to the file `out`. |
| `--css <out>` | Writes the stylesheet to the file `out`. |

### `surf emit-rust <folder>`

Writes the generated Rust code to stdout.

| Option | Meaning |
|---|---|
| `-o <out>` | Writes the code to the file `out`. |

---

## 4. Packages

`surf.toml` names the packages of a project, and `surf.lock` holds the commit of each package (ADR-0071). Run these commands in the folder of the project.

### `surf add <name> <git or path> [--rev <commit>]`

Installs a package, and then adds it to `surf.toml`. When the install fails, `surf.toml` and `src/pkg` do not change. The command changes only the package in `surf.toml`, so the comments and the other lines stay (ADR-0114).

The `path` of a package is relative to the file that names it: `surf.toml` of the project, or `surf.toml` of a package.

| Option | Meaning |
|---|---|
| `--rev <commit>` | A package from git: uses this commit, tag or branch. Without it, `surf update` takes the newest commit of the default branch. A package from a folder cannot have a `rev`. For a commit of a git folder, give the address `file://<folder>`. |

### `surf remove <name>`

Removes a package.

### `surf install`

Installs the packages at the commits of `surf.lock`. A commit of `surf.lock` is valid only while the `git` and the `rev` of the package in `surf.toml` stay the same. After a change of these values, the command takes the commit of the new values.

### `surf update`

Installs the newest commit of each package.

---

## 5. Tools

### `surf doctor`

Checks that this machine has the tools of a build. A release binary reports the private Rust toolchain, the runtime pack and the linker. The first native build downloads the toolchain and the pack, so their absence is not an error. On Linux, the command also reports the libraries of a desktop app.

On Windows, the Rust toolchain brings the linker. `rust-lld` links with the MinGW libraries of the Rust part `rust-mingw`, so a build needs no Visual Studio (ADR-0136). The command reports these files. A binary from a checkout on Windows also needs Rust for the system `x86_64-pc-windows-gnullvm`. The build scripts of the runtime run on the system of `rustc`, and the Rust for the system `x86_64-pc-windows-msvc` links them with Visual Studio.

A native build (`--target desktop`, `--target server` and `surf package`) needs the linker. A web build does not need it. So a missing linker shows `absent`, and the exit code stays 0. The last line then says that a web build works and that a native build needs the linker first. Install the linker before the first native build (section 1). A binary from a checkout also needs Rust: a missing `rustc` or `cargo` shows `MISSING` and gives a non-zero exit code.

### `surf upgrade`

Replaces this binary with the newest release after a SHA-256 check. The next native build downloads the runtime pack of the new version, and it removes the old pack and the old build results. A binary from a checkout cannot upgrade.

The command installs only a newer version. When the server names an older version as the newest release, the command stops with an error. To install a specified version, use `scripts/install.sh` with `SURF_VERSION`.

### `surf lsp`

Runs the language server on stdin and stdout. `editors/vscode` and `editors/zed` use it.

### `surf mcp [--http <port>] [--bind <address>] [--origin <origin>]`

Serves the documents of Surface and the checks of the compiler to an agent, through the Model Context Protocol (ADR-0107).

- With no option, it uses stdin and stdout.
- `--http <port>` serves the Streamable HTTP transport at `/mcp`.
- `--bind <address>` sets the address of that server. The default is the loopback address.
- `--origin <origin>` adds a value to the list of accepted `Origin` headers. You can give it more than once.

The tools are `list_sections`, `get_documentation`, `check_code` and `format_code`. The server reads no project file, and it runs no code of a request. So `check_code` gives the error of a missing `locales` key for a sample that uses `Locale` (ADR-0132).

### `surf --help`

Shows the short list of the commands. `surf help` and `-h` do the same.

### `surf --version`

Shows the version. `-V` does the same.

---

## 6. Environment variables

| Variable | Meaning |
|---|---|
| `SURF_HOME` | The folder of the private tools. The default is `~/.surf`. A new version removes the old versions in `toolchain` and `runtime`, and the build results in `target`. A folder with a name that does not start with a digit stays, and `target` stays when Cargo did not make it. |
| `SURF_RELEASES` | The address of the releases. `surf` and `scripts/install.sh` read it. Use it for a mirror. A redirect from an `https` address to `http` is an error. |
| `SURF_VERSION` | `scripts/install.sh` installs this version. The default is the newest release. |
| `CARGO_TARGET_DIR` | The target folder of a native build, in place of the folder in `~/.surf`. |
| `CARGO` | A binary from a checkout runs this program in place of `cargo`. |
| `SURF_SIGN_IDENTITY` | `surf package` on macOS: a certificate of the keychain. Without it, the signature is an ad-hoc one, so the app runs only on this Mac. |
| `SURF_NOTARY_PROFILE` | `surf package` on macOS: a profile of `notarytool`. With it, Apple checks the disk image. |
| `SURF_PFX` | `surf package` for Windows: the `.pfx` file of the code-signing certificate. `--pfx` replaces it. |
| `SURF_PFX_PASSWORD` | `surf package` for Windows: the password of the `.pfx` file. `--pfx-password` replaces it. |
