Skip to content

Go SDK

The Go SDK (secretspec-go) is a thin client over the secretspec-ffi C ABI, loaded via purego (dlopen, no cgo). Resolution happens in the Rust core, so the SDK inherits every provider with no Go-side logic.

package main

import (
	"fmt"
	"log"

	secretspec "github.com/cachix/secretspec/secretspec-go"
)

func main() {
	resolved, err := secretspec.New().
		WithProvider("keyring://").
		WithProfile("production").
		WithReason("boot web app").
		Load()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(resolved.Provider, resolved.Profile)
	db := resolved.Secrets["DATABASE_URL"]
	fmt.Println(db.Get()) // the value, or the file path for as_path secrets
	resolved.SetAsEnv()   // export everything into the process environment
}

A missing required secret returns *MissingRequiredError; any other failure returns *Error (with a stable .Kind).

Use WithScope("api") to resolve only a named [scopes.api] subset. The selected name is available as Resolved.Scope and Report.Scope:

package main

import (
	"log"

	secretspec "github.com/cachix/secretspec/secretspec-go"
)

func main() {
	resolved, err := secretspec.New().WithScope("api").Load()
	if err != nil {
		log.Fatal(err)
	}
	defer resolved.Close()
}

Generate typed structs with secretspec schema plus quicktype, then unmarshal resolved.FieldsJSON():

Terminal window
$ secretspec schema | quicktype -s schema --top-level SecretSpec --lang go -o secrets_gen.go
package main

import (
	"fmt"
	"log"

	secretspec "github.com/cachix/secretspec/secretspec-go"
)

func main() {
	resolved, err := secretspec.New().Load()
	if err != nil {
		log.Fatal(err)
	}
	defer resolved.Close()

	data, _ := resolved.FieldsJSON()
	typed, _ := UnmarshalSecretSpec(data) // typed, generated
	fmt.Println(typed.DatabaseURL)
}

The native secretspec-ffi cdylib is resolved at runtime, in order:

  1. The SECRETSPEC_FFI_LIB environment variable (an explicit path).
  2. A library embedded at build time with -tags embed_lib.
  3. A Cargo target directory found by searching up from the working directory (the development path).

The SDK uses purego, so the cdylib is loaded at runtime, not linked. Either install/build libsecretspec_ffi and set SECRETSPEC_FFI_LIB, or stage the per-platform library into lib/ and build with -tags embed_lib for a self-contained binary. The embedded library is extracted to a per-user, owner-only cache directory at first use, and is not distributed through the Go module proxy.

For a self-contained binary with no runtime library to locate, build with -tags static instead. This uses cgo and links libsecretspec_ffi.a directly into the Go binary. In a development checkout:

Terminal window
$ bash scripts/stage-staticlib.sh
$ CGO_ENABLED=1 go build -tags static ./...

Install one library type with cargo-c:

Terminal window
# Use "static" (the default) or "shared"; use separate prefixes for both.
$ bash secretspec-ffi/scripts/cinstall.sh "$PREFIX" static

Then use the same build command for either type:

Terminal window
$ PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" CGO_ENABLED=1 go build -tags pkgconfig ./...

Unlike staging, this also works for a go get dependency. A shared install in a non-system prefix also requires PREFIX/lib in the platform’s runtime library search path.