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()

    // Probe touches the keychain, so give it a deadline. Nothing in the
    // package applies one for you, and a locked keychain can block an
    // unbounded call for as long as the platform takes.
    probeCtx, cancel := context.WithTimeout(ctx, credentials.KeychainOpTimeout)
    defer cancel()

    // 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(probeCtx),
        "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):
You chose: env
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 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 — with the same deadline discipline as the probe, because these calls reach the same keychain:

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

        kcCtx, kcCancel := context.WithTimeout(ctx, credentials.KeychainOpTimeout)
        defer kcCancel()

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

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

Note the deadline is on the keychain calls only, not on InputSecret — a person typing a key needs longer than five seconds.

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. That is the graceful path; the ungraceful one, where the keychain is present but locked and the call never returns, is why the deadlines above are there. See why keychain calls are bounded.

Where next