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¶
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:
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:
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:
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¶
- Choose a storage mode — the trade-offs between the three modes, and CI behaviour.
- Enable OS-keychain storage — the opt-in import and the regulated-build opt-out.
- Theme the prompts — replace
DefaultPrompterwith a themed TUI so the questions match your tool. - Trust model — what each mode protects against.
- API reference — every symbol used above, with its defaults and failure behaviour.