SDK Quick Start — bRRAIn Docs
Write your first Platform SDK program: build a client, write a vault file, read it back and list its folder, and test it locally against a fake pod.
SDK Quick Start
In this guide you write a small program that writes a note into your extension's vault namespace, reads it back and lists the folder, then test it on your own machine against a fake pod. About 15 minutes.
Prerequisites
- A module set up as in Installation: developer-seat access, Go 1.25.9 or later, and the
requireplusreplacelines ingo.mod. - Basic Go: modules,
context,net/http/httptest.
1. Write the program
Create cmd/hello-notes/main.go:
package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"time"
platformsdk "github.com/Qosil/bRRAIn/pkg/platform-sdk"
)
const slug = "hello-notes"
func mustEnv(name string) string {
v := strings.TrimSpace(os.Getenv(name))
if v == "" {
log.Fatalf("%s: %s is required (the supervisor sets it)", slug, name)
}
return v
}
func main() {
c, err := platformsdk.New(platformsdk.Options{
BaseURL: mustEnv("BRRAIN_INTERNAL_URL"),
Token: mustEnv("BRRAIN_INTERNAL_TOKEN"),
// AppSlug is left empty: New reads BRRAIN_APP_SLUG, which the
// supervisor sets to your manifest's slug.
})
if err != nil {
log.Fatalf("%s: build SDK client: %v", slug, err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := run(ctx, c, time.Now().UTC()); err != nil {
log.Fatalf("%s: %v", slug, err)
}
}
// run writes a note into the extension's own namespace, reads it back,
// and lists the folder it lives in.
func run(ctx context.Context, c *platformsdk.Client, now time.Time) error {
dir := "extensions/" + slug + "/notes"
path := dir + "/" + now.Format("2006-01-02") + "-hello.md"
note := fmt.Sprintf(`---
title: "Hello from %s"
date: %s
author: "[AI] %s"
tags: []
---
First note written through Platform SDK %s.
`, slug, now.Format("2006-01-02"), slug, platformsdk.SDKVersion)
if err := c.Vault().Write(ctx, platformsdk.VaultWriteRequest{
Path: path,
Content: []byte(note),
}); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
body, err := c.Vault().ReadBytes(ctx, path)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
fmt.Printf("read back %d bytes from %s\n", len(body), path)
entries, err := c.Vault().List(ctx, dir)
if err != nil {
return fmt.Errorf("list %s: %w", dir, err)
}
for _, e := range entries {
fmt.Printf(" %s (%d bytes)\n", e.Name, e.Size)
}
return nil
}
What to notice:
- Configuration comes from the supervisor.
BRRAIN_INTERNAL_URLandBRRAIN_INTERNAL_TOKENare set for your process when the pod starts it. The program fails fast with a clear message if either is missing. - One client, many calls.
c.Vault()is cheap; build the client once. - Your namespace.
extensions/<slug>/is private to your extension. Other extensions get 403 there. - A complete file. The note carries a title, a date, an honest
[AI]author and atags:property (empty here; see Vault paths and POPE tags). ReadBytes, notRead. File content travels base64-encoded.ReadBytesreturns the stored bytes;Readreturns the raw wire value.
2. Test it without a pod
The SDK is plain HTTP, so you can serve the pod's wire format from httptest and run the real client against it. Create cmd/hello-notes/main_test.go:
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path"
"strings"
"testing"
"time"
platformsdk "github.com/Qosil/bRRAIn/pkg/platform-sdk"
)
// fakePod serves the three internal routes run() uses, in the same wire
// format as a brain pod: file content travels as base64 ([]byte in JSON).
func fakePod(t *testing.T) *httptest.Server {
t.Helper()
files := map[string][]byte{}
mux := http.NewServeMux()
auth := func(w http.ResponseWriter, r *http.Request) bool {
if r.Header.Get("Authorization") != "Bearer test-token" {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return false
}
return true
}
mux.HandleFunc("POST /internal/vault/file", func(w http.ResponseWriter, r *http.Request) {
if !auth(w, r) {
return
}
var req platformsdk.VaultWriteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
files[req.Path] = req.Content
w.Write([]byte(`{"status":"ok"}`))
})
mux.HandleFunc("GET /internal/vault/file", func(w http.ResponseWriter, r *http.Request) {
if !auth(w, r) {
return
}
p := r.URL.Query().Get("path")
body, ok := files[p]
if !ok {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(map[string]any{"path": p, "content": body})
})
mux.HandleFunc("GET /internal/vault/list", func(w http.ResponseWriter, r *http.Request) {
if !auth(w, r) {
return
}
dir := r.URL.Query().Get("path")
var entries []platformsdk.VaultEntry
for p, b := range files {
if path.Dir(p) == dir {
entries = append(entries, platformsdk.VaultEntry{Path: p, Name: path.Base(p), Size: int64(len(b))})
}
}
json.NewEncoder(w).Encode(platformsdk.VaultListResponse{Path: dir, Entries: entries})
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return srv
}
func TestRun(t *testing.T) {
srv := fakePod(t)
c, err := platformsdk.New(platformsdk.Options{
BaseURL: srv.URL,
Token: "test-token",
AppSlug: "hello-notes", // explicit in tests; the supervisor sets it in production
})
if err != nil {
t.Fatal(err)
}
now := time.Date(2026, 10, 1, 9, 0, 0, 0, time.UTC)
if err := run(context.Background(), c, now); err != nil {
t.Fatalf("run: %v", err)
}
}
func TestRunWrongToken(t *testing.T) {
srv := fakePod(t)
c, _ := platformsdk.New(platformsdk.Options{BaseURL: srv.URL, Token: "wrong"})
err := run(context.Background(), c, time.Now())
if err == nil || !strings.Contains(err.Error(), "returned 401") {
t.Fatalf("want a 401 APIError, got %v", err)
}
}
Run it:
go test ./cmd/hello-notes -v
Both tests pass. The second one shows what a wrong token looks like: an *platformsdk.APIError whose message reads platformsdk: POST /internal/vault/file returned 401: ....
3. Run it on a pod
The internal API is served to extensions on the pod; it is not a public endpoint. To run against real data, package the program as an extension (a manifest.json naming your slug, binary, port variable and the sub-clients you call: here vault.read, vault.write, vault.list) and install it on a development pod. The supervisor then supplies BRRAIN_INTERNAL_URL, BRRAIN_INTERNAL_TOKEN and BRRAIN_APP_SLUG. A long-running extension serves HTTP; see Go with net/http for the server shape, and the course's packaging module for the manifest, install and update.
Troubleshooting
| Symptom | Meaning |
| --- | --- |
| platformsdk: BaseURL is required or BaseURL must be absolute | BRRAIN_INTERNAL_URL is empty or has no scheme |
| APIError 401 | Missing or wrong token |
| APIError 403 on a vault path | The path is in another extension's namespace or a platform-reserved one |
| APIError 404 on read | The file does not exist yet; often a normal first-run branch |
| A connection refused transport error | Nothing is listening at BRRAIN_INTERNAL_URL: the pod is not ready or the URL is wrong |
| json: cannot unmarshal string into Go value | You passed Read's raw result to json.Unmarshal; use ReadBytes |