Skip to the content

CLI

The surf command

Each command and each option of `surf`.This page as Markdown
Statuscurrent on 2026-11-06, for version 0.
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).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 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, 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 (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 $passwordExport-Certificate -Cert $cert -FilePath test.cerImport-Certificate -FilePath test.cer -CertStoreLocation Cert:\LocalMachine\TrustedPeople$env:SURF_PFX = 'test.pfx'$env:SURF_PFX_PASSWORD = '<the password>'surf package --os windowsAdd-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.