Agent index (llms.txt) — fetch this first, then the page it names.
Go Client SDK
The Orama Network Go Client SDK provides a clean, type-safe Go interface for interacting with the Orama Network. It abstracts away the complexity of peer connections, authentication, and error handling.
For TypeScript, see the SDK Reference. Both talk to the same gateway; use whichever matches the code you are writing. Humans use the CLI, not a dashboard: see the CLI Reference.
Installation
go get github.com/DeBrosOfficial/network/pkg/client
Quick Start
package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"github.com/DeBrosOfficial/network/pkg/client"
)
func main() {
// Create client configuration
cfg := client.DefaultClientConfig("my-app")
cfg.GatewayURL = "https://ns-myapp.cluster.example.com"
cfg.JWT = os.Getenv("ORAMA_ACCESS_TOKEN") // a signed-in user's token: storage refuses a key on its own
// Create client
c, err := client.NewClient(cfg)
if err != nil {
log.Fatal(err)
}
// Connect to the network
if err := c.Connect(); err != nil {
log.Fatal(err)
}
defer c.Disconnect()
// Use the client
ctx := context.Background()
// Upload to storage
resp, err := c.Storage().Upload(ctx, strings.NewReader("Hello, Orama!"), "hello.txt")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Uploaded: CID=%s\n", resp.Cid)
}Configuration
ClientConfig
type ClientConfig struct {
AppName string // Application name (required)
DatabaseName string // Database name (default: "<AppName>_db")
BootstrapPeers []string // LibP2P bootstrap peer multiaddresses
DatabaseEndpoints []string // RQLite nodes to dial directly (in-mesh only); empty = through the gateway
DatabaseReadLevel string // rqlite read consistency: "none" (default when empty) or "weak"
GatewayURL string // Gateway URL for HTTP API access
ConnectTimeout time.Duration // Connection timeout (default: 30s)
RetryAttempts int // Retry attempts (default: 3)
RetryDelay time.Duration // Delay between retries (default: 5s)
QuietMode bool // Suppress debug/info logs
APIKey string // API key for gateway auth
JWT string // Optional JWT bearer token
IdentityPath string // Path to persistent LibP2P identity key file
PubSubSocket string // Unix socket of the node's pubsub API (orama-namespace-pubsub@index); admits only the gateways' user
ListenAddrs []string // LibP2P listen multiaddrs; empty = no listener
}The client only dials its bootstrap peers, so by default its LibP2P host has
no listener at all. Set ListenAddrs to accept inbound connections on a named
interface (for example a node's WireGuard address, /ip4/10.0.0.3/tcp/0);
Connect refuses an unspecified address such as /ip4/0.0.0.0/..., which is
every interface, the public one included.
Connect dials the bootstrap peers once and returns even when none answers;
the client then redials every bootstrap peer that is not connected every five
seconds until Disconnect, so a peer that was not listening yet (a gateway
starts in the same second as the node it bootstraps from), or that restarted,
is connected as soon as it is back. The first failure of each peer is logged at
warn level and its recovery at info level, not every attempt.
Creating a Client
// Default configuration (app name is required)
cfg := client.DefaultClientConfig("my-app")
cfg.GatewayURL = "https://ns-myapp.cluster.example.com"
cfg.APIKey = os.Getenv("ORAMA_API_KEY")
c, err := client.NewClient(cfg)
if err != nil {
log.Fatal(err)
}DefaultClientConfig fills in default bootstrap peers and leaves DatabaseEndpoints empty; NewClient returns an error if the config is nil or the app name is empty.
DatabaseReadLevel is the rqlite consistency level reads are served at. client.ReadLevelNone (the default) reads this node's local replica: fastest, but it can miss a write the leader has acknowledged and this node has not yet applied. client.ReadLevelWeak routes the read to the leader, which has every acknowledged write, at the cost of one hop when this node is not the leader.
Authentication
Set cfg.APIKey or cfg.JWT. Which one a call needs depends on its route: storage wants a signed-in user's token (or a deployed app's workload token), and the database routes below want an admin key or a wallet holding the developer role or above (Access control). The client's namespace is derived from its credential.
API Key Authentication
cfg := client.DefaultClientConfig("my-app")
cfg.APIKey = "your-api-key-here"
c, err := client.NewClient(cfg)JWT Token Authentication
cfg := client.DefaultClientConfig("my-app")
cfg.JWT = "your-jwt-token-here"
c, err := client.NewClient(cfg)Obtaining Credentials
// 1. Login with wallet signature (not implemented in the Go client) // Use the gateway API directly: POST /v1/auth/challenge + /v1/auth/verify // (see Sign-in and sessions: /docs/developer/sign-in) // 2. Issue API key after authentication // POST /v1/auth/api-key with JWT token
Storage Client
Upload, download, pin, and unpin files to IPFS.
Upload File
Upload takes an io.Reader:
resp, err := c.Storage().Upload(ctx, strings.NewReader("Hello, World!"), "hello.txt")
if err != nil {
log.Fatal(err)
}
fmt.Printf("CID: %s, Size: %d\n", resp.Cid, resp.Size)Get File
Get returns an io.ReadCloser:
cid := "QmXxx..."
rc, err := c.Storage().Get(ctx, cid)
if err != nil {
log.Fatal(err)
}
defer rc.Close()
data, err := io.ReadAll(rc)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Downloaded %d bytes\n", len(data))Pin File
Pin takes the CID and a name:
cid := "QmXxx..."
resp, err := c.Storage().Pin(ctx, cid, "hello.txt")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Pinned: %s\n", resp.Cid)Unpin File
cid := "QmXxx..."
err := c.Storage().Unpin(ctx, cid)
if err != nil {
log.Fatal(err)
}
fmt.Println("Unpinned successfully")By default a successful unpin removes the pin immediately, but the underlying
blocks are physically reclaimed only by the periodic GC sweep (~6h). For a
privacy-grade delete ("delete for everyone" / "free up space"), call
DELETE /v1/storage/unpin/:cid?immediate=true: when the CID's last pin is
removed (no other namespace still references it), the blocks are evicted
cluster-wide right away so the content is no longer fetchable. Use it only for
genuine privacy deletes — it fans out per-node work and should not be set on
routine unpins (e.g. avatar rotation).
The cluster keeps ONE pin per CID however many namespaces uploaded the same
bytes. Whether an unpin removes it is decided from a cluster-wide reference
count in the cluster registry (ipfs_cid_refs), never from the caller's own
namespace database: a pin and a deployment register a reference, an unpin
releases the caller's, and only the release that leaves none removes the pin.
The delete and the count are two statements in that order, so of two
namespaces unpinning at once the later one sees zero and neither can miss the
other. Deployments register the content and build CIDs they serve in the same index,
and deployment update and delete, and namespace delete, release through the
same count. An unpin answers a retryable 503 while the index is still being
built (once, at the upgrade that introduced it, until every namespace has
loaded its existing references), and a registry read that fails leaves the pin
in place (evicted: "skipped").
The response carries an evicted field:
| Value | Meaning |
|---|---|
"true" | Every active node confirmed a complete local reclaim. The bytes are gone cluster-wide. |
"partial" | At least one node did not confirm — unreachable, errored, or reported an incomplete reclaim. The pin is still removed everywhere, but the blocks survive on that node until the next GC sweep (≤6h). |
"shared" | Another namespace still references this CID (a storage pin or a deployment), so nothing was removed or evicted. Correct and expected for deduplicated content. |
"skipped" | Immediate reclaim was not requested, or the reference check could not run. |
Only "true" is a delete-for-everyone guarantee. Do not surface "permanently
deleted" to a user on a "partial"; log it, because it means some node still
holds the content.
Check Pin Status
cid := "QmXxx..."
status, err := c.Storage().Status(ctx, cid)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Status: %s, Replication factor: %d, Peers: %v\n",
status.Status, status.ReplicationFactor, status.Peers)StorageStatus includes Cid, Name, Status ("pinned", "pinning", "queued", "unpinned", "error", "unknown"; the cluster-wide state across the peers the pin is allocated to, a peer it is not allocated to never counts), ReplicationMin, ReplicationMax, ReplicationFactor, Peers, and Error.
Cache Client
Not yet available. The SDK does not currently expose the Olric distributed cache — there is no Cache() accessor on the client. Caching is used internally by the gateway.
Database Client
Query the namespace's RQLite distributed SQL database.
With the configuration of the Quick Start (a GatewayURL and a credential, no
DatabaseEndpoints) the database client goes through the gateway's
/v1/rqlite API and authenticates each request with the client's APIKey or
JWT, as the storage client does, so it works from anywhere the gateway is
reachable. What the credential may do is the database access its role and scope
grant. Set DatabaseEndpoints (for example to client.DefaultDatabaseEndpoints())
to dial RQLite nodes directly instead; that needs a route into the WireGuard
mesh and is how a gateway reaches its own database.
Through the gateway, DropTable of a table that is not there succeeds, and
GetSchema leaves out the platform's own tables that the gateway does not let
a tenant query.
Write Query
Writes (INSERT, UPDATE, DELETE, CREATE, DROP, ...) go through Query as well — the client detects write statements and executes them accordingly:
sql := "INSERT INTO users (name, email) VALUES (?, ?)"
_, err := c.Database().Query(ctx, sql, "Alice", "alice@example.com")
if err != nil {
log.Fatal(err)
}Read Query
Query returns a *QueryResult with Columns, Rows, and Count:
sql := "SELECT id, name, email FROM users WHERE id = ?"
result, err := c.Database().Query(ctx, sql, 123)
if err != nil {
log.Fatal(err)
}
for _, row := range result.Rows {
// row is []interface{}, ordered to match result.Columns
fmt.Println(row)
}Create Table
schema := `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`
err := c.Database().CreateTable(ctx, schema)
if err != nil {
log.Fatal(err)
}There is also DropTable(ctx, tableName) and GetSchema(ctx) for schema introspection.
Transaction
Transactions take a slice of SQL statements that are executed atomically:
queries := []string{
"INSERT INTO users (name) VALUES ('Alice')",
"INSERT INTO users (name) VALUES ('Bob')",
}
err := c.Database().Transaction(ctx, queries)
if err != nil {
log.Fatal(err)
}PubSub Client
Publish and subscribe to topics.
Publish Message
topic := "chat"
message := []byte("Hello, everyone!")
err := c.PubSub().Publish(ctx, topic, message)
if err != nil {
log.Fatal(err)
}Publish returns nil only once the node's pubsub service holds the message; an
error means it was not published. Sequential publishes to a topic are delivered
in order.
Subscribe to Topic
The handler is a func(topic string, data []byte) error:
topic := "chat"
handler := func(topic string, data []byte) error {
fmt.Printf("Received on %s: %s\n", topic, string(data))
return nil
}
err := c.PubSub().Subscribe(ctx, topic, handler)
if err != nil {
log.Fatal(err)
}
// Later: unsubscribe
defer c.PubSub().Unsubscribe(ctx, topic)Any number of handlers may subscribe to one topic and each receives every
message. Subscribe returns once the pubsub service holds the subscription, and
Unsubscribe undoes one Subscribe (the most recent), closing the stream when
none is left. A caller that shares a topic with others and must remove exactly
its own handler uses SubscribeHandle, which returns that handler's stop
function (idempotent):
stop, err := c.PubSub().SubscribeHandle(ctx, topic, handler)
if err != nil {
log.Fatal(err)
}
defer stop()Batch Publish
PublishBatch publishes multiple messages in parallel (one per topic), and PublishSame sends the same payload to every topic:
msgs := []client.TopicMessage{
{Topic: "chat", Data: []byte("hello")},
{Topic: "alerts", Data: []byte("warning")},
}
err := c.PubSub().PublishBatch(ctx, msgs, client.PublishBatchOptions{})List Topics
topics, err := c.PubSub().ListTopics(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Topics: %v\n", topics)ListTopics returns the topics of the client's namespace that this node's
pubsub service holds a subscription on, sorted; another namespace's topics are
never listed.
Sending ORAMA
The chain is not in pkg/client. github.com/DeBrosOfficial/network/pkg/onchain sends one account's transactions, signed by the RootWallet agent, and orama chain send and orama chain withdraw-earnings are built on it.
chain := onchain.Gateway{Reader: &chainread.Reader{Gateway: gatewayURL}} // or onchain.REST{Base: nodeRESTURL}
c, err := onchain.New(chain, rwagent.New(os.Getenv("RW_AGENT_SOCK")), "orama-stagenet-5")
// Move 25 ORAMA of earnings to your own balance (there is no destination argument).
_, err = c.WithdrawEarnings(ctx, "25000000000")
// Pay in the open: you must say so. The zero Privacy is Private.
receipt, err := c.Send(ctx, recipient, "5000000000", onchain.Public)
fmt.Println(onchain.PublicWarning)Amounts are norama, as decimal text. Send takes a Privacy, and its zero value is Private: a private send needs the RootWallet to build the shielded bundle, which it cannot yet, so it returns ErrPrivateUnavailable before the wallet or the chain is touched. It never turns into a public payment; only onchain.Public does, and PublicWarning is the text to show next to it. The client derives the account number, sequence, gas and fee itself.
Anonymity Proxy
Two endpoints route traffic through Tor (the client every node runs) so the destination never
learns the end user's IP. Both require the proxy grant and a genuine
end-user (SIWE wallet) JWT — an app-runtime API key alone is refused, because
these are per-user capabilities and an extracted bundle key must not carry them.
Neither is exposed by the SDK yet; call them over HTTP (see also Tor).
Request proxy — POST /v1/proxy/anon
The gateway performs one HTTP request on the caller's behalf and returns the response. Simple, but the gateway is inside the TLS boundary: it sees the URL, the headers and the body in cleartext. Appropriate for fetching a single document (a link preview, an article); not appropriate for general browsing, where it would make the gateway a complete browsing-history observer.
The answer is {"status_code", "headers", "body"} with body the destination's
response body base64-encoded, so binary content survives the JSON envelope;
decode it before use. A failed upstream request is a 200 with an error field.
Anonymity tunnel — GET /v1/proxy/tunnel (WebSocket)
Carries an opaque TCP stream. The client negotiates TLS end-to-end with the destination through the tunnel, so the gateway relays ciphertext.
wss://ns-<namespace>.<base-domain>/v1/proxy/tunnel?host=example.com&port=443&jwt=<user JWT>
- +
host— destination hostname or public IP. Required. Resolved at the exit relay, never by the gateway. - +
port—80or443. Optional, defaults to443. - +Authenticate with
Authorization: Bearer <jwt>, or?jwt=when the client cannot set headers on a WebSocket handshake (browsers, React Native).
Once open, every binary WebSocket frame is written to the destination and every chunk read back is delivered as a binary frame. There is no framing of our own — write TLS records, read TLS records. A text frame is a protocol violation and closes the tunnel.
What the gateway can and cannot see. It cannot see paths, headers, bodies or any content: that is all inside the TLS session it is relaying. It can see the destination host and port, because it cannot dial a connection without them. This is true of every tunnelling proxy. Do not tell users the tunnel hides which sites they visit from the platform — it hides what they do there, and hides them from the site.
Using it as a browser proxy. Web views take a proxy configuration, not a
WebSocket. Run a small loopback HTTP-CONNECT relay inside the app and point the
web view at it: for each local CONNECT host:port, open one tunnel and splice.
The relay is also where the user's JWT is attached, which is required anyway on
the platform that cannot supply proxy credentials from its web view.
Limits (per node):
| Limit | Value |
|---|---|
| Destination ports | 80, 443 only |
| Concurrent tunnels per user | 24 |
| Concurrent tunnels per node | 512 |
| Bytes per tunnel, each direction | 256 MiB |
| Idle timeout | 2 minutes |
| Maximum tunnel lifetime | 30 minutes |
Exceeding a concurrency cap returns 429 with Retry-After. A refused
destination returns 400. An unreachable destination returns 502 with a
deliberately generic message — a tunnel that reports exactly why a dial failed
is a port scanner for whatever the exit relay can reach.
Private destinations are refused: loopback, RFC1918, link-local,
carrier-grade NAT, multicast and localhost are all rejected, so a tunnel
cannot be aimed at the WireGuard mesh or a cloud metadata endpoint.
Circuit isolation. Each user gets their own circuit through the anonymity network (Tor stream isolation by SOCKS credentials), selected by an HMAC of their identity under a node-local secret. Users on the same node are therefore not linkable to one another at the exit, and the anonymity client never receives a wallet address.
Serverless Client
Not yet available in the SDK. The Go client does not expose a Serverless() accessor. Serverless functions are written in Go, compiled with TinyGo, and deployed/managed via the orama function CLI — see Functions for the full workflow.
Error Handling
All client methods return typed errors that can be checked:
import "github.com/DeBrosOfficial/network/pkg/errors"
resp, err := c.Storage().Upload(ctx, reader, "file.txt")
if err != nil {
if errors.IsNotFound(err) {
fmt.Println("Resource not found")
} else if errors.IsUnauthorized(err) {
fmt.Println("Authentication failed")
} else if errors.IsValidation(err) {
fmt.Println("Validation error")
} else {
log.Fatal(err)
}
}Advanced Usage
Custom Timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() resp, err := c.Storage().Upload(ctx, reader, "file.txt")
Retry Logic
import "github.com/DeBrosOfficial/network/pkg/errors"
maxRetries := 3
for i := 0; i < maxRetries; i++ {
resp, err := c.Storage().Upload(ctx, reader, "file.txt")
if err == nil {
break
}
if !errors.ShouldRetry(err) {
return err
}
time.Sleep(time.Second * time.Duration(i+1))
}Namespaces
The client's namespace is derived from its credentials (API key or JWT) — there is no per-request namespace override. To operate in multiple namespaces, create separate clients with different credentials:
cfg1 := client.DefaultClientConfig("app-one")
cfg1.APIKey = "api-key-for-namespace-one"
c1, err := client.NewClient(cfg1)
cfg2 := client.DefaultClientConfig("app-two")
cfg2.APIKey = "api-key-for-namespace-two"
c2, err := client.NewClient(cfg2)Testing
The SDK does not ship mock implementations. NetworkClient, StorageClient, DatabaseClient, and PubSubClient are interfaces, so you can write your own mocks (or generate them) for unit tests:
type mockStorage struct {
client.StorageClient
}
func (m *mockStorage) Upload(ctx context.Context, r io.Reader, name string) (*client.StorageUploadResult, error) {
return &client.StorageUploadResult{Cid: "QmMock", Name: name}, nil
}Examples
Serverless function examples (hello, echo) live in core/examples/functions/. core/examples/demo/ is a complete walkthrough: three functions that call the cache, the namespace's database and the caller host functions (visits, guestbook, hello), a static page that calls them, and the commands that put it on a network. There are currently no standalone SDK example programs in the repository.
API reference
Complete API documentation is on GoDoc. Issues go to the GitHub issue tracker.