Skip to content

Getting started

This tutorial builds a tiny "credential setup" flow: it asks the user how they want to store an API key, records that decision, and resolves the key back at runtime. You will see the three storage modes, the interactive Prompter, and the resolve step — the whole shape of the package in about thirty lines.

By the end you will have a program that runs on any machine, with no OS keychain required.

Prerequisites

  • Go 1.26 or newer.
  • A terminal (the stdlib prompter reads from standard input).

1. Create a module

mkdir creddemo && cd creddemo
go mod init creddemo
go get gitlab.com/phpboyscout/go/credentials

2. Offer the storage modes

The package never decides for you which modes are appropriate — it hands you the list, filtered by the environment. ModeChoices returns the selectable modes as plain data (keychain only when usable, literal only outside CI); a Prompter turns that into a question.

Create main.go:

package main

import (
    "context"
    "fmt"
    "os"

    "gitlab.com/phpboyscout/go/credentials"
)

func main() {
    ctx := context.Background()
    prompter := credentials.DefaultPrompter()

    // Env-var reference is always offered; keychain only when a backend
    // is registered and reachable; literal only outside CI. This demo
    // registers no keychain backend, so two modes are offered.
    choices := credentials.ModeChoices(
        credentials.IsCI(),
        credentials.Probe(ctx),
        "Environment variable reference (recommended)",
        "OS keychain",
        "Literal value (plaintext)",
    )

    mode, err := prompter.SelectMode(ctx, "How should we store your API key?", choices)
    if err != nil {
        fmt.Fprintln(os.Stderr, "cancelled:", err)
        os.Exit(1)
    }

    fmt.Println("You chose:", mode)
}

Run it:

go run .

You will see a numbered menu. Press Enter to take the default (environment-variable reference).

3. Capture the reference, resolve the secret

Env-var mode stores the name of a variable, not the secret. Add the capture and resolve steps to main(), after the SelectMode call:

    if mode == credentials.ModeEnvVar {
        name, err := prompter.InputEnvVarName(ctx,
            "Which environment variable holds the key?",
            "CREDDEMO_API_KEY",
            credentials.ValidateEnvVarName, // enforces ^[A-Z][A-Z0-9_]{0,63}$
        )
        if err != nil {
            fmt.Fprintln(os.Stderr, "cancelled:", err)
            os.Exit(1)
        }

        // A real tool writes `name` to its config file. At runtime it
        // resolves the secret from the environment — the secret itself
        // never touches disk.
        fmt.Printf("Config would record: api.env = %q\n", name)
        fmt.Printf("Resolved key: %q\n", os.Getenv(name))
    }

Run it again, accept the default variable name, and watch it resolve from your environment:

CREDDEMO_API_KEY=sk-demo-123 go run .
How should we store your API key?
  * 1) Environment variable reference (recommended)
    2) Literal value (plaintext)
Select [1-2] (default 1):
Which environment variable holds the key? [CREDDEMO_API_KEY]:
Config would record: api.env = "CREDDEMO_API_KEY"
Resolved key: "sk-demo-123"

That is the whole model: the config records a reference, and the secret is resolved from a source outside the config file.

4. (Optional) Store a secret in the OS keychain

On a desktop with an OS keychain, you can store the secret itself outside both the config and the environment. Add one blank import to activate the keychain backend:

import (
    // ... existing imports ...
    _ "gitlab.com/phpboyscout/go/credentials/keychain"
)

Now credentials.Probe(ctx) returns true on a machine with a working keychain, so ModeChoices offers OS keychain as a third option. When the user picks it, the secret round-trips through the platform store:

    if mode == credentials.ModeKeychain {
        secret, _ := prompter.InputSecret(ctx, "Paste your API key")

        if err := credentials.Store(ctx, "creddemo", "api", secret); err != nil {
            fmt.Fprintln(os.Stderr, "store failed:", err)
            os.Exit(1)
        }

        got, _ := credentials.Retrieve(ctx, "creddemo", "api")
        fmt.Printf("Stored and read back %d bytes from the keychain\n", len(got))
    }

On a headless server without a keychain, Probe returns false and this option is simply never offered — the flow degrades gracefully to env-var and literal modes.

Where next