Skip to content

CLI tools

surreal ext

Available since: v4.0.0

ext is the component of the Surreal CLI for Surrealism extensions: it scaffolds a project, builds it into a .surli archive, inspects and runs the archive, signs it, and installs it into a database. The component is versioned with the database, and its executable is surreal-ext.

Usage
surreal ext [OPTIONS] <COMMAND>
Note

Before SurrealDB 4.0, the same commands were part of the database as surreal db module, also run as surreal module. In 4.0, surreal db module, surreal db extension and surreal module only print where the commands have moved.

SubcommandPurpose
initScaffold a new extension project.
buildBuild the project into a .surli archive.
runRun a function of an archive with arguments.
sigShow a function's signature.
infoList the functions an archive exports.
configRead and change the project's package.toml.
signSign an archive with an SSH key or through Sigstore.
installCheck an archive's signer and capabilities, and define it in a database.
upgradeRedefine an installed extension from a new archive.

An extension is a Rust library whose functions are marked with the #[surrealism] attribute. The commands on this page create the project, compile it and package it, but the functions themselves are Rust code that you write. Building one needs the Rust toolchain and its WebAssembly target:

rustup target add wasm32-wasip2

init creates a project whose src/lib.rs holds three example functions:

src/lib.rs
use surrealism::surrealism;

#[surrealism(default)]
fn hello() -> String {
	format!("Hello from Surrealism!")
}

#[surrealism]
fn greet(name: String) -> String {
	format!("Hello, {name}!")
}

#[surrealism]
fn add(a: i64, b: i64) -> i64 {
	a + b
}

Replace them with your own functions, using any Rust crate that compiles to WebAssembly, then build the project. Once the extension is defined in a database as demo, greet is called as ext::demo::greet('Ada'), and the function marked default as ext::demo(). The quick tutorial walks through a complete extension, and the attribute reference covers database access through a Transaction, init, comments and namespaced exports.

Scaffold a new extension project: a Rust crate with a package.toml, a .cargo/config.toml and the starter src/lib.rs shown in Writing an extension.

Usage
surreal ext init [OPTIONS] [PATH]

Options

NameDescription
[PATH]
Directory to create the project in. The current directory is used when omitted.
--headless
Scaffold without prompting, for use in scripts and CI. Requires --org.
--org<ORG>
Organisation the extension belongs to.
--name<NAME>
Name of the extension. The directory name is used when omitted.
surreal ext init --headless --org acme --name demo ./demo

The package.toml it writes names the package, and lists under [capabilities] what the extension will ask for when it is installed:

package.toml
target = "rust"
abi = 4

[package]
organisation = "acme"
name = "demo"
version = "1.0.0"

[capabilities]
allow_scripting = false
allow_arbitrary_queries = false
allow_write = false
allow_functions = []
allow_net = []
strict_timeout = true

A project from SurrealDB 3.3 or earlier has a surrealism.toml instead, which is still read when there is no package.toml. The .cargo/config.toml holds the tokio_unstable flag, commented out. Uncomment it when a dependency needs Tokio's net feature.

Compile the project's Rust code to WebAssembly (wasm32-wasip2), package it into a .surli archive, and print the archive's digest. The build needs the Rust toolchain described in Writing an extension.

Usage
surreal ext build [OPTIONS] [SOURCE_PATH]

Options

NameDescription
[SOURCE_PATH]
Path to the project. The current directory is used when omitted.
--out, -o<OUT>
Path or file name of the archive. Defaults to <organisation>-<name>-<version>.surli in the project directory.
--debug
Build in debug mode, which skips the optimisation pass for a faster build.
surreal ext build
Sample output
Building WASM module (target: wasm32-wasip2, profile: release)...
Optimizing bundle...
Pre-initialising...
Extracting function signatures...
Packed ./acme-demo-1.0.0.surli (digest sha256:7b36da7d8386c1561b7d61f83f39a3bf590e25b774d935d9f8252604e80134b5)

The build runs the extension's init function once and stores the result in the archive, so every call starts from that state with fresh memory. The digest is the SHA-256 hash of the archive's MANIFEST, which lists every file in the archive with its own hash. A definition's DIGEST pins this value.

Run one function of an archive, without a database. Repeat --arg for each argument, written as a SurrealQL value.

Usage
surreal ext run [OPTIONS] --fnc <FNC> <FILE>

Options

NameDescription
<FILE>
required
Path to the .surli archive.
--fnc<FNC>
required
Name of the function to run.
--arg<ARGS>
A value passed to the function. Repeat it for each argument.
surreal ext run --fnc greet --arg '"Ada"' acme-demo-1.0.0.surli
Output
✅ 'Hello, Ada!'

Show the signature of one function in an archive.

Usage
surreal ext sig [OPTIONS] --fnc <FNC> <FILE>
surreal ext sig --fnc greet acme-demo-1.0.0.surli
Output
Signature:
 - greet(name: string) -> string [readonly]

List the package and the functions an archive exports. <mod> stands for the name the extension is given when it is defined.

Usage
surreal ext info [OPTIONS] <FILE>

Add --json (-j) to print the exports as a JSON document, for use in scripts. sig takes --json too.

surreal ext info acme-demo-1.0.0.surli
Output
Info for @acme/demo@1.0.0
===========================

- <mod>::greet(name: string) -> string [readonly]
- <mod>::add(a: int, b: int) -> int [readonly]
- <mod>() -> string [readonly]

Read and change the project's package.toml. Each subcommand takes --path to name the project directory, which defaults to the current directory, and keeps the file's comments and ordering.

SubcommandPurpose
showPrint the package file.
get <KEY>Print one value, such as package.version or capabilities.allow_net.
set <KEY> <VALUE>Set one value.
add <KEY> <VALUE>Add a value to capabilities.allow_net or capabilities.allow_functions.
remove <KEY> <VALUE>Remove a value from one of those lists.
signer ssh <FILE>Name an SSH public key as the package's signer.
signer github --repo <OWNER/NAME> --workflow <FILE>Name a GitHub Actions workflow as the signer, for Sigstore keyless signing.
signer gitlabName a GitLab CI pipeline as the signer.
signer removeRemove the signer.
fs add <DIR> [--as <PATH>]Bundle a directory into the archive's read-only filesystem.
fs remove <PATH>Remove a bundled directory.

The capabilities requested in package.toml are what install shows and asks to grant. An extension receives only the capabilities that its definition grants.

surreal ext config add capabilities.allow_net api.colour.io:443
surreal ext config add capabilities.allow_functions http::get
surreal ext config set capabilities.allow_write true

Sign an archive, which adds a SIGNATURE file to it. The project's package.toml must name a signer first, with config signer, and the archive must be built after that, so that the signer is part of the signed MANIFEST.

Usage
surreal ext sign [OPTIONS] <FILE>

Options

NameDefaultDescription
<FILE>
required
NoneThe .surli archive to sign.
--key<KEY>
NoneSign with an SSH private key, through ssh-keygen -Y sign, instead of Sigstore.
--bundle<FILE>
NoneImport a Sigstore bundle made over the archive's MANIFEST.
--trusted-root<FILE>
NoneA Sigstore trusted_root.json to verify the signature against. Required with --bundle and with a private Sigstore instance.
--fulcio-url<URL>
https://fulcio.sigstore.devCertificate authority for keyless signing.
--rekor-url<URL>
https://log2025-1.rekor.sigstore.devTransparency log for keyless signing.
--tsa-url<URL>
https://timestamp.sigstore.dev/api/v1/timestampTimestamp authority for keyless signing.
--out, -o<FILE>
NoneWrite the signed archive here instead of replacing the input.

Without --key or --bundle, the command signs keyless through Sigstore, which needs the identity of a CI workflow, such as GitHub Actions. With an SSH key:

surreal ext config signer ssh ~/.ssh/release.pub
surreal ext build
surreal ext sign --key ~/.ssh/release acme-demo-1.0.0.surli
Sample output
Signed acme-demo-1.0.0.surli (sha256:7b36da7d8386c1561b7d61f83f39a3bf590e25b774d935d9f8252604e80134b5)

Signing does not change the digest, because the signature is a separate file that the MANIFEST does not list.

Install an archive into a database. The command reads the archive, shows its package, signer, digest, functions and the capabilities it requests, and asks which to accept. It then runs DEFINE EXTENSION with the DIGEST, SIGNER and CAPABILITIES filled in.

Usage
surreal ext install [OPTIONS] --as <NAME> --namespace <NAMESPACE> --database <DATABASE> <SOURCE>

Options

NameDescription
<SOURCE>
required
silo::<org>::<pkg><x.y.z>, or f"<bucket>:/<path>.surli" for an archive that is already in a bucket of the database.
--as<NAME>
required
The name to call the extension by, as in ext::<name>::<function>().
--trust-signer<SIGNER>
The signer to accept without asking: any, an SSH key fingerprint (SHA256:…), or a Sigstore identity (the workflow URL), optionally followed by its issuer. Any other signer is refused.
--with-capability<CAP>
Grant one requested capability without asking: net:<target>, functions:<pattern>, queries, scripting, write, no-strict-timeout, memory=<bytes> or timeout=<duration>. Repeat it for each. It cannot grant more than the archive requests.
--unsigned
Install without verifying a signature, and record the extension as UNSIGNED. The only way to install an unsigned archive.
--dry-run
Show the review and ask the same questions, then print the statement instead of running it.
--offline
Skip the online checks of a Sigstore signature. The offline verification still runs.
--trust-root<FILE>
A Sigstore trusted_root.json to verify a signature against, for a private Sigstore or an air-gapped install.

The command also takes the usual connection options: --endpoint, --username and --password or --token, --auth-level, --namespace and --database, or the matching SURREAL_* environment variables. The user needs permission to define extensions in the database, and read access to the bucket for a bucket source.

surreal ext install 'f"extensions:/acme-colour-1.0.0.surli"' --as colour \
    -e ws://localhost:8000 -u root -p secret --ns app --db main \
    --trust-signer SHA256:WAbQymSgov5xQFo9qtOeZGglO/HWeqqovf6ztZ0HM7U \
    --with-capability net:api.colour.io:443
Sample output
  Package     acme/colour 1.0.0 from f"extensions:/acme-colour-1.0.0.surli"
  Signed by   key ssh-ed25519 SHA256:WAbQymSgov5xQFo9qtOeZGglO/HWeqqovf6ztZ0HM7U
              ✓ matches package.toml [signer]: key SHA256:WAbQymSgov5xQFo9qtOeZGglO/HWeqqovf6ztZ0HM7U
  Anchor      the key itself (confirm its fingerprint with the publisher)
  Digest      sha256:8be7b49f1f3116e0f723c55ca0c9658452165fd2338995ab36a9ecf3b29c79fa
  Adds        ext::colour::blend, ext::colour::recolour
  Needs       network     api.colour.io:443
  · functions http::get                           requested, not granted (--with-capability grants it)

Trust this signer? yes (--trust-signer)
Capabilities: as granted by --with-capability

Installed ext::colour

Every decision is a named flag, and there is no option that accepts everything. Without a terminal, a run that leaves a decision open fails and names the flag it needs:

Error output
Trust this signer? needs an answer: run in a terminal, or pass --trust-signer <signer|any>

Redefine an installed extension from a new archive with DEFINE EXTENSION OVERWRITE. The command compares the new archive with the installed definition, keeps the PERMISSIONS and COMMENT, and starts from the capabilities granted at install, so that only capabilities the new archive requests beyond the installed one need approval.

Usage
surreal ext upgrade [OPTIONS] --namespace <NAMESPACE> --database <DATABASE> <NAME> [SOURCE]

Options

NameDescription
<NAME>
required
The installed extension's name, as in ext::<name>.
[SOURCE]
The new source. Defaults to the newest Silo release of the installed package, or the installed bucket file read again.
--trust-signer<SIGNER>
As for install.
--allow-signer-change
Accept a signer that differs from the installed one. --trust-signer must still name it.
--allow-downgrade
Allow moving to a lower version than the installed one.
--allow-package-change
Allow moving to another organisation or package.
--with-capability<CAP>
As for install. Without it, a run without a terminal keeps the installed grant and grants nothing new.
surreal ext upgrade colour 'f"extensions:/acme-colour-1.1.0.surli"' \
    -e ws://localhost:8000 -u root -p secret --ns app --db main \
    --trust-signer SHA256:WAbQymSgov5xQFo9qtOeZGglO/HWeqqovf6ztZ0HM7U

Upload each release under its own file name, as above. When a new archive replaces the installed file under the same name, the installed archive can no longer be read, so the command cannot check for a downgrade or a change of package, and in a script it then needs both --allow-downgrade and --allow-package-change.

surreal ext --help
Output
Develop, build, inspect and run Surrealism WASM modules

Usage: surreal-ext [OPTIONS] <COMMAND>

Commands:
  init     Initialize a new Surrealism module project
  run      Run a function with arguments
  sig      Show the function signature
  info     Show the module information
  build    Build a WASM module
  sign     Sign a .surli archive: Sigstore keyless from CI, an SSH key, or an imported bundle
  config   Read and change the project's package.toml
  install  Install an extension into a database: inspect, approve, define
  upgrade  Upgrade an installed extension: compare signer and capabilities, redefine
  help     Print this message or the help of the given subcommand(s)

Options:
  -v, --version  Print version
  -j, --json     Emit machine-readable JSON on stdout
  -h, --help     Print help

Was this page helpful?