6.8 KiB
Clavitor Edition System Specification
Overview
Clavitor Vault has two editions with build-time separation — not runtime flags, not config files, not license keys. The edition is determined at compile time via Go build tags.
| Edition | Build Tag | Target User |
|---|---|---|
| Community | (none, default) | Self-hosted individuals, privacy-first users |
| Commercial | -tags commercial |
Managed service, enterprise, multi-POP deployments |
Architecture
edition/
├── edition.go # Interface + shared types (always compiled)
├── community.go # Community implementation (go:build !commercial)
└── commercial.go # Commercial implementation (go:build commercial)
The edition.Current variable is set at init() time based on build tags. Application code never checks build tags directly — it uses edition.Current.Name() or calls methods on the interface.
Build Commands
# Community Edition (default) - Elastic 2, self-hosted, no external calls
go build -o clavitor ./cmd/clavitor/
# Commercial Edition - managed service, telemetry, replication
go build -tags commercial -o clavitor ./cmd/clavitor/
The Edition Interface
type Edition interface {
// Name returns "community" or "commercial"
Name() string
// AlertOperator sends critical operational alerts
// Community: logs to stderr
// Commercial: POSTs to telemetry endpoint + logs
AlertOperator(ctx context.Context, alertType, message string, details map[string]any)
// IsTelemetryEnabled returns true if data goes to central servers
IsTelemetryEnabled() bool
// NotFoundHandler handles 404/405 routes
// Community: simple 404 (fast, clean shutdown)
// Commercial: tarpit (anti-scanner, interruptible on shutdown)
NotFoundHandler() http.HandlerFunc
}
Feature Matrix
| Feature | Community | Commercial |
|---|---|---|
| License | Elastic License 2.0 | Commercial |
| Telemetry | None (unless user manually configures) | Enabled by default to clavitor.ai |
| Operator Alerts | Local logs only | POSTs to /v1/alerts endpoint |
| Replication | ❌ Not available | ✅ Real-time sync to backup POPs |
| Tarpit | ❌ Simple 404 | ✅ 30s anti-scanner drain |
| Central Management | ❌ None | ✅ Multi-POP dashboard |
| SCIM/SIEM | ❌ No | ✅ Yes |
Commercial-Only Features (Detailed)
1. Real-Time Replication to Backup POPs
File: edition/replication.go (only compiled with -tags commercial)
Background worker that:
- Polls
EntryListUnreplicated()for dirty entries - POSTs encrypted entry blobs to backup POP (Calgary/Zurich)
- Automatic retry with exponential backoff
- Batching (up to 100 entries per request)
- Conflict resolution: last-write-wins
Community: No replication code exists in binary. ReplicatedAt field in DB is never used.
2. Tarpit (Anti-Scanner Defense)
File: edition/commercial.go → NotFoundHandler()
For 404/405 responses:
- Holds connection for 30 seconds
- Drips one byte per second
- Caps at 1000 concurrent tarpit connections
- Interruptible: Checks
edition.ShutdownContexteach iteration
Community: Returns HTTP 404 immediately. Clean, fast shutdown.
3. Centralized Telemetry & Alerting
Files: edition/commercial.go
- Periodic metrics POST to
TelemetryHost - Operator alerts POST to
/v1/alerts - Configured via
CommercialConfigstruct
Community: AlertOperator() logs locally only. No network calls.
Code Patterns
Checking Edition
// DON'T do this:
// if edition.Current.Name() == "commercial" { ... }
// DO this - use interface methods:
edition.Current.AlertOperator(ctx, "error", "message", details)
The interface abstracts the behavior. Don't branch on the name unless absolutely necessary (e.g., UI hints).
Adding Commercial-Only Features
-
Define behavior in interface (
edition/edition.go):type Edition interface { // ... existing methods ... NewFeature() SomeType } -
Implement in community (
edition/community.go):func (e *communityEdition) NewFeature() SomeType { // Return no-op or simple implementation return nil } -
Implement in commercial (
edition/commercial.go):func (e *commercialEdition) NewFeature() SomeType { // Full implementation return &commercialFeature{...} } -
Use in application code:
feature := edition.Current.NewFeature() if feature != nil { feature.DoSomething() }
Graceful Shutdown
Both editions support graceful shutdown via edition.ShutdownContext:
// main.go sets this on startup
edition.ShutdownContext = shutdownCtx
// Commercial tarpit checks this:
select {
case <-edition.ShutdownContext.Done():
return // Exit immediately on SIGTERM
case <-time.After(time.Second):
// Continue dripping
}
Community: Shutdown is instant (no long-running handlers).
Commercial: Tarpit connections exit within 1 second of SIGTERM.
Testing
# Test both editions
go test ./edition/...
go test -tags commercial ./edition/...
# Build verification
go build ./cmd/clavitor/ # Community
go build -tags commercial ./cmd/clavitor/ # Commercial
# Verify feature presence
strings /tmp/clavitor-community | grep -i tarpit # Should be empty
strings /tmp/clavitor-commercial | grep -i tarpit # Should find references
Important Rules
- Never remove build tags — they enforce the dual-license model
- Always use
edition.Current— no build tag checks in app code - Community is default —
go buildwithout tags must work - Commercial config is optional — commercial binary runs without config (just degraded)
- No runtime switching — edition is baked into binary, not changeable
FAQ for Agents
Q: Why not use a config flag for community vs commercial? A: Build-time separation ensures:
- Community users cannot accidentally enable commercial features
- Commercial code (telemetry, replication) doesn't exist in Elastic 2 binary
- Clean mental model: you get what you build
Q: Can a community user upgrade to commercial without rebuilding?
A: No. They must rebuild with -tags commercial and configure CommercialConfig.
Q: What happens if commercial code is called without config? A: Graceful degradation:
- Telemetry disabled (no host configured)
- Replication disabled (no config file)
- Alerts logged locally only
- Tarpit still works (needs no config)
Q: How do I know which edition is running? A: Check logs at startup:
Starting Clavitor Vault v2.0.45 - Community Edition
# or
Starting Clavitor Vault v2.0.45 - Commercial Edition
Or call edition.Current.Name() programmatically.