Skip to main content

SDKs

Arcan provides official client SDKs for Go, Python, and Node.js. All SDKs have zero external dependencies and use only standard library modules.

Install

go get getarcan.dev/arcan/sdk/go

Quick Start

package main

import (
"fmt"
"log"

arcan "getarcan.dev/arcan/sdk/go"
)

func main() {
client, err := arcan.New(
arcan.WithURL("https://arcan.internal:8443"),
arcan.WithToken("arc_xxx"),
arcan.WithRealm("production"),
arcan.WithEnv("prod"),
)
if err != nil {
log.Fatal(err)
}

dbURL, err := client.Get("DATABASE_URL")
if err != nil {
log.Fatal(err)
}
fmt.Println(dbURL)
}

Environment Variable Auto-Config

All SDKs support automatic configuration from environment variables. Set ARCAN_URL, ARCAN_TOKEN, ARCAN_REALM, and ARCAN_ENV, then create a client with no arguments:

client, err := arcan.New()
if err != nil {
log.Fatal(err)
}
dbURL := client.MustGet("DATABASE_URL")

API Reference

Client Options

OptionDefaultDescription
WithURL(url)ARCAN_URL or https://localhost:8443Server URL
WithToken(token)ARCAN_TOKENAuth token (required)
WithRealm(realm)ARCAN_REALM or "default"Realm/namespace
WithEnv(env)ARCAN_ENV or "dev"Environment
WithInsecureSkipVerify(bool)trueAccept self-signed certs
WithCacheTTL(duration)5mCache TTL (0 to disable)

Methods

All SDKs provide the same core methods:

MethodDescription
Get(key) / get(key)Fetch a secret value
GetWithMeta(key) / get_meta(key) / getMeta(key)Fetch secret with metadata
List() / list()Fetch all secrets
Set(key, value) / set(key, value)Create or update a secret
Delete(key) / delete(key)Delete a secret
Health() / health()Check server health

Go additionally has MustGet(key) which panics on error (useful for initialization).

Error Handling

import "errors"

val, err := client.Get("MY_SECRET")
if errors.Is(err, arcan.ErrNotFound) {
// Secret does not exist
} else if errors.Is(err, arcan.ErrAuth) {
// Invalid or expired token
} else if errors.Is(err, arcan.ErrConnection) {
// Cannot reach server
} else if errors.Is(err, arcan.ErrServer) {
// Server returned 5xx
}

Caching

All SDKs cache secrets in memory for 5 minutes by default. The cache is thread-safe. Write operations (Set, Delete) automatically invalidate relevant cache entries.

// Disable caching
client, _ := arcan.New(arcan.WithCacheTTL(0))

// Cache for 30 seconds
client, _ := arcan.New(arcan.WithCacheTTL(30 * time.Second))

Real-World Examples

Go: Database connection with retry

package main

import (
"database/sql"
"log"
"net/http"
"time"

_ "github.com/lib/pq"
arcan "getarcan.dev/arcan/sdk/go"
)

func main() {
// Auto-configures from ARCAN_URL, ARCAN_TOKEN, ARCAN_REALM, ARCAN_ENV
secrets, err := arcan.New()
if err != nil {
log.Fatalf("arcan init failed: %v", err)
}

// Fetch database connection string
dsn, err := secrets.Get("DATABASE_URL")
if err != nil {
log.Fatalf("failed to fetch DATABASE_URL: %v", err)
}

// Connect to PostgreSQL
db, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatalf("failed to open database: %v", err)
}
defer db.Close()

db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)

if err := db.Ping(); err != nil {
log.Fatalf("database unreachable: %v", err)
}
log.Println("Connected to database successfully")

// Fetch API key for downstream service
apiKey := secrets.MustGet("PAYMENT_API_KEY")

http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
if err := db.Ping(); err != nil {
http.Error(w, "database unhealthy", http.StatusServiceUnavailable)
return
}
w.Write([]byte("ok"))
})

_ = apiKey // used in payment handler
log.Fatal(http.ListenAndServe(":8080", nil))
}

Python: FastAPI application with secrets

from contextlib import asynccontextmanager
import asyncpg
from fastapi import FastAPI
from arcan import ArcanClient, ConnectionError, AuthError

# Initialize Arcan — reads ARCAN_URL, ARCAN_TOKEN, ARCAN_REALM, ARCAN_ENV
secrets = ArcanClient()

@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: fetch secrets and connect to database
try:
db_url = secrets.get("DATABASE_URL")
redis_url = secrets.get("REDIS_URL")
except ConnectionError:
raise RuntimeError("Cannot reach Arcan server — is ARCAN_URL correct?")
except AuthError:
raise RuntimeError("Arcan token is invalid or expired — check ARCAN_TOKEN")

app.state.db = await asyncpg.create_pool(db_url, min_size=5, max_size=20)
print(f"Database pool created with {app.state.db.get_size()} connections")

yield

# Shutdown
await app.state.db.close()

app = FastAPI(lifespan=lifespan)

@app.get("/users/{user_id}")
async def get_user(user_id: int):
row = await app.state.db.fetchrow("SELECT * FROM users WHERE id = $1", user_id)
if not row:
return {"error": "user not found"}
return dict(row)

Node.js: Express app with connection pooling

import express from 'express';
import { Pool } from 'pg';
import { ArcanClient, ConnectionError, AuthError } from '@getarcan/sdk';

// Auto-configures from ARCAN_URL, ARCAN_TOKEN, ARCAN_REALM, ARCAN_ENV
const secrets = new ArcanClient();

async function main() {
// Fetch all required secrets up front
let dbUrl: string;
let stripeKey: string;

try {
dbUrl = await secrets.get('DATABASE_URL');
stripeKey = await secrets.get('STRIPE_SECRET_KEY');
} catch (err) {
if (err instanceof ConnectionError) {
console.error('Cannot reach Arcan server. Check ARCAN_URL.');
} else if (err instanceof AuthError) {
console.error('Arcan token is invalid. Check ARCAN_TOKEN.');
}
process.exit(1);
}

const pool = new Pool({ connectionString: dbUrl, max: 20 });

const app = express();

app.get('/health', async (_req, res) => {
try {
await pool.query('SELECT 1');
res.json({ status: 'ok' });
} catch {
res.status(503).json({ status: 'unhealthy' });
}
});

app.listen(3000, () => {
console.log('Server running on :3000');
});
}

main();

Cache Behavior

All SDKs cache secrets in memory for 5 minutes by default. Here is how the cache works in practice:

client, _ := arcan.New(arcan.WithCacheTTL(2 * time.Minute))

// First call: makes an HTTP request to Arcan server (~50ms)
val1, _ := client.Get("DATABASE_URL")

// Second call within 2 minutes: returns from cache (~0ms)
val2, _ := client.Get("DATABASE_URL")

// After updating a secret, the cache entry is automatically invalidated
client.Set("DATABASE_URL", "postgres://new-host:5432/db")

// Next call fetches the fresh value from the server
val3, _ := client.Get("DATABASE_URL")
tip

For long-running applications, the default 5-minute cache TTL works well — it reduces API load while ensuring secrets refresh within minutes of a rotation. For short-lived jobs (CI/CD, cron), set cacheTtlMs: 0 (or equivalent) to always fetch fresh values.

Environment Variable Auto-Config in Practice

Set these four environment variables in your deployment platform (ECS task definition, Kubernetes pod spec, systemd unit, etc.) and the SDK requires zero configuration in code:

export ARCAN_URL=https://arcan.internal:8443
export ARCAN_TOKEN=arc_prod_a1b2c3d4e5f6
export ARCAN_REALM=myapp
export ARCAN_ENV=prod

Then in your application:

// No URL, token, realm, or env needed — all from environment
client, err := arcan.New()
if err != nil {
log.Fatal(err)
}
dbURL := client.MustGet("DATABASE_URL")

This pattern keeps secrets out of your codebase entirely. The only secret your deployment platform needs is the Arcan token — everything else is fetched at runtime.

Complete Example: Fetch DATABASE_URL and Connect to PostgreSQL

Each example below shows the full flow: configure via environment variables, connect to Arcan, fetch DATABASE_URL, connect to PostgreSQL, and handle errors.

Go

package main

import (
"database/sql"
"errors"
"fmt"
"log"
"os"

_ "github.com/lib/pq"
arcan "getarcan.dev/arcan/sdk/go"
)

func main() {
// Option 1: Auto-config from environment variables
// Set these in your deployment platform (ECS task def, K8s pod spec, systemd unit):
// ARCAN_URL=https://arcan.internal:8443
// ARCAN_TOKEN=arc_prod_a1b2c3d4
// ARCAN_REALM=myapp
// ARCAN_ENV=prod
client, err := arcan.New()
if err != nil {
// Option 2: Explicit config (useful for local development)
client, err = arcan.New(
arcan.WithURL("https://arcan.internal:8443"),
arcan.WithToken(os.Getenv("ARCAN_TOKEN")),
arcan.WithRealm("myapp"),
arcan.WithEnv("prod"),
)
if err != nil {
log.Fatalf("Failed to initialize Arcan client: %v", err)
}
}

// Fetch the database connection string
dsn, err := client.Get("DATABASE_URL")
if err != nil {
if errors.Is(err, arcan.ErrNotFound) {
log.Fatal("DATABASE_URL not found in Arcan. Run: arcan kv set DATABASE_URL '<value>' -r myapp -e prod")
} else if errors.Is(err, arcan.ErrAuth) {
log.Fatal("Arcan token is invalid or expired. Check ARCAN_TOKEN.")
} else if errors.Is(err, arcan.ErrConnection) {
log.Fatalf("Cannot reach Arcan server at %s. Check ARCAN_URL.", os.Getenv("ARCAN_URL"))
}
log.Fatalf("Failed to fetch DATABASE_URL: %v", err)
}

// Connect to PostgreSQL
db, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatalf("Failed to parse DATABASE_URL: %v", err)
}
defer db.Close()

if err := db.Ping(); err != nil {
log.Fatalf("Cannot reach database: %v", err)
}

fmt.Println("Connected to PostgreSQL via Arcan successfully")

// Use the database
var version string
if err := db.QueryRow("SELECT version()").Scan(&version); err != nil {
log.Fatal(err)
}
fmt.Printf("PostgreSQL version: %s\n", version)
}

Python

import sys
import psycopg2
from arcan import ArcanClient, NotFoundError, AuthError, ConnectionError

def main():
# Auto-config from environment variables:
# ARCAN_URL=https://arcan.internal:8443
# ARCAN_TOKEN=arc_prod_a1b2c3d4
# ARCAN_REALM=myapp
# ARCAN_ENV=prod
client = ArcanClient()

# Fetch the database connection string
try:
dsn = client.get("DATABASE_URL")
except NotFoundError:
print("DATABASE_URL not found in Arcan.")
print("Run: arcan kv set DATABASE_URL '<value>' -r myapp -e prod")
sys.exit(1)
except AuthError:
print("Arcan token is invalid or expired. Check ARCAN_TOKEN.")
sys.exit(1)
except ConnectionError:
print("Cannot reach Arcan server. Check ARCAN_URL.")
sys.exit(1)

# Connect to PostgreSQL
try:
conn = psycopg2.connect(dsn)
conn.autocommit = True
except psycopg2.OperationalError as e:
print(f"Cannot connect to database: {e}")
sys.exit(1)

print("Connected to PostgreSQL via Arcan successfully")

# Use the database
with conn.cursor() as cur:
cur.execute("SELECT version()")
version = cur.fetchone()[0]
print(f"PostgreSQL version: {version}")

conn.close()

if __name__ == "__main__":
main()

Node.js

import { Pool } from 'pg';
import {
ArcanClient,
NotFoundError,
AuthError,
ConnectionError,
} from '@getarcan/sdk';

async function main() {
// Auto-config from environment variables:
// ARCAN_URL=https://arcan.internal:8443
// ARCAN_TOKEN=arc_prod_a1b2c3d4
// ARCAN_REALM=myapp
// ARCAN_ENV=prod
const client = new ArcanClient();

// Fetch the database connection string
let dsn: string;
try {
dsn = await client.get('DATABASE_URL');
} catch (err) {
if (err instanceof NotFoundError) {
console.error('DATABASE_URL not found in Arcan.');
console.error('Run: arcan kv set DATABASE_URL \'<value>\' -r myapp -e prod');
} else if (err instanceof AuthError) {
console.error('Arcan token is invalid or expired. Check ARCAN_TOKEN.');
} else if (err instanceof ConnectionError) {
console.error('Cannot reach Arcan server. Check ARCAN_URL.');
} else {
console.error('Failed to fetch DATABASE_URL:', err);
}
process.exit(1);
}

// Connect to PostgreSQL
const pool = new Pool({ connectionString: dsn, max: 20 });

try {
const result = await pool.query('SELECT version()');
console.log('Connected to PostgreSQL via Arcan successfully');
console.log(`PostgreSQL version: ${result.rows[0].version}`);
} catch (err) {
console.error('Cannot connect to database:', err);
process.exit(1);
}

// Graceful shutdown
process.on('SIGTERM', async () => {
await pool.end();
process.exit(0);
});
}

main();
tip

Set the four environment variables (ARCAN_URL, ARCAN_TOKEN, ARCAN_REALM, ARCAN_ENV) in your deployment platform once. Then every SDK call in your application code requires zero configuration -- just ArcanClient() with no arguments.

Security

  • Use valid TLS certificates in production (not self-signed)
  • Store API tokens in your platform's secret management (not hardcoded)
  • Use read-only tokens (arcan token create --scopes read) for applications that only need to read secrets
  • Enable audit logging on the Arcan server to track all secret access