Artifact 63ab7ace2b0622575d0cbf5d51225a6bdc97fe0c3e1f48a6f114e78411dc4905:
- File
docs/site/content/docs/guides/production.md
— part of check-in
[451d368554]
at
2026-10-24 13:16:26
on branch main
— feat(horizon): bind each seed to its horizon and sign seeds apart
Every seed an origin mints now carries T +horizon-seed <C> <rule> (draft-fossil-horizon-00 §5.2.1), inside the signed bytes. The card is not counted as a tag in force, Mint never derives a horizon-seed row into a seed, and Apply skips the card.
Before a clone holds a seed, signed or not, it checks the seed against its store cone (§5.2.4). The seed must carry exactly one binding card naming the store's C and rule, and its D must be no earlier than the held seed's. A seed that fails is refused as a wrongly signed one and the clone falls back to the content horizon. A deepen discards the held seed first, so the floor restarts at the new C. A held seed that is no longer bound is discarded when it is next applied.
Seeds are signed with their own horizon-seed-command and are no longer signed by clearsign and pgp-command. SSH seed signatures use the fossil-horizon-seed namespace. clearsign.Key.Verify now takes the namespace, and check-in verification keeps fossilscm.
Adds 9 conformance cases and flips HZ-5.2-8, HZ-5.2-11, HZ-5.2-12 and HZ-5.2-13 to covered. The zit interop gate moves to zit 0.7.1, whose seeds are byte-identical with ours. (user: [email protected], size: 5661) [annotate] [blame] [check-ins using] [more...]
title: Production patterns weight: 10
aliases: ["/docs/production/"]
The Quickstart uses log.Fatal to keep the snippets short. This page shows the patterns you'll actually want when libfossil is embedded in a service.
Error handling
Don't log.Fatal from a long-running process — return errors up to a caller that can log, retry, or fail the request.
func openRepo(path string) (*libfossil.Repo, error) {
repo, err := libfossil.Open(path)
if err != nil {
return nil, fmt.Errorf("open %s: %w", path, err)
}
return repo, nil
}
The errors you get back are wrapped Go errors; use errors.Is and errors.As against the sentinel errors exported from the libfossil package when you need to branch on a specific failure (for example ErrVersionNotFound, ErrAmbiguousVersion or ErrArtifactNotFound).
Context cancellation
Sync, Pull, and Clone accept a context.Context. Pass a request-scoped context with a deadline so a stalled remote doesn't tie up your goroutine forever.
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
if _, err := repo.Sync(ctx, transport, libfossil.SyncOpts{Pull: true}); err != nil {
return fmt.Errorf("sync: %w", err)
}
The HTTP transport honors the deadline at the network boundary; the rest of the sync state machine checks ctx.Done() between rounds.
Telemetry
Wire an observer to your Repo's operations to surface sync rounds, checkout events, and per-error events to your existing logging or metrics stack. See Extension points for the full interface, and observer/otel for an OpenTelemetry-ready implementation that emits spans + counters.
import (
libfossil "github.com/danmestas/go-libfossil"
otelobs "github.com/danmestas/go-libfossil/observer/otel"
)
obs := otelobs.NewSyncObserver()
_, err := repo.Sync(ctx, transport, libfossil.SyncOpts{
Pull: true,
Observer: obs,
})
observer/otel is a separate Go module so the OpenTelemetry SDK doesn't leak into consumers that don't want it.
Repo handle lifetime
A *Repo wraps a SQLite database connection pool. Open it once at service startup, keep it for the lifetime of the process, and Close() it during shutdown — don't open and close per request.
type service struct {
repo *libfossil.Repo
}
func newService(path string) (*service, error) {
repo, err := libfossil.Open(path)
if err != nil {
return nil, err
}
return &service{repo: repo}, nil
}
func (s *service) Shutdown() error {
return s.repo.Close()
}
Concurrent reads through one Repo are safe — SQLite handles the locking. Writes serialize at the database level (the drivers open write transactions with BEGIN IMMEDIATE, and a writer waits up to the 5-second busy timeout for the lock); if your service has heavy write traffic, queue write operations through a single goroutine rather than relying on lock contention to back-pressure.
Driver selection
The shipped cmd/libfossil CLI uses modernc by default; go build -tags test_ncruces ./cmd/libfossil selects ncruces.
For library embedders targeting Linux/macOS/Windows, the default modernc driver is the right choice — pure Go, static binaries, no surprises. Switch to ncruces only when you're shipping a WASI/browser binary or when you need ncruces-specific behavior. Blank-import exactly one driver:
import _ "github.com/danmestas/go-libfossil/db/driver/ncruces"
// Do not also import:
// _ "github.com/danmestas/go-libfossil/db/driver/modernc"
Drivers are mutually exclusive — a process can register exactly one. See db for the registry contract.
Signing check-ins
With the clearsign setting on, every check-in that Repo.Commit, Checkout.Checkin or repo branch new stores is signed, as fossil and zit sign them. Fossil's legacy omitsign setting, when on, turns signing off again. The signer is the pgp-command setting, fossil's contract:
- PGP: unset, it is
gpg --clearsign -o, run asCOMMAND OUTFILE INFILE. - SSH: a command whose name starts with
ssh, such asssh-keygen -q -Y sign -n fossilscm -f ~/.ssh/id_ed25519, is run asCOMMAND INFILEand its detached signature is wrapped in fossil's SSH envelope. The namespace must befossilscm.
libfossil -R repo.fossil repo config set clearsign on
libfossil -R repo.fossil repo config set pgp-command "ssh-keygen -q -Y sign -n fossilscm -f ~/.ssh/id_ed25519"
The signed text is the artifact, so a signed check-in is named by the hash of the signed bytes. Stock fossil and zit clone and read signed check-ins. A signer that fails abandons the commit. CommitOpts.NoSign, CheckoutCommitOpts.NoSign and --nosign store one check-in unsigned. The command runs through the shell, so ~ and environment variables expand. It runs inside the commit's transaction, so a signer that waits on a passphrase prompt holds the repository's write lock until it returns or two minutes pass. On js/wasm there is no external signer.
These settings sign no horizon seed. An origin signs the seeds it mints with its own horizon-seed-command, whose SSH signer must use the namespace fossil-horizon-seed; with none set, seeds go out unsigned. A clone verifies them against the horizon-seed-key setting, which repo clone --seed-key (or CloneOpts.SeedKey) sets. See Signing for the envelope, verification and seed details.