# Build a WASM extension in Go

This tutorial walks through building a minimal Synapse proxy extension in Go, compiling it to WebAssembly, and loading it into a running Synapse instance. For the payload shapes and full list of exports for every extension type, see [Developing Webassembly extensions](https://docs.cerbos.dev/synapse/latest/extensions/wasm-development).

Synapse loads WASM modules using the [Extism](https://extism.org/) runtime. This guide uses the [Extism Go PDK](https://github.com/extism/go-pdk), which provides convenience wrappers for Go developers. There are versions of the PDK available for [several other languages](https://extism.org/docs/concepts/pdk/). If your preferred language doesn’t have a PDK but supports compiling the code into a WASI reactor, it’s still possible to develop a Synapse extension by implementing the Extism ABI in your code. Details of that are beyond the scope of this documentation.

|     |     |
| --- | --- |
|  | Synapse creates multiple instances of each extension to serve parallel requests. Each instance has its own globals and is isolated from the other instances. The only way to share state is through [the built-in cache](https://docs.cerbos.dev/synapse/latest/extensions/wasm-development#host-funcs) or through an external data store. |

## Before you begin

You need:

- [Go 1.24](https://go.dev/dl/) or later
- Docker
- A Synapse licence. If you do not already have one, sign up at [cerbos.dev/workshop](/content/workshop/index.html). Your credentials are issued together with the URL of the Cerbos distribution repository; substitute that URL wherever `CERBOS_DISTRIBUTION_REPO` appears below.
- `curl` and a JSON formatter such as `python3 -m json.tool` or `jq` (optional).

Log in to the Cerbos distribution repository:

```console
$ docker login CERBOS_DISTRIBUTION_REPO --username=YOUR_LICENCE_USER --password=YOUR_LICENCE_KEY
```

## Create the project

```console
$ mkdir -p wasm-go-tutorial/policies wasm-go-tutorial/extensions wasm-go-tutorial/src
$ cd wasm-go-tutorial/src
$ go mod init example.com/synapse-extension
$ go get github.com/extism/go-pdk@latest
$ go get github.com/tidwall/sjson@latest
$ cd ..
```

`go-pdk` provides the Extism input/output helpers and memory allocator. `sjson` is used to modify the request JSON without defining Go structs for every field.

## Write the extension

Create `src/main.go`:

```go
package main

import (
    "github.com/extism/go-pdk"
    "github.com/tidwall/sjson"
)

//go:wasmexport cerbosInit (1)
func cerbosInit() int32 { return 0 }

//go:wasmexport cerbosDeinit
func cerbosDeinit() int32 { return 0 }

//go:wasmexport augmentCheckRequest (2)
func augmentCheckRequest() int32 {
    input := pdk.Input() (3)

modified, err := sjson.SetBytes(input, "principal.attr.company", "Romaguera-Crona")
    if err != nil {
        pdk.SetError(err)
        return 1
    }

pdk.Output(modified) (4)
    return 0
}

//go:wasmexport augmentCheckResponse
func augmentCheckResponse() int32 {
    pdk.Output(pdk.Input())
    return 0
}

func main() {} (5)
```

|     |     |
| --- | --- |
| **1** | The `//go:wasmexport` directive controls the WASM symbol name. It must match the Synapse ABI exactly (`cerbosInit`, `augmentCheckRequest`, and so on). The Go function name can differ. |
| **2** | One of the proxy extension hooks. Synapse invokes it before every `CheckResources` request reaches the PDP. |
| **3** | `pdk.Input()` returns the JSON-encoded request as `[]byte`. |
| **4** | `pdk.Output` is mandatory on every successful invocation, even for pass-through. Returning `0` without output would be considered an error. |
| **5** | Required by the Go toolchain but never called when the module runs as an Extism reactor. Leave it empty. |

## Build the module

```console
$ GOOS=wasip1 GOARCH=wasm go build -C src -o ../extensions/proxy.wasm -buildmode=c-shared main.go
```

`GOOS=wasip1 GOARCH=wasm` targets WebAssembly with the WASI preview 1 ABI. `-buildmode=c-shared` produces a reactor module that exports its symbols instead of running `main` and exiting.

|     |     |
| --- | --- |
|  | [TinyGo](https://tinygo.org/) produces modules roughly 10× smaller: `tinygo build -o extensions/proxy.wasm -target wasip1 -buildmode=c-shared src/main.go`. |

## Write a policy

Create `policies/invoice.yaml`:

```yaml
apiVersion: api.cerbos.dev/v1
resourcePolicy:
  resource: invoice
  version: default
  rules:
    - actions: [view]
      effect: EFFECT_ALLOW
      roles: [employee]
      condition:
        match:
          expr: request.resource.attr.company == request.principal.attr.company
```

## Configure and run Synapse

Create `config.yaml`:

```yaml
server:
  listenAddress: ":3594"

pdp:
  inProcess:
    storage:
      driver: "disk"
      disk:
        directory: /policies
    audit:
      enabled: true
      backend: file
      file:
        path: stdout

extensions:
  proxyExtensions:
    principalEnricher: (1)
      extensionURL: /extensions/proxy.wasm (2)
      required: true (3)
```

|     |     |
| --- | --- |
| **1** | Name for this extension as it appears in logs. |
| **2** | Path inside the container. The `.wasm` suffix tells Synapse to load the file with the WASM runtime. See [Extension URLs](https://docs.cerbos.dev/synapse/latest/extensions/url-format) for other supported schemes. |
| **3** | A failure inside a required extension short-circuits the chain and returns an error to the caller. Set this to `false` for advisory enrichment. |

Start the container:

```console
$ docker run --rm --name synapse-wasm-go -p 3594:3594 \
    -v $(pwd)/config.yaml:/config/config.yaml:ro \
    -v $(pwd)/policies:/policies:ro \
    -v $(pwd)/extensions:/extensions:ro \
    CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \
    server --conf.path=/config/config.yaml --log.level=debug
```

Logs should show:

```json
{"severity":"DEBUG","extension":"principalEnricher","runtime":"wasm","message":"Downloaded extension"}
```

In a second terminal, send a request without a `principal.attr` block:

```console
$ curl -s -X POST http://localhost:3594/api/check/resources \
    -H 'Content-Type: application/json' \
    -d '{
          "requestId": "wasm-go-001",
          "principal": {"id": "1", "roles": ["employee"]},
          "resources": [{\
            "actions": ["view"],\
            "resource": {"kind": "invoice", "id": "inv-42",\
                         "attr": {"company": "Romaguera-Crona"}}\
          }]
        }'
```

Response

```json
{
  "requestId": "wasm-go-001",
  "results": [\
    { "resource": {"id": "inv-42", "kind": "invoice"},\
      "actions": {"view": "EFFECT_ALLOW"} }\
  ]
}
```

The extension injected `principal.attr.company` before the PDP evaluated the policy. Remove the extension or change the value and the decision flips to `EFFECT_DENY`.

## Language-specific notes

- Export names are case-sensitive and must match the Synapse ABI exactly. The `//go:wasmexport` directive controls the symbol.
- Only export the hooks you implement. Missing exports are ignored. Hooks that are expected to produce output must do so using `pdk.Output` even if it’s simply echoing the input.
- Host functions are declared with `//go:wasmimport extism:host/user <name>`, invoked by passing shared-memory offsets, and read back with `pdk.FindMemory`. Full list and signatures: [extensions/wasm-development.adoc#host-funcs](https://docs.cerbos.dev/synapse/latest/extensions/wasm-development#host-funcs).
- `encoding/json` works inside WASM but produces large binaries. `gjson` / `sjson` keep the binary small when you only need to read or mutate a few fields.
- `pdk.Log(pdk.LogDebug, "msg")` writes to the Synapse log, tagged with the extension name. Levels below `--log.level` are dropped on the host side.
- Outbound HTTP via `pdk.NewHTTPRequest` is blocked unless the target is listed in `allowedHosts` in `config.yaml`.

## Going further

Refer to [Developing Webassembly extensions](https://docs.cerbos.dev/synapse/latest/extensions/wasm-development) for details about the functions that should be exported by different extension types and common operations such as calling host functions.
