Add tools/cache package that re-exports luxfi/cache Cacher interface
and LRU, plus a TTL wrapper for time-based expiration. Migrate the
hand-rolled JWKS key cache in tools/security to use cache.TTL.
IAM is the sole user store. Base creates ephemeral (unsaved) auth records
from JWT claims for rule evaluation. No writes to _superusers or users
collections. IAM admins (isAdmin=true) get _superusers collection context
for full access. Regular users get users collection context.
Zero local user records. Zero _superusers records. IAM is truth.
When an IAM JWT has isAdmin=true, the user is mapped to a _superusers
collection record in Base. This gives IAM admins full superuser access
to all collections — no separate _superusers auth needed.
Removed the local token fallback for _superusers. IAM is the sole
auth source in external-only mode. No hacks, no PB legacy.
- loadAuthToken now falls back to local validation for _superusers tokens
when JWKS validation fails in external-only mode. Non-superuser local
tokens are still rejected. Fixes seeding with IAM enabled.
- Scrub PocketBase references from docs, SDK, and UI config
When IAM_ENDPOINT is empty or set to 'disabled'/'none', skip JWKS
registration and ExternalAuthOnly enforcement. This allows PB superuser
tokens to work for all collections, enabling data seeding and migration
without requiring a running IAM instance.
Implements OIDC authorization code flow with PKCE for the superuser
login page (/_/). When BASE_OIDC_ISSUER is set, the email/password
form is hidden and replaced with a single "Sign in" button that
redirects through the OIDC flow. The callback stores the auth token
in localStorage and redirects to the admin UI.
Routes:
GET /_/auth/oidc/redirect - start OIDC flow (state+PKCE cookies)
GET /_/auth/oidc/callback - exchange code, find/create superuser
GET /_/api/oidc/config - reports whether OIDC is enabled
Env vars: BASE_OIDC_ISSUER, BASE_OIDC_CLIENT_ID,
BASE_OIDC_CLIENT_SECRET, BASE_OIDC_AUTO_CREATE_SUPERUSER
- FullUserLifecycle: register → store → read → anchor
- MultiDeviceSync: laptop writes → phone syncs → phone edits → laptop merges
- OrgMultiTenant: same user in different orgs gets different DEKs
- ConcurrentAccess: 50 parallel writers, all readable, correct oplog
- AnchorDeterministic: same state → same root, new write → new root
- DeviceRevocation: close session zeroes DEK, reads fail after revoke
18 total tests. All pass. Zero external deps.
Local-first data SDK for Hanzo Base apps:
- Per-user SQLite shard (encrypted, isolated)
- DEK/KEK hierarchy: Master KEK → Org KEK → User DEK → AES-256-GCM
- Master KEK from HSM (AWS/GCP/Azure) or K-Chain ML-KEM threshold
- CRDT sync via ZAP for conflict-free multi-device merge
- Merkle root anchoring to chain (identity, not row data)
- API: /vault/put, /vault/get, /vault/anchor, /vault/sync, /vault/export
- All routes require auth — user ID from Base auth context
Zero integration: import plugin, register, done. Each user gets
their own encrypted database file. Reads/writes instant. Sync background.
Chain stores roots, never data.
- Switch from QEMU to native hanzo-build runners (amd64+arm64)
- Use hanzoai/.github docker-build.yml reusable workflow
- Fix: tag builds produced invalid refs/tags/ prefix in image tag
- Bump Go to 1.26
Replace "Hanzo Base" fallbacks with generic "Base" in all Svelte UI
components. When $appName is set (e.g. "<tenant>"), it displays
that instead. Help text uses "the application" rather than a brand.
Files: App.svelte, FullPage.svelte, PageWrapper.svelte, index.html,
PageBackups, PageStorage, PageMail, TrustedProxyAccordion,
CollectionRulesTab, LarkOptions, PageExportCollections
- ProvisionOrg/User creates directories and paths correctly
- Invalid slugs rejected (path traversal, uppercase, specials)
- HKDF DEK is deterministic, unique per org, unique per user,
org≠user even with same input (different HKDF info param)
- Empty master key returns empty DEK (dev mode, no encryption)
- Lookup works after provision, returns false before
- DeleteUser removes directory and lookup entry
- Integration: OrgDB + PoolManager read/write through dual pools
- Multi-org isolation: same table name, different data per org
- OrgDB DEK derivation: raw HMAC → crypto/hkdf.Key (RFC 5869, stdlib)
- security.SHA3_256, SHA3_512 using crypto/sha3 (FIPS 202, stdlib)
- No more x/crypto for key derivation or SHA3
Go 1.26 moved these from x/crypto to stdlib:
crypto/hkdf, crypto/pbkdf2, crypto/sha3, crypto/mlkem
PoolManager now lives in hanzoai/dbx v1.13.0 (canonical home).
base/plugins/platform/db_pool.go is a thin re-export wrapper.
Stack: dbx (pool) → orm (models) → base (platform)
One and only one way: dbx.NewPoolManager()
ID field changed to max=36, pattern=^[a-z0-9\-]+$ to support UUIDs.
Tests in apis/collection_test.go and plugins/migratecmd expected
the old max=15, pattern=^[a-z0-9]+$.
Optimizations from lux/metrics patterns:
- 16 shards with RWMutex (RLock for cache hits, Lock for misses)
- Cache-line padded PoolStats (no false sharing across cores)
- Two-phase eviction (find under lock, close without lock)
- FNV hash for shard selection
Benchmarks (M1 Max, -cpu=1,4,10):
CacheHit: 118 ns (0 alloc) — scales flat across CPUs
CacheMiss: 78 µs — dominated by SQLite open, no lock contention
ConcurrentHit: 1.8 µs at 4 CPUs (1.3x faster than single)
ReadWrite: 12 µs — stable across CPU counts (WAL is the bottleneck)
ManyOrgs: 1.1 µs at 10 CPUs (6.4x faster than single) ← sharding pays off
At 10,000 org scale with eviction pressure, 10-CPU throughput
is 3.1M ops/sec with only 465 B/op.
6 tests pass: get/release, LRU eviction, no-evict-in-use,
100-goroutine concurrent access, close, data persistence.
DBPoolManager: LRU cache of dual-pool SQLite connections (read pool
with 20 conns + write pool with 1 conn per database file). Evicts
least-recently-used pools when capacity is reached.
platformOrgDBResolver middleware (priority loadAuthToken+4):
- Reads X-Org-Id and X-User-Id headers (set by upstream middleware)
- Resolves or auto-provisions the org/user SQLite file
- Stores DBPool in request context via re.Set("orgDB"/"userDB")
- Services are fully stateless — any instance serves any org/user
File layout:
{DataDir}/orgs/{orgId}/org.db
{DataDir}/orgs/{orgId}/users/{userId}/data.db
Activated when OrgIsolation="sqlite" in PlatformConfig.