Skip to content

Client SDK (Go)

The Go client in network/pkg/client/ provides typed access to Database (RQLite), Pub/Sub, and Network information. This page shows setup, authentication, configuration, and end-to-end examples aligned with the current codebase. It also includes an in-depth guide to the ORM-like RQLite client in network/pkg/rqlite for struct-mapped CRUD and a fluent query builder.

References:

  • Code: network/pkg/client/
  • RQLite ORM-like: network/pkg/rqlite
  • Examples: network/examples/basic_usage.go

Install & Import

import (
  "context"
  "github.com/DeBrosOfficial/network/pkg/client"
)

Authentication and Namespace

The client supports two credential types via client.ClientConfig:

  • APIKey: raw API key (format ak_<random>:<namespace>) — namespace is parsed from the suffix.
  • JWT: short-lived JWT issued by the Gateway — preferred; namespace read from the Namespace claim.

Exchange API key for JWT with the Gateway (recommended):

// POST http://localhost:6001/v1/auth/token with Authorization: Bearer <API_KEY>
// See docs/network/authentication.md for details.

Client namespace resolution at Connect() (see deriveNamespace()):

  1. If JWT is set, read Namespace claim from JWT.
  2. Else if APIKey is set, parse namespace from ak_...:<namespace>.
  3. Else fallback to AppName.

You can also override namespace per-call for storage and pubsub with client.WithNamespace(ctx, "my-ns"). The client enforces namespace consistency between credentials and context.


Create and Connect a Client

cfg := client.DefaultClientConfig("my-app")
// This is optional bootstrap peers exist by default.
cfg.BootstrapPeers = []string{
  "/ip4/127.0.0.1/tcp/4001/p2p/<PEER_ID>",
}

// Option A: Use API key directly (namespace from key)
// cfg.APIKey = "ak_xxx:my-app"

// Option B (recommended): Exchange API key for JWT, then set JWT
// cfg.JWT = "<your_jwt>"

cli, err := client.NewClient(cfg)
if err != nil { panic(err) }
if err := cli.Connect(); err != nil { panic(err) }
defer cli.Disconnect()

Tips:

  • Bootstrap peers must be reachable libp2p multiaddrs.
  • RQLite endpoints auto-resolve from defaults or config (see DefaultDatabaseEndpoints()), with retries.

Database API

Interface: client.DatabaseClient in network/pkg/client/interface.go

  • Query(ctx, sql, args...) (*QueryResult, error)
  • Transaction(ctx, []string) error
  • CreateTable(ctx, schema string) error
  • DropTable(ctx, tableName string) error
  • GetSchema(ctx) (*SchemaInfo, error)

Examples:

ctx := context.Background()
db := cli.Database()

// Create or migrate schema
schema := `CREATE TABLE IF NOT EXISTS messages (
  id INTEGER PRIMARY KEY,
  content TEXT NOT NULL,
  timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)`
if err := db.CreateTable(ctx, schema); err != nil { panic(err) }

// Parameterized write and read
_, err := db.Query(ctx, "INSERT INTO messages(content) VALUES (?)", "hello")
if err != nil { panic(err) }

res, err := db.Query(ctx, "SELECT id, content, timestamp FROM messages ORDER BY id DESC LIMIT ?", 10)
if err != nil { panic(err) }
_ = res // res.Columns, res.Rows, res.Count

// Atomic migration (multiple statements)
stmts := []string{
  "ALTER TABLE messages ADD COLUMN author TEXT",
}
if err := db.Transaction(ctx, stmts); err != nil { panic(err) }

// Inspect schema
info, err := db.GetSchema(ctx)
if err != nil { panic(err) }
_ = info

Behavior:

  • Writes and reads are routed using RQLite; client retries and re-establishes connections on failures (gorqlite under the hood).
  • Transaction runs statements sequentially; any failure aborts and triggers retry logic.

RQLite ORM-like (pkg/rqlite)

The network/pkg/rqlite package provides an ORM-like layer over RQLite with struct mapping, a fluent query builder, repositories, and transactions.

Entities and mapping

type User struct {
    ID        int64     `db:"id,pk,auto"`
    Email     string    `db:"email"`
    FirstName string    `db:"first_name"`
    LastName  string    `db:"last_name"`
    CreatedAt time.Time `db:"created_at"`
}

func (User) TableName() string { return "users" }

Rules: - If no db tag is provided, the field name is used as the column (case-insensitive). - A field named ID is treated as the primary key by default; add auto for autoincrement.

Save and Remove

u := &User{Email: "[email protected]", FirstName: "Alice", LastName: "A"}
_ = client.Save(ctx, u)  // INSERT; sets u.ID if autoincrement

u.LastName = "Updated"
_ = client.Save(ctx, u)  // UPDATE by primary key

_ = client.Remove(ctx, u) // DELETE by primary key

FindOneBy / FindBy

var one User
_ = client.FindOneBy(ctx, &one, "users", map[string]any{"email": "[email protected]"})

var many []User
_ = client.FindBy(ctx, &many, "users", map[string]any{"last_name": "A"},
    rqlite.WithOrderBy("created_at DESC"), rqlite.WithLimit(50))

QueryBuilder

var results []User
qb := client.CreateQueryBuilder("users u").
    InnerJoin("posts p", "p.user_id = u.id").
    Where("u.email LIKE ?", "%@example.com").
    AndWhere("p.created_at >= ?", "2024-01-01T00:00:00Z").
    GroupBy("u.id").
    OrderBy("u.created_at DESC").
    Limit(20).
    Offset(0)

_ = qb.GetMany(ctx, &results)

var one User
_ = qb.Limit(1).GetOne(ctx, &one)

Raw queries

var users []User
_ = client.Query(ctx, &users, "SELECT id, email FROM users WHERE email LIKE ?", "%@example.com")

var rows []map[string]any
_ = client.Query(ctx, &rows, "SELECT id, email FROM users WHERE id IN (?,?)", 1, 2)

Transactions

err := client.Tx(ctx, func(tx rqlite.Tx) error {
    var me User
    if err := tx.Query(ctx, &me, "SELECT * FROM users WHERE id = ?", 1); err != nil {
        return err
    }
    me.LastName = "Updated"
    if err := tx.Save(ctx, &me); err != nil {
        return err
    }
    var recent []User
    if err := tx.CreateQueryBuilder("users").
        OrderBy("created_at DESC").
        Limit(5).
        GetMany(ctx, &recent); err != nil {
        return err
    }
    return nil // commit
})

Repositories (generic)

repo := client.Repository[User]("users")
var many []User
_ = repo.Find(ctx, &many, map[string]any{"last_name": "A"}, rqlite.WithOrderBy("created_at DESC"), rqlite.WithLimit(10))
var one User
_ = repo.FindOne(ctx, &one, map[string]any{"email": "[email protected]"})
_ = repo.Save(ctx, &one)
_ = repo.Remove(ctx, &one)

Pub/Sub API

Interface: client.PubSubClient

  • Subscribe(ctx, topic string, handler MessageHandler) error
  • Publish(ctx, topic string, data []byte) error
  • Unsubscribe(ctx, topic string) error
  • ListTopics(ctx) ([]string, error)

Example:

ctx := context.Background()
ps := cli.PubSub()

handler := func(topic string, data []byte) error {
  // handle message
  return nil
}

if err := ps.Subscribe(ctx, "notifications", handler); err != nil { panic(err) }
if err := ps.Publish(ctx, "notifications", []byte("hi")); err != nil { panic(err) }
topics, _ := ps.ListTopics(ctx)
_ = topics

Notes:

  • Uses libp2p GossipSub; client is a lightweight participant and connects only to bootstrap peers.

Network Info API

Interface: client.NetworkInfo

  • GetStatus(ctx) (*NetworkStatus, error)
  • GetPeers(ctx) ([]PeerInfo, error)
  • ConnectToPeer(ctx, addr string) error
  • DisconnectFromPeer(ctx, peerID string) error
ctx := context.Background()
n := cli.Network()
status, _ := n.GetStatus(ctx)
peers, _ := n.GetPeers(ctx)
_ = status; _ = peers

End-to-End: Auth (API Key → JWT) + Client

// 1) Exchange API key for JWT via Gateway
// POST http://localhost:6001/v1/auth/token with header: Authorization: Bearer <API_KEY>
// Response: { access_token, expires_in, namespace }

// 2) Configure client with JWT
cfg := client.DefaultClientConfig("my-app")
cfg.JWT = "<access_token>"
cfg.BootstrapPeers = []string{"/ip4/127.0.0.1/tcp/4001/p2p/<PEER_ID>"}
cli, _ := client.NewClient(cfg)
_ = cli.Connect()
defer cli.Disconnect()

Context Helpers

  • client.WithNamespace(ctx, ns) — sets namespace override in the context (applies to supported operations).
  • client.WithInternalAuth(ctx) — bypasses auth checks for internal system operations (for gateway/node internals; not for regular app code).

Troubleshooting

  • Ensure bootstrap peer multiaddrs are correct and reachable.
  • RQLite leader election may momentarily cause write failures; the client retries automatically.
  • If you pass a namespace in context that doesn’t match credentials, requireAccess will reject the call.

See also

  • Full example: network/examples/basic_usage.go
  • Gateway: see docs/network/gateway.md for endpoints and examples
  • Migrations: see docs/network/migrations.md