License validation + tier-based feature gating for Go apps.
| name | badge |
|---|---|
| CI | |
| Coverage | |
| Go | |
| License | |
| Built By |
Locket provides a clean interface for validating license keys against multiple backends (Lemon Squeezy, Keygen, or a static/dev mode), then mapping the validated tier to feature flags defined in config.
- 94.7% test coverage — 47 tests across all adapters
- Zero external runtime dependencies — only yaml.v3 for config tags
- Pluggable backends — swap Lemon Squeezy ↔ Keygen by changing one constructor option
go get github.com/purfect-labs/locketpackage main
import (
"fmt"
"github.com/purfect-labs/locket"
)
func main() {
v := locket.New(
locket.WithStatic(),
locket.WithTierConfig(locket.DefaultTierConfig()),
)
lic, err := v.Validate("dev-t2-key")
if err != nil {
panic(err)
}
fmt.Printf("Valid: %v\n", lic.Valid) // true
fmt.Printf("Tier: %s\n", lic.Tier) // "t2"
fmt.Printf("Features: %v\n", lic.Features) // ["chat_premium", "song_unlimited", "export_csv", "offline_mode"]
if locket.HasFeature(lic.Tier, "export_csv", locket.DefaultTierConfig()) {
fmt.Println("Export CSV: ENABLED")
}
}Hardcoded keys for development:
| Key | Tier |
|---|---|
dev-t0-key |
t0 |
dev-t1-key |
t1 |
dev-t2-key |
t2 |
dev-t3-key |
t3 |
dev-expired-key |
valid=false |
v := locket.New(locket.WithStatic())Validates via GET /api/license/validate?license_key=XXXXX. Maps variant names
to tier strings (configurable).
v := locket.New(
locket.WithLemonsqueezy(
locket.WithLemonsqueezyAPIKey(os.Getenv("LS_API_KEY")),
),
locket.WithTierConfig(cfg),
)Options:
| Option | Default |
|---|---|
WithLemonsqueezyAPIKey(key) |
"" |
WithLemonsqueezyBaseURL(url) |
https://api.lemonsqueezy.com/v1 |
WithLemonsqueezyTimeout(d) |
10s |
WithLemonsqueezyVariantOpts(m) |
{Free→t0, Basic→t1, Pro→t2, Enterprise→t3} |
Validates via POST /licenses/actions/validate-key. Maps policy names to tier
strings (configurable). Supports expiry parsing.
v := locket.New(
locket.WithKeygen(
locket.WithKeygenAPIKey(os.Getenv("KEYGEN_API_KEY")),
),
locket.WithTierConfig(cfg),
)Options:
| Option | Default |
|---|---|
WithKeygenAPIKey(key) |
"" |
WithKeygenBaseURL(url) |
https://api.keygen.sh/v1 |
WithKeygenTimeout(d) |
10s |
WithKeygenPolicyMap(m) |
{free→t0, basic→t1, pro→t2, enterprise→t3} |
Configurable test double — return any License or error you want.
v := locket.New(
locket.WithMock(&locket.MockAdapter{
ValidateFn: func(key string) (*locket.License, error) {
return &locket.License{Valid: true, Tier: "t1"}, nil
},
}),
)type LicenseValidator interface {
Validate(key string) (*License, error)
}Implement this interface to add custom backends.
type License struct {
Valid bool
Tier string
Features []string
Expiry time.Time
MachineLimit int
MachineCount int
}ok := locket.HasFeature(tier, featureName, config)// Default 4-tier config (t0–t3)
cfg := locket.DefaultTierConfig()
// Custom config
cfg := &locket.TierConfig{
Tiers: map[string]locket.Tier{
"free": {Label: "Free", Features: []string{"chat"}},
"pro": {Label: "Pro", Features: []string{"chat", "export", "offline"}},
}
}var ErrInvalidKey = fmt.Errorf("locket: invalid license key")
var ErrExpired = fmt.Errorf("locket: license has expired")
var ErrNetwork = fmt.Errorf("locket: network error")
var ErrMaxActivations = fmt.Errorf("locket: maximum activations reached")See _examples/ for runnable code:
| Example | What it shows |
|---|---|
| basic | Simple key validation |
| backend | HTTP middleware feature gating |
| frontend | Tier-based UI rendering |
| custom-config | Custom tier definitions |
| all-options | Every permutation + error handling |
┌─────────────────────────────┐
│ App Code │
│ v.Validate(key) │
│ HasFeature(tier, feat, c) │
└─────────┬───────────────────┘
│
┌─────────▼───────────────────┐
│ locket.LicenseValidator │ ← Interface
│ (plug any adapter) │
└─────────┬───────────────────┘
│
┌─────────▼───────────────────┐
│ Backend Adapter │
│ static | lemonsqueezy │
│ keygen | mock | custom │
└─────────────────────────────┘
See CONTRIBUTING.md. Conventional commits required for auto-versioning.
MIT — see LICENSE.