Writing Taubyte Functions (Go SDK)
When to use
- Authoring a new Go handler (HTTP / PubSub / P2P)
- Reading or fixing existing function source
- Wiring a function to a database, storage, messaging channel, or library
- Calling another library's exported function from inside a function
- Calling external HTTP services from inside a function
- Producing PubSub messages from an HTTP function
- Surfacing a WebSocket URL to a browser
- Using IPFS-backed files
Mental model
A Taubyte serverless function is a TinyGo-compiled WebAssembly module. The SDK (github.com/taubyte/go-sdk) is a Go wrapper over host functions the Taubyte VM exports. You write Go, compile to WASM (via the taubyte/go-wasi Docker recipe — see verifying-taubyte-functions), and the cloud invokes your exported function for each event.
Key constraints:
- Package name is NOT
main—mainis reserved for the build container. Use any other name (commonlylib). - Each handler is exported with
//export <name>and matchesexecution.callin the function's YAML. - Signature is
func Handler(e event.Event) uint32— returns0on success, non-zero on failure. event.Eventis a tagged union; you call.HTTP(),.PubSub(), or.P2P()to get the typed handle, depending on the function'strigger.type.
Anti-pattern: switching the handler file to package main to "fix" compile errors when the real problem is build hygiene (generated lib/, main.go, bad PATH in .taubyte/build.sh, etc.). Stay on package lib (or another non-main name) per the constraints above; fix the toolchain and repo state instead — see verifying-taubyte-functions and configuring-taubyte-build-runtime.
Entry file name: the scaffold uses empty.go at the function root, and .taubyte/build.sh calls build "${FILENAME}" for that file. Renaming to lib.go (or changing FILENAME) without a fully verified taubyte/go-wasi workflow often breaks remote builds (TinyGo / verify noise, missing artifact.wasm). Prefer package lib in empty.go and a build.sh that exports PATH for go/tinygo then build empty.go (or build "${FILENAME}" with FILENAME=empty.go) — see configuring-taubyte-build-runtime.
Module + imports
go.mod should depend on:
github.com/taubyte/go-sdk
Common imports by feature:
import (
"github.com/taubyte/go-sdk/event"
http "github.com/taubyte/go-sdk/http/event" // inbound HTTP
httpclient "github.com/taubyte/go-sdk/http/client" // outbound HTTP
"github.com/taubyte/go-sdk/database" // KV
"github.com/taubyte/go-sdk/storage" // object storage
pubsubnode "github.com/taubyte/go-sdk/pubsub/node" // PubSub producer
"github.com/taubyte/go-sdk/pubsub" // (consumer types)
p2pnode "github.com/taubyte/go-sdk/p2p/node" // P2P producer/service
"github.com/taubyte/go-sdk/ipfs/client" // IPFS files
)
Handler skeleton (HTTP)
package lib
import (
"github.com/taubyte/go-sdk/event"
http "github.com/taubyte/go-sdk/http/event"
)
func fail(h http.Event, err error, code int) uint32 {
h.Write([]byte(err.Error()))
h.Return(code)
return 1
}
//export Hello
func Hello(e event.Event) uint32 {
h, err := e.HTTP()
if err != nil {
return 1
}
h.Headers().Set("Content-Type", "text/plain")
if _, err := h.Write([]byte("hello")); err != nil {
return fail(h, err, 500)
}
h.Return(200)
return 0
}
The //export Hello symbol must match execution.call: Hello in the function YAML.
Body-then-Return rule (critical)
For HTTP handlers, always in this order:
- Set headers (
h.Headers().Set(...)) - Write body (
h.Write(...)) - Call
h.Return(<status>)last
Reversing this — calling Return before Write — produces empty/broken responses; clients see a "the API does nothing" symptom. This is one of the most common Go-handler footguns.
YAML ↔ Go matcher consistency rule
Every resource that uses a match (database, storage, messaging) is keyed at runtime by the exact same string the Go code passes to New(...) / Channel(...). Mismatches (including a leading /) cause silent "open failed" / consumer never fires.
| Config YAML | Go literal |
|---|---|
databases/<x>.yaml → match: /example/kv |
database.New("/example/kv") |
storages/<x>.yaml → match: /simple/storage |
storage.New("/simple/storage") |
messaging/<x>.yaml → channel.match: chat + PubSub fn trigger.channel: chat |
pubsubnode.Channel("chat") |
Grep database.New(, storage.New(, pubsubnode.Channel( after every YAML edit and confirm each literal exists as a match in the same application.
HTTP event API (http/event)
HttpEvent (received from e.HTTP()):
| Call | Purpose |
|---|---|
Body() → HttpEventBody (Read/Close) |
Request body reader |
Headers() → HttpEventHeaders (Get/Set/List) |
Request/response headers |
Host() → (string, error) |
Request host |
Method() → (string, error) |
HTTP method |
Path() → (string, error) |
Request path |
Query() → HttpQueries (Get/List) |
Query string |
UserAgent() → (string, error) |
UA header |
Write(data []byte) → (int, error) |
Response body write |
Return(code int) → error |
Send status — call LAST |
Read JSON body:
defer h.Body().Close()
var req MyReq
if err := json.NewDecoder(h.Body()).Decode(&req); err != nil {
return fail(h, err, 400)
}
Read query var:
key, err := h.Query().Get("key")
Outbound HTTP client (http/client)
For calling external services from a function:
req, err := httpclient.New(httpclient.Method("GET"), httpclient.Headers(map[string][]string{
"Accept": {"application/json"},
}))
// req.Headers().Set(...), req.Body() to set body, req.Method().Set("POST")
resp, err := req.Do()
data, err := io.ReadAll(resp.Body())
(API uses option functions Method(...), Headers(...), Body(...) and accessor methods Headers(), Method(), Body() for mutation after construction.)
Database (database) — KV
db, err := database.New("/example/kv") // match exactly the YAML "match"
if err != nil { return fail(h, err, 500) }
err = db.Put("hello", []byte("world")) // write
val, err := db.Get("hello") // read -> []byte
keys, err := db.List("prefix/") // list keys with prefix
err = db.Delete("hello") // delete
err = db.Close() // close (optional)
Taubyte databases are instantiated on demand when first opened. With a regex match like /profile/history/[^/]+, opening /profile/history/userA creates a per-user database transparently.
Storage (storage) — object bucket
sto, err := storage.New("/simple/storage") // match exactly the YAML "match"
file := sto.File(filename) // select by name
// Write (overwrite=true to replace existing)
_, err = file.Add([]byte(data), true)
// Read
reader, err := file.GetFile() // io.ReadCloser
defer reader.Close()
io.Copy(h, reader) // stream into HTTP response
// Versions / delete
versions, _ := file.Versions()
_ = file.Delete(version)
_ = file.DeleteAllVersions()
// Bucket-level
files, _ := sto.ListFiles()
remaining, _ := sto.RemainingCapacity()
PubSub (pubsub, pubsub/node)
Consumer (function with trigger.type: pubsub)
//export OnChat
func OnChat(e event.Event) uint32 {
ps, err := e.PubSub()
if err != nil { return 1 }
data, _ := ps.Data() // []byte payload
ch, _ := ps.Channel() // *ChannelObject
name := ch.Name()
_ = name; _ = data
return 0
}
In the function YAML:
trigger:
type: pubsub
channel: chat # must equal messaging channel.match AND pubsubnode.Channel("chat")
execution:
call: OnChat
Producer (called from any function — typically an HTTP handler)
import pubsubnode "github.com/taubyte/go-sdk/pubsub/node"
ch, err := pubsubnode.Channel("chat") // matches messaging channel.match
err = ch.Publish([]byte(payload))
WebSocket URL (browser handoff)
ch, err := pubsubnode.Channel("chat")
ws, err := ch.WebSocket()
u, err := ws.Url() // url.URL — return to client
h.Write([]byte(u.String()))
Requires bridges.websocket.enable: true in the messaging YAML.
YAML wiring (must all match)
messaging.<x>.yaml: channel.match: chat
functions.<consumer>.yaml: trigger.channel: chat
Go consumer subscribes implicitly via trigger
Go producer: pubsubnode.Channel("chat")
P2P (p2p/event, p2p/node)
Consumer (function with trigger.type: p2p)
//export OnP2P
func OnP2P(e event.Event) uint32 {
p, err := e.P2P()
if err != nil { return 1 }
cmd, _ := p.Command() // command name
proto, _ := p.Protocol() // protocol path
data, _ := p.Data() // payload
p.Write([]byte("ack")) // reply
_ = cmd; _ = proto; _ = data
return 0
}
Function YAML for a P2P handler:
trigger:
type: p2p
protocol: /myproto/v1
command: ping
execution:
call: OnP2P
Producer (call another node's P2P command)
svc := p2pnode.New("/myproto/v1")
cmd, err := svc.Command("ping")
resp, err := cmd.Send([]byte("hello")) // returns response bytes
A service that wants to listen:
proto, err := svc.Listen() // tells the node to accept this protocol
IPFS files (ipfs/client)
c, err := client.New()
// Write a new file → push to IPFS → get a CID
w, err := c.Create()
w.Write([]byte("hello ipfs"))
cidv, err := w.Push() // returns cid.Cid
// Read by CID
r, err := c.Open(cidv) // ReadOnlyContent (io.ReadSeekCloser + Cid())
data, err := io.ReadAll(r)
r.Close()
ReadWriteContent is io.ReadWriteSeeker + io.Closer + Push() (cid.Cid, error).
Importing another library's exported function
When a function lives in the code repo but needs to call code in a separate library repo, use //go:wasmimport:
// libraries/<library_resource_name> is the source.path of the library YAML
//go:wasmimport libraries/tauhow_example_library add
func add(a, b uint32) uint64
Then use add like any Go function. The path before the symbol mirrors the library's source.path from libraries/<x>.yaml. Resolution is application-scoped first, then global.
The library itself just exports its function the normal way:
package lib
//export add
func add(a, b uint32) uint64 {
return uint64(a) + uint64(b)
}
Function YAML cheatsheet (relevant fields)
description: <one-liner>
source: . # ".": inline (code repo); else "libraries/<name>"
trigger:
type: https | http | pubsub | p2p
method: GET | POST | PUT | DELETE # http(s) only
paths: [ /api/x ] # http(s) only
channel: <channel> # pubsub only
protocol: <proto> # p2p only
command: <cmd> # p2p only
domains: [ <domain_name> ] # http(s) only
execution:
timeout: 30s
memory: 64MB
call: <ExportedSymbol> # MUST equal //export <name>
Required project plumbing
- The function YAML's
execution.callexactly matches a//export <name>symbol. - Function root layout (Go):
go.mod+ source.gofiles at the function root. Never create alib/subdirectory or hand-authormain.go. - For libraries that another function imports via
//go:wasmimport, thesource.pathinlibraries/<x>.yamlmust reflect the actual on-disk path inside the library repo where the sources live. - For storage/database/messaging matchers: see the YAML ↔ Go consistency table above.
Workflow checklist
Function authoring progress:
- [ ] Resource YAML created via tau new function (creating-taubyte-resources)
- [ ] Package name is NOT `main`
- [ ] Each handler has //export <name> matching execution.call
- [ ] HTTP handlers: Headers().Set -> Write -> Return(status), in that order
- [ ] database.New / storage.New / pubsubnode.Channel literals match YAML "match"/"channel" exactly
- [ ] If using //go:wasmimport, library source.path matches the library repo layout
- [ ] Local WASM verify (verifying-taubyte-functions Docker recipe), if requested
- [ ] git push code (and library/website if applicable)
- [ ] Dream: dream inject push-specific for the changed repo (triggering-dream-builds)
Gotchas
- Empty body on success:
Return(200)was called beforeWrite([]byte(...)). Reorder. database.New("...") error: matcher mismatch withdatabases/<x>.yaml— including a stray leading/.- PubSub consumer never fires:
trigger.channel≠ messagingchannel.match≠pubsubnode.Channel("..."). All three strings must be identical. WebSocket().Url()returns empty: messaging YAML missingbridges.websocket.enable: true.- Build error "package main not allowed": rename your package to anything other than
main(commonlylib). //exportsymbol not found in WASM: typo between//exportand YAMLexecution.call; or the symbol is unexported (lowercase first letter is fine —//exportcontrols export, not Go's identifier case).- Library import path wrong:
//go:wasmimport libraries/<name> <fn>—<name>is the library resource name (matches the YAML filename / library identity), not a Go package import path. - Returning early on errors loses status: write the error body and call
h.Return(code)before thereturn <non-zero>(see thefailhelper above).
Reference docs
- Module: github.com/taubyte/go-sdk
- Top-level docs: tau.how
- Functions: tau.how/development/functions
- Libraries: tau.how/development/libraries
- Databases: tau.how/development/databases
- Storage: tau.how/development/storage
Related skills
creating-taubyte-resources—tau new functionflag patterns and YAML scaffoldingverifying-taubyte-functions— local Go WASM build via thetaubyte/go-wasiDocker recipebuilding-taubyte-websites— companion build rule (/out) for the website sidetriggering-dream-builds— push the function to Dream after editingpushing-taubyte-projects— push code/library/website repos