> Full SurrealDB documentation index: https://surrealdb.com/docs/llms.txt

# surreal ext

The ext component of the Surreal CLI scaffolds, builds, inspects, runs, signs, installs and upgrades Surrealism extensions packaged as .surli archives, and edits a project's package.toml.

_(since v4.0.0)_

`ext` is the component of the [Surreal CLI](/docs/reference/cli.md) for [Surrealism](/docs/learn/extensions/plugins/overview.md) 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`.

<Synopsis>
surreal ext [OPTIONS] <COMMAND>
</Synopsis>

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

| Subcommand | Purpose |
| --- | --- |
| [`init`](#ext-init) | Scaffold a new extension project. |
| [`build`](#ext-build) | Build the project into a `.surli` archive. |
| [`run`](#ext-run) | Run a function of an archive with arguments. |
| [`sig`](#ext-sig) | Show a function's signature. |
| [`info`](#ext-info) | List the functions an archive exports. |
| [`config`](#ext-config) | Read and change the project's `package.toml`. |
| [`sign`](#ext-sign) | Sign an archive with an SSH key or through Sigstore. |
| [`install`](#ext-install) | Check an archive's signer and capabilities, and define it in a database. |
| [`upgrade`](#ext-upgrade) | Redefine an installed extension from a new archive. |

## Writing an extension

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](https://www.rust-lang.org/tools/install) and its WebAssembly target:

```bash
rustup target add wasm32-wasip2
```

[`init`](#ext-init) creates a project whose `src/lib.rs` holds three example functions:

```rust title="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`](#ext-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](/docs/learn/extensions/plugins/quick-tutorial.md) walks through a complete extension, and the [attribute reference](/docs/learn/extensions/guides/surrealism-attribute-reference.md) covers database access through a `Transaction`, `init`, comments and namespaced exports.

## surreal ext init {#ext-init}

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](#writing-an-extension).

<Synopsis>
surreal ext init [OPTIONS] [PATH]
</Synopsis>

<OptionsTable
    title="Options"
    options={[
        { "name": "[PATH]", "description": "Directory to create the project in. The current directory is used when omitted." },
        { "name": "--headless", "description": "Scaffold without prompting, for use in scripts and CI. Requires `--org`." },
        { "name": "--org", "value": "<ORG>", "description": "Organisation the extension belongs to." },
        { "name": "--name", "value": "<NAME>", "description": "Name of the extension. The directory name is used when omitted." }
    ]}
/>

```bash
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:

```toml title="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.

## surreal ext build {#ext-build}

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](#writing-an-extension).

<Synopsis>
surreal ext build [OPTIONS] [SOURCE_PATH]
</Synopsis>

<OptionsTable
    title="Options"
    options={[
        { "name": "[SOURCE_PATH]", "description": "Path to the project. The current directory is used when omitted." },
        { "name": "--out", "short": "-o", "value": "<OUT>", "description": "Path or file name of the archive. Defaults to `<organisation>-<name>-<version>.surli` in the project directory." },
        { "name": "--debug", "description": "Build in debug mode, which skips the optimisation pass for a faster build." }
    ]}
/>

```bash
surreal ext build
```

```text title="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`](/docs/reference/query-language/statements/define/extension.md#digest) pins this value.

## surreal ext run {#ext-run}

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

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

<OptionsTable
    title="Options"
    options={[
        { "name": "<FILE>", "required": true, "description": "Path to the `.surli` archive." },
        { "name": "--fnc", "value": "<FNC>", "required": true, "description": "Name of the function to run." },
        { "name": "--arg", "value": "<ARGS>", "description": "A value passed to the function. Repeat it for each argument." }
    ]}
/>

```bash
surreal ext run --fnc greet --arg '"Ada"' acme-demo-1.0.0.surli
```

```text title="Output"
✅ 'Hello, Ada!'
```

## surreal ext sig {#ext-sig}

Show the signature of one function in an archive.

<Synopsis>
surreal ext sig [OPTIONS] --fnc <FNC> <FILE>
</Synopsis>

```bash
surreal ext sig --fnc greet acme-demo-1.0.0.surli
```

```text title="Output"
Signature:
 - greet(name: string) -> string [readonly]
```

## surreal ext info {#ext-info}

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

<Synopsis>
surreal ext info [OPTIONS] <FILE>
</Synopsis>

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

```bash
surreal ext info acme-demo-1.0.0.surli
```

```text title="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]
```

## surreal ext config {#ext-config}

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.

| Subcommand | Purpose |
| --- | --- |
| `show` | Print 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 gitlab` | Name a GitLab CI pipeline as the signer. |
| `signer remove` | Remove 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`](#ext-install) shows and asks to grant. An extension receives only the capabilities that its definition grants.

```bash
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
```

## surreal ext sign {#ext-sign}

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

<Synopsis>
surreal ext sign [OPTIONS] <FILE>
</Synopsis>

<OptionsTable
    title="Options"
    options={[
        { "name": "<FILE>", "required": true, "description": "The `.surli` archive to sign." },
        { "name": "--key", "value": "<KEY>", "description": "Sign with an SSH private key, through `ssh-keygen -Y sign`, instead of Sigstore." },
        { "name": "--bundle", "value": "<FILE>", "description": "Import a Sigstore bundle made over the archive's `MANIFEST`." },
        { "name": "--trusted-root", "value": "<FILE>", "description": "A Sigstore `trusted_root.json` to verify the signature against. Required with `--bundle` and with a private Sigstore instance." },
        { "name": "--fulcio-url", "value": "<URL>", "default": "https://fulcio.sigstore.dev", "description": "Certificate authority for keyless signing." },
        { "name": "--rekor-url", "value": "<URL>", "default": "https://log2025-1.rekor.sigstore.dev", "description": "Transparency log for keyless signing." },
        { "name": "--tsa-url", "value": "<URL>", "default": "https://timestamp.sigstore.dev/api/v1/timestamp", "description": "Timestamp authority for keyless signing." },
        { "name": "--out", "short": "-o", "value": "<FILE>", "description": "Write 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:

```bash
surreal ext config signer ssh ~/.ssh/release.pub
surreal ext build
surreal ext sign --key ~/.ssh/release acme-demo-1.0.0.surli
```

```text title="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.

## surreal ext install {#ext-install}

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`](/docs/reference/query-language/statements/define/extension.md) with the `DIGEST`, `SIGNER` and `CAPABILITIES` filled in.

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

<OptionsTable
    title="Options"
    options={[
        { "name": "<SOURCE>", "required": true, "description": "`silo::<org>::<pkg><x.y.z>`, or `f\"<bucket>:/<path>.surli\"` for an archive that is already in a bucket of the database." },
        { "name": "--as", "value": "<NAME>", "required": true, "description": "The name to call the extension by, as in `ext::<name>::<function>()`." },
        { "name": "--trust-signer", "value": "<SIGNER>", "description": "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." },
        { "name": "--with-capability", "value": "<CAP>", "description": "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." },
        { "name": "--unsigned", "description": "Install without verifying a signature, and record the extension as `UNSIGNED`. The only way to install an unsigned archive." },
        { "name": "--dry-run", "description": "Show the review and ask the same questions, then print the statement instead of running it." },
        { "name": "--offline", "description": "Skip the online checks of a Sigstore signature. The offline verification still runs." },
        { "name": "--trust-root", "value": "<FILE>", "description": "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.

```bash
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
```

```text title="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:

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

## surreal ext upgrade {#ext-upgrade}

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.

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

<OptionsTable
    title="Options"
    options={[
        { "name": "<NAME>", "required": true, "description": "The installed extension's name, as in `ext::<name>`." },
        { "name": "[SOURCE]", "description": "The new source. Defaults to the newest Silo release of the installed package, or the installed bucket file read again." },
        { "name": "--trust-signer", "value": "<SIGNER>", "description": "As for `install`." },
        { "name": "--allow-signer-change", "description": "Accept a signer that differs from the installed one. `--trust-signer` must still name it." },
        { "name": "--allow-downgrade", "description": "Allow moving to a lower version than the installed one." },
        { "name": "--allow-package-change", "description": "Allow moving to another organisation or package." },
        { "name": "--with-capability", "value": "<CAP>", "description": "As for `install`. Without it, a run without a terminal keeps the installed grant and grants nothing new." }
    ]}
/>

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

## Command help

```bash
surreal ext --help
```

```text title="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
```

## Further reading

- [Surrealism overview](/docs/learn/extensions/plugins/overview.md): the end-to-end workflow
- [`DEFINE EXTENSION`](/docs/reference/query-language/statements/define/extension.md): `DIGEST`, `SIGNER`, `UNSIGNED` and `CAPABILITIES`
- [Creating custom extensions](/docs/learn/extensions/guides/creating-custom-extensions.md): attributes, `package.toml` and bundled filesystems
