Files
base/LLM.md
zeekay 7e92f2d9e5 base: BASE_API_PREFIX + rip dead BASE_AUTH_MODE / /_/auth/oidc
One mount-prefix knob for the data plane: BASE_API_PREFIX (default
/v1). Multi-app deployments set /v1/<app> so a gateway can route by
path. Hanzo IAM is always a fixed sibling at /v1/iam — apps do not
mount their own IAM at /v1/<app>/iam.

Ripped:
- BASE_AUTH_MODE env knob — was a no-op after the IAM-native rip
  (the "builtin" branch had no routes left to bind).
- apis/superuser_auth_oidc.go (~460 lines) — stale parallel OIDC
  superuser path at /_/auth/oidc/*. The canonical path is
  /v1/iam/oauth/* via the platform plugin (proxy or embedded).
- apis/base.go EXTERNAL_AUTH_ONLY env-var fallback — the platform
  plugin is mandatory and sets the store keys directly. One way only.

installer.go: skip the first-superuser installer when the platform
plugin has flagged external-only mode (IAM owns first login).

LLM.md updated to reflect the new contract.
2026-05-15 16:03:03 -07:00

12 KiB

LLM.md - Hanzo Base

Identity

Hanzo Base is the local-first application runtime for Lux.

Not a fork, not a wrapper. The Web5 client runtime.

Architecture

Base = local encrypted SQLite + CRDT sync + chain-anchored trust
Lux  = trust/control plane (identity, keys, policy, anchors)

Per user/app/org:

  • Local encrypted SQLite file as the primary database
  • CRDT op log for sync/merge (encrypted ops — peers see ciphertext only)
  • Portable identity from Lux I-Chain (IdentityVM)
  • Key wrapping/recovery/sharing from Lux K-Chain (KeyVM) + T-Chain (ThresholdVM)
  • Chain anchors for integrity, policy, receipts, portability
  • Cloud sync as encrypted blob/oplog replication, NOT as source of truth

The server is a relay/index/cache layer, not the owner of truth.

Core Plugins

Plugin Path Purpose
vault plugins/vault/ Per-user encrypted SQLite shards, DEK/KEK, CRDT sync, chain anchor
kms plugins/kms/ Client-side KMS integration (talks to K-Chain or cloud HSM)
zap plugins/zap/ ZAP transport (8.7us latency)
platform plugins/platform/ Hanzo platform integration
functions plugins/functions/ Event workers (on CRDT ops, chain receipts)
jsvm plugins/jsvm/ JavaScript plugin VM

Vault SDK (plugins/vault/)

5 primitives, 18 tests:

  1. Identity — OpenUser(userID) -> resolve DID, derive DEK, bind device
  2. Key Access — DEK/KEK hierarchy: Master KEK -> Org KEK -> User DEK
  3. Local DB — Put(key, value), Get(key), Delete(key) -> encrypted SQLite
  4. Sync — Sync(), Merge(ops) -> CRDT over ZAP (encrypted ops)
  5. Anchor — Anchor() -> merkle root to chain, audit receipt

Key hierarchy:

Cloud HSM / K-Chain ML-KEM
  +-- Master KEK (never on disk)
        +-- Org KEK = HMAC-SHA256(master, "vault:org:" + orgID)
              +-- User DEK = HMAC-SHA256(orgKEK, "vault:user:" + userID)
                    +-- AES-256-GCM per entry (random nonce)

What Goes On-Chain (Lux)

  • DID / identity roots
  • Key handles and rotation events
  • Capability / policy state
  • Sync checkpoint Merkle roots
  • Audit receipts
  • Payment / metering records
  • Provider registry

What Stays Local (Base)

  • All mutable app data (SQLite)
  • CRDT operation logs
  • Decrypted user state
  • Device key material
  • Blob references
  • App-specific indexes

The Firebase Replacement

Firebase Web5 (Base + Lux)
Auth DID + capability/session gateway
Firestore Encrypted SQLite shard per user
Offline sync CRDT (already local-first)
Storage Content-addressed encrypted blobs
Functions Workers on CRDT ops / chain receipts
Security Rules Signed capabilities + chain policy

Roadmap

v1 (shipped): vault plugin, encrypted SQLite, DEK/KEK, CRDT sync, anchor v2: org sharing, multi-device enrollment, threshold recovery, per-collection sharing v3: provider marketplace, pay-for-sync/storage/recovery, portable exports v4: FHE/ZK policy modules for selected confidential compute workloads

Build & Run

go build ./...
go test ./...
go test ./plugins/vault/  # 18 tests
go test ./cmd/cli/        # 39 tests (network flags, cluster, operator, config, etc.)
go test ./cmd/            # integration tests (collection, record, login, superuser)

CLI Surface (2026-04-13)

Every Base-derived daemon (ats, bd, ta) uses cmd.AddCLISubcommands(root) to get:

Command Purpose Lux CLI Equivalent
cluster init/start/stop/status/leader/replicate/failover Manage base-ha HA groups lux network start/stop/status
operator apply/status/describe/upgrade/logs Manage Liquidity K8s operator CRDs lux chain deploy
config show/set-env/set-org/init CLI config (~/.config/base/config.json) lux config
status Daemon health + cluster state lux status
self version/doctor Binary management lux self
rpc get/post/patch/delete Direct API passthrough lux rpc

Network Flags

All commands accept --mainnet/-m, --testnet/-t, --devnet/-d, --dev. Exactly one may be set. Fallback: $LIQUIDITY_ENV -> $BASE_ENV -> default local.

Config File

~/.config/base/config.json (respects $XDG_CONFIG_HOME). Contains default env, per-env URLs, default org.

Cluster (HA)

Local mode (--dev): spawns N base-ha processes with auto-filled BASE_PEERS. K8s mode (--mainnet/--testnet/--devnet): kubectl scale against the correct GKE context. Consensus: --consensus lux (default) or --consensus pubsub.

Operator (K8s CRDs)

Wraps kubectl against liquid.network/v1alpha1 CRDs. Context map:

  • devnet: gke_liquidity-devnet_us-central1_dev
  • testnet: gke_liquidity-testnet_us-central1_test
  • mainnet: gke_liquidity-mainnet_us-central1_main

FHE Position

FHE is NOT the default execution model. Use it for:

  • Encrypted policy checks
  • Encrypted scoring/matching
  • Private collaborative computations
  • Sensitive server-side transforms

Do NOT make "FHE SQLite" the baseline. Local SQLite is decrypted locally. Cloud sees ciphertext. Chain sees commitments. FHE is opt-in compute.

Key Principle

Web5 = local-first apps with blockchain as the trust layer. Put trust on-chain, keep state local, sync privately, make identity portable.

Ecosystem Alignment (2026-04-10)

See the full alignment guide below. Summary of conflicts:

Area Base Current Ecosystem Standard Status
Timestamp fields created/updated createdAt/updatedAt CONFLICT
API prefix configurable via BASE_API_PREFIX (default /v1) /v1 or /v1/<app> DONE
Soft delete Hard delete only Deleted bool flag MISSING
Multi-tenancy None Per-org SQLite + CEK MISSING
Auth Built-in auth collections Hanzo IAM (OIDC/JWKS, mandatory) DONE (platform plugin, one way)
SSE event name CONNECT CONNECT OK (server + SDK aligned)
Error format {status, message, data} {status, message, data} OK
Pagination {items, page, perPage, totalItems, totalPages} Same OK

Migration path: 5 phases, backward-compatible aliases first. Full details: research brief produced by scientist agent on 2026-04-10.

IAM-native auth (one and only one way)

Hanzo IAM is the only auth source. There is no BASE_AUTH_MODE toggle, no built-in password / OTP / MFA / OAuth2 / email-change / password-reset surface, no legacy /_/auth/oidc/* parallel path. The platform plugin (plugins/platform/) is mandatory and registers IAM unconditionally; booting without IAM_ENDPOINT (or IAM_MODE=embedded) errors at startup.

Two ways to host IAM, identical OIDC contract from the client side:

  1. External (default): set IAM_ENDPOINT=https://hanzo.id (or your tenant). /v1/iam/* is a transparent reverse proxy to that endpoint. Full Hanzo IAM features: federation, MFA, social, magic links, multi-tenant orgs.
  2. Embedded: set IAM_MODE=embedded. /v1/iam/* is served in-process by the minimal OIDC provider in plugins/platform/iam_embedded.go (email+password only, no federation). For single-tenant solo deployments. See section below for the surface details.

Both modes expose the same six paths under /v1/iam/* (discovery, JWKS, authorize, login, token, userinfo) and the same RS256 JWT shape. The @hanzo/iam/browser SDK does PKCE redirect against either with no client-side branching — only the feature ceiling differs.

The middleware mirrors IAM identity into _superusers by email, so an admin row with the JWT's email automatically authorizes admin endpoints (collections, settings, logs, backups). Identity from IAM; admin privilege from a _superusers row keyed by email.

Store keys: StoreKeyExternalAuthOnly (always true once platform registers), StoreKeyJWKSURL (external mode), StoreKeyAuthUsersCollection (default "users").

Mount prefix (BASE_API_PREFIX)

One knob for where the app's data plane lives. Default /v1. For multi-app deployments where a gateway routes by path, set BASE_API_PREFIX=/v1/<app> (e.g. /v1/base, /v1/team).

The SPA client must match: VITE_API_PREFIX (in gui/apps/admin-*/vite.config.ts define block) is the client-side counterpart. Both are configured at deploy together.

IAM is always a fixed sibling at /v1/iam regardless of BASE_API_PREFIX. In production a gateway typically routes /v1/iam/* to the central IAM service; in solo/dev mode IAM_MODE=embedded serves it in-process. Apps do NOT mount their own IAM at /v1/<app>/iam.

Root liveness probe stays at /healthz (outside the mount prefix) so ops doesn't have to know the app name.

Embedded IAM Mode (IAM_MODE=embedded)

Set IAM_MODE=embedded to boot Base with an in-process OIDC provider at /v1/iam/* instead of reverse-proxying to an external Hanzo IAM. Same @hanzo/iam/browser PKCE contract from the client's perspective — the path doesn't change, only the implementation. We use /v1/iam, not /api/iam — this is NOT Casdoor.

Surface (minimal viable, NOT a full Hanzo IAM):

  • GET /v1/iam/.well-known/openid-configuration — OIDC discovery (issuer derived from request Host)
  • GET /v1/iam/.well-known/jwks — public RSA JWK
  • GET /v1/iam/oauth/authorize — plain HTML login form
  • POST /v1/iam/oauth/login — verifies email+password, redirects to redirect_uri?code=...&state=...
  • POST /v1/iam/oauth/token — exchanges single-use code for RS256-signed JWT (1h TTL)
  • GET /v1/iam/oauth/userinfo — bearer-validated user record

Signing key: ${DataDir}/iam.key (RSA-2048 PEM, 0600). Generated on first boot; back it up alongside the SQLite database — losing it invalidates every outstanding JWT.

Users: _iam_users system collection (email + bcrypt-cost-12 password

  • name). Bootstrap via either:
  • env: [email protected] EMBEDDED_IAM_ROOT_PASSWORD=... on first boot (no-op if _iam_users already has rows)
  • CLI: ./base iam-user create [email protected] (prompts for password via stdin, or honor IAM_USER_PASSWORD)

Token validation runs in-process via the platformEmbeddedAuth middleware bound at DefaultLoadAuthTokenMiddlewarePriority - 1. The JWT is verified against the local signer (NOT the JWKS-over-HTTP path external mode uses) and re.Auth is set to the matching _iam_users record, so the standard identity-header pipeline keeps working unchanged.

Out of scope (boot against an external Casdoor at IAM_ENDPOINT if you need any of these): multi-tenant orgs, social federation (Google/GitHub/SAML), MFA/OTP, password reset, refresh tokens, fancy login UI.

Network (Quasar replication) — singleton collapse (v0.48.1)

BASE_NETWORK=quasar only engages the Quasar cross-pod plane when at least one peer is present. A pod started with BASE_PEERS="" (empty or unset) collapses to the standalone noop: no ZAP listener, no self-dial, no reconnect loop. Same binary scales 1 → N by adding peers to BASE_PEERS.

Env matrix:

BASE_NETWORK BASE_PEERS Enabled Behavior
unset * false legacy single-node SQLite
standalone * false explicit standalone
quasar empty false sole writer, no replication
quasar a,b,... true full Quasar quorum over ZAP

BASE_PEERS entries may be the operator-emitted pod FQDN (liquid-bd-0.liquid-bd-network.<ns>.svc.cluster.local:9999) while BASE_NODE_ID is the bare hostname; isSelfPeer matches on the first DNS label so the transport never dials itself.