clavitor/clavis/clavis-vault/SPEC-editions.md

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

For 404/405 responses:

  • Holds connection for 30 seconds
  • Drips one byte per second
  • Caps at 1000 concurrent tarpit connections
  • Interruptible: Checks edition.ShutdownContext each 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 CommercialConfig struct

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

  1. Define behavior in interface (edition/edition.go):

    type Edition interface {
        // ... existing methods ...
        NewFeature() SomeType
    }
    
  2. Implement in community (edition/community.go):

    func (e *communityEdition) NewFeature() SomeType {
        // Return no-op or simple implementation
        return nil
    }
    
  3. Implement in commercial (edition/commercial.go):

    func (e *commercialEdition) NewFeature() SomeType {
        // Full implementation
        return &commercialFeature{...}
    }
    
  4. 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

  1. Never remove build tags — they enforce the dual-license model
  2. Always use edition.Current — no build tag checks in app code
  3. Community is defaultgo build without tags must work
  4. Commercial config is optional — commercial binary runs without config (just degraded)
  5. 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.