clavitor/clavis/clavis-vault/SPEC-dual-build.md

8.8 KiB

Spec: Community / Enterprise Dual-Build Model

Context

Clavitor ships two editions from a single codebase:

  • Community Edition (ELv2, open source): single vault, full encryption, CLI, browser extension, manual scope assignment
  • Enterprise Edition (closed source, hosted): everything in Community + management plane APIs, auto-scope, SCIM webhook receiver, cross-vault audit feed, telemetry hooks

The split is compile-time via Go build tags. No runtime feature flags. No license key checks. Enterprise code simply doesn't exist in the community binary.

Architecture

Build tags

All enterprise-only code lives behind //go:build enterprise.

Community build:   go build -o clavitor .
Enterprise build:  go build -tags enterprise -o clavitor .

File structure

api/
  handlers.go              # shared: GET /entry, GET /totp, GET /entries, GET /search
  handlers_admin.go        # shared: POST/PUT/DELETE /agent, /entry (WebAuthn-gated)
  handlers_enterprise.go   # enterprise only (build tag): management plane APIs
  routes.go                # shared routes
  routes_enterprise.go     # enterprise only (build tag): registers enterprise routes

scopes/
  scopes.go                # shared: scope matching, entry access checks
  autoscope.go             # enterprise only (build tag): LLM-based scope classification

scim/
  scim.go                  # enterprise only (build tag): SCIM webhook receiver

audit/
  audit.go                 # shared: per-vault audit logging
  audit_feed.go            # enterprise only (build tag): cross-vault audit export feed

telemetry/
  telemetry.go             # enterprise only (build tag): node telemetry reporting

Enterprise route registration

Use an init pattern so enterprise routes self-register when the build tag is present:

// routes_enterprise.go
//go:build enterprise

package api

func init() {
    enterpriseRoutes = append(enterpriseRoutes,
        Route{"POST", "/api/mgmt/agents/bulk", handleBulkAgentCreate},
        Route{"DELETE", "/api/mgmt/agents/bulk", handleBulkAgentRevoke},
        Route{"GET", "/api/mgmt/audit", handleCrossVaultAudit},
        Route{"POST", "/api/scim/v2/Users", handleSCIMUserCreate},
        Route{"DELETE", "/api/scim/v2/Users/{id}", handleSCIMUserDelete},
        Route{"POST", "/api/autoscope", handleAutoScope},
    )
}
// routes.go (shared)
package api

var enterpriseRoutes []Route // empty in community build

func RegisterRoutes(mux *http.ServeMux) {
    // shared routes
    mux.HandleFunc("GET /api/entries", handleListEntries)
    mux.HandleFunc("GET /api/entries/{id}", handleGetEntry)
    mux.HandleFunc("GET /api/ext/totp/{id}", handleGetTOTP)
    // ... all shared routes

    // enterprise routes (empty slice in community build)
    for _, r := range enterpriseRoutes {
        mux.HandleFunc(r.Method+" "+r.Path, r.Handler)
    }
}

Scope implementation (shared)

Implement the scope model in the shared codebase. This is needed by both editions.

Schema (add to existing DB):

-- agents table
CREATE TABLE IF NOT EXISTS agents (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    token_hash  TEXT UNIQUE NOT NULL,
    name        TEXT NOT NULL,
    scopes      TEXT NOT NULL DEFAULT '',
    all_access  INTEGER NOT NULL DEFAULT 0,
    admin       INTEGER NOT NULL DEFAULT 0,
    created_at  INTEGER NOT NULL
);

-- add scopes column to entries table
ALTER TABLE entries ADD COLUMN scopes TEXT NOT NULL DEFAULT '';

Agent ID = scope. Auto-increment ordinal, displayed as 4-char zero-padded hex (printf("%04x", id)).

Token format: cvt_ prefix + 32 bytes random (base62). Stored as sha256 hash. The cvt_ prefix allows secret scanning tools (GitHub, GitLab) to detect leaked tokens.

Matching logic:

  • Entry scopes empty ("") = owner only (all_access required)
  • Otherwise: set intersection of agent.scopes and entry.scopes
  • all_access=1 bypasses scope check

Flags:

all_access admin Role
0 0 Scoped read-only (agents, family members, MSP techs)
1 0 Read everything, change nothing (MSP tech full access)
0 1 Scoped read + admin ops with WebAuthn (unusual but valid)
1 1 Vault owner

Admin operations (shared, WebAuthn-gated)

All admin endpoints require: valid bearer token + admin=1 + valid WebAuthn assertion.

POST   /api/agents              create agent/token
GET    /api/agents               list agents (admin read, no WebAuthn needed)
PUT    /api/agents/{id}          modify agent
DELETE /api/agents/{id}          revoke agent
POST   /api/entries              create entry
PUT    /api/entries/{id}         modify entry (including scopes)
PUT    /api/entries/{id}/scopes  modify scopes only
DELETE /api/entries/{id}         delete entry
POST   /api/webauthn/challenge   get WebAuthn challenge (60s TTL)

WebAuthn tables:

CREATE TABLE IF NOT EXISTS webauthn_credentials (
    id          TEXT PRIMARY KEY,
    public_key  BLOB NOT NULL,
    sign_count  INTEGER NOT NULL DEFAULT 0,
    created_at  INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS webauthn_challenges (
    challenge_id TEXT PRIMARY KEY,
    challenge    TEXT NOT NULL,
    created_at   INTEGER NOT NULL,
    expires_at   INTEGER NOT NULL
);

WebAuthn flow

1. Client: POST /api/webauthn/challenge → vault returns random challenge (60s TTL)
2. User taps hardware key (fingerprint, face, YubiKey)
3. Browser signs challenge with hardware token's private key
4. Client sends: bearer token + X-WebAuthn-Assertion header + X-WebAuthn-Challenge header + admin request body
5. Vault verifies:
   a. Bearer token valid? (agent row exists)
   b. admin = 1?
   c. WebAuthn signature valid against stored public key?
   d. Challenge matches and not expired?
   → All yes? Execute admin operation.
   → Any no? 403.
6. Challenge deleted (single use). Expired challenges cleaned up periodically.

Auto-scope (enterprise only)

//go:build enterprise

// POST /api/autoscope
// Takes entry fields, returns suggested scope assignments
// Uses LLM to classify fields as Credential vs Identity
// and suggest which agent scopes should have access

Makefile

build:
	go build -o clavitor .

build-enterprise:
	go build -tags enterprise -o clavitor .

build-prod:
	GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -tags enterprise -o clavitor-linux-amd64 .

Constraints

  • The community binary must work standalone with zero enterprise code compiled in
  • No runtime checks for "is this enterprise?" — the code paths don't exist
  • No feature flags, no license keys, no env vars
  • Enterprise files must all have //go:build enterprise as the first line
  • The agents table and scopes column are shared (both editions need scoping)
  • WebAuthn is shared (both editions need hardware-gated admin)
  • Token format cvt_ prefix is shared
  • Cannot delete the last admin agent (prevent lockout)
  • Cannot remove admin flag from self if no other admin exists

Tier enforcement

The vault itself does not enforce tier limits. The hosted management layer (outside the vault) enforces:

  • Token count: SELECT COUNT(*) FROM agents checked before POST /api/agents
  • Device count: SELECT COUNT(*) FROM webauthn_credentials checked before WebAuthn registration
  • Vault count: checked at the account level, not within the vault

Self-hosted (Community Edition) vaults have no limits on tokens or devices within a single vault.

Migration

For existing vaults (no scopes yet)

  1. Add scopes column to entries: ALTER TABLE entries ADD COLUMN scopes TEXT NOT NULL DEFAULT '';
  2. Create agents table.
  3. Existing auth tokens become agent rows: create agent row for each existing token with all_access=1, admin=1 (vault owner).
  4. All existing entries have scopes = "" (owner-only) by default — secure default, nothing changes until user explicitly creates agents and assigns scopes.

Vault owner bootstrap

On first vault setup or migration:

  1. User registers WebAuthn credential (hardware key enrollment).
  2. System creates first agent row: id=1, name="Owner", all_access=1, admin=1.
  3. User receives their bearer token (shown once).
  4. All entries default to scopes="" (owner only).
  5. User creates additional agents and assigns scopes via admin operations.

Deliverables

  1. Implement the agents table, token auth, and scope matching in shared code
  2. Implement WebAuthn challenge/verify flow for admin operations
  3. Implement all shared API endpoints (read + admin)
  4. Create enterprise-only stubs (handlers_enterprise.go, routes_enterprise.go, autoscope.go, scim.go, audit_feed.go) with build tags and basic structure
  5. Update Makefile with both build targets
  6. Migration path for existing vaults (add scopes column, create initial owner agent)

Reference

  • Full scope spec: /home/johan/dev/clavitor/clavis/clavis-vault/SPEC-scopes.md
  • Pricing architecture: /home/johan/dev/clavitor/marketing/pricing-architecture.md