docs(arch): ADR-035 — concrete persistence adapter shapes, resolve OQ-36
Commits the concrete adapter shape deferred by ADR-033: read-sync / write-async split with honker NOTIFY/LISTEN for no-restart cache invalidation, against SQLite, in a separate alknet-store-sqlite crate. Two constraints drive the design: (1) the hot-path read trait (IdentityProvider::resolve_from_fingerprint, CredentialStore::get) is sync — called in the accept loop, no .await — so a SQLite-backed adapter must cache in memory and serve sync reads from the cache; (2) auth changes must take effect without a restart (an early issue the project already fixed for ConfigIdentityProvider via ArcSwap config reload). honker's SQLite NOTIFY/LISTEN (single-digit-ms wake, no polling) is the cache-invalidation mechanism that makes both hold: write commits to SQLite + emits NOTIFY, the running process's LISTEN wakes, the in-memory index reloads and atomically swaps, the next read sees the new state. Same ArcSwap-reload pattern as config, generalized from 'config file is source of truth' to 'SQLite is source of truth, honker signals when it changed.' New async IdentityStore write trait (put_peer / update_peer / remove_peer) extends the sync IdentityProvider read trait for peer mutations. ConfigIdentityProvider does NOT implement it (config reload is its write path — a posture enforced by the absence of a backend, not a type-system constraint); SqliteIdentityProvider implements both. CredentialStore::put/delete refined to async (within ADR-031's one-way door — the contract was get/put/delete keyed by provider persisting EncryptedData never decrypting; sync-vs-async was unspecified). CredentialStoreError renamed to shared StoreError covering both traits. alknet-store-sqlite is one crate implementing both IdentityStore and CredentialStore with shared SQLite connection + honker LISTEN infra (splitting later is a two-way door). Schema shape committed (one row per PeerEntry with JSON columns for fingerprints/scopes/resources; one row per EncryptedData blob keyed by provider); exact DDL is an implementation-detail two-way door in the adapter crate. The keypal adapter-factory pattern is intentionally not ported to Rust (runtime column-mapping is a TS affordance; in Rust each adapter is a concrete type, cross-cutting concerns are a shared helper module). Amends ADR-031 (put/delete async refinement, StoreError rename), ADR-033 (concrete adapter shape now specified, two-crate framing collapsed to one), ADR-034 (OQ-36 now resolved), auth.md (IdentityStore section, cache-invalidation summary, OQ-36 reference), config.md (two write paths note), and the OQ-36/OQ-34 entries in open-questions.md. Review fixed 4 criticals (error-type name divergence, duplicate IdentityProvider sketch, upsert/Duplicate ambiguity, 'shape unchanged' contradiction), 7 warnings, 5 suggestions.
This commit is contained in:
@@ -587,12 +587,14 @@ is a feature extension, not an unmade architecture decision.
|
||||
them, wired by the assembly layer.
|
||||
|
||||
The concrete adapter shapes (table schemas, backend choice, indexing,
|
||||
caching) are the two-way-door remainder, tracked as OQ-36 (deferred for
|
||||
exploration). The trait shapes are the one-way door, committed by
|
||||
ADR-030, ADR-031, and ADR-033.
|
||||
caching) were the two-way-door remainder, tracked as OQ-36 — **now
|
||||
resolved by [ADR-035](decisions/035-concrete-persistence-adapter-shapes.md)**
|
||||
(read/write split, honker+SQLite, `alknet-store-sqlite` crate). The
|
||||
trait shapes are the one-way door, committed by ADR-030, ADR-031, and
|
||||
ADR-033; ADR-035 builds on them.
|
||||
- **Cross-references**: ADR-008, ADR-018, ADR-021, ADR-025, ADR-029,
|
||||
ADR-030, ADR-031, ADR-033, OQ-33, OQ-36, [auth.md](crates/core/auth.md),
|
||||
[config.md](crates/core/config.md)
|
||||
ADR-030, ADR-031, ADR-033, ADR-035, OQ-33, OQ-36,
|
||||
[auth.md](crates/core/auth.md), [config.md](crates/core/config.md)
|
||||
|
||||
## Theme: Storage and Adapters
|
||||
|
||||
@@ -623,45 +625,68 @@ is a feature extension, not an unmade architecture decision.
|
||||
- **Cross-references**: ADR-030, [auth.md](crates/core/auth.md),
|
||||
[config.md](crates/core/config.md)
|
||||
|
||||
### OQ-36: Concrete Persistence Adapter Shapes (Deferred for Exploration)
|
||||
### OQ-36: Concrete Persistence Adapter Shapes
|
||||
|
||||
- **Origin**: ADR-033 §"What this does NOT do" (concrete adapter shapes not
|
||||
specified), the project's note that the repo pattern is a tool to reach
|
||||
for, not a one-size-fits-all mold
|
||||
- **Status**: open (deferred for exploration)
|
||||
- **Status**: **resolved** (2026-06-28 by ADR-035)
|
||||
- **Door type**: Two-way (adapter shapes are implementation details;
|
||||
the trait shapes are the one-way doors, already committed by ADR-030/031/033)
|
||||
- **Priority**: medium (must be addressed before the next round of
|
||||
implementation; not blocking the current OQ-29 decision)
|
||||
- **Resolution**: The repo/adapter pattern is committed (ADR-033): core
|
||||
defines repo traits + in-memory default adapters; persistence adapters
|
||||
are separate crates; the assembly layer wires the adapter.
|
||||
- **Priority**: medium → resolved
|
||||
- **Resolution**: **[ADR-035](decisions/035-concrete-persistence-adapter-shapes.md)
|
||||
commits the concrete adapter shape.** The design is driven by two
|
||||
constraints: the hot-path read trait (`IdentityProvider::resolve_from_
|
||||
fingerprint`, `CredentialStore::get`) is **sync** (called in the
|
||||
accept loop, no `.await`), and auth changes must take effect **without
|
||||
a restart** (an early issue the project already fixed for
|
||||
`ConfigIdentityProvider` via `ArcSwap` config reload).
|
||||
|
||||
**What ships with core** (not deferred): the repo traits
|
||||
(`IdentityProvider`, `CredentialStore`) and their in-memory default
|
||||
adapters (`ConfigIdentityProvider`, `InMemoryCredentialStore`). These are
|
||||
the one-way-door commitments — they ship with the core crate, not as
|
||||
separate adapters. The in-memory adapters are real implementations, not
|
||||
stubs — a full repo pattern (the same trait surface a persistence
|
||||
adapter would implement), just backed by config / `HashMap` instead of
|
||||
a database.
|
||||
The resolution:
|
||||
- **Read trait stays sync; persistence adapters cache in memory.** A
|
||||
SQLite-backed adapter serves sync reads from an in-memory index
|
||||
(`HashMap<fingerprint, PeerEntry>` / `HashMap<String, EncryptedData>`),
|
||||
loaded from SQLite at construction and refreshed on honker `NOTIFY`.
|
||||
Same `ArcSwap`-backed full-reload pattern as `ConfigIdentityProvider`,
|
||||
generalized from "config file is source of truth" to "SQLite is
|
||||
source of truth, honker signals when it changed."
|
||||
- **New async `IdentityStore` write trait** (`put_peer` / `update_peer`
|
||||
/ `remove_peer`) extends `IdentityProvider` for peer mutations.
|
||||
`ConfigIdentityProvider` does NOT implement it (config reload is its
|
||||
write path); the SQLite adapter does. The read trait stays lean;
|
||||
the write surface is opt-in.
|
||||
- **`CredentialStore::put`/`delete` become async** (refines ADR-031's
|
||||
sync sketch — within the one-way door ADR-031 committed; `get` stays
|
||||
sync/cached). `InMemoryCredentialStore`'s write methods are
|
||||
async-with-no-awaits (signature change only).
|
||||
- **honker is the cache-invalidation mechanism** — a hard dependency of
|
||||
`alknet-store-sqlite`, NOT of `alknet-core`. honker's SQLite
|
||||
`NOTIFY`/`LISTEN` (single-digit-ms wake, no polling) is what makes
|
||||
the sync-read + cached-index + no-restart combination work. Without
|
||||
it, the adapter either polls (stale window) or requires restart
|
||||
(the bug already fixed). Not optional for the SQLite adapter.
|
||||
- **`alknet-store-sqlite`** — one crate, both adapters
|
||||
(`SqliteIdentityProvider: IdentityProvider + IdentityStore`,
|
||||
`SqliteCredentialStore: CredentialStore`), shared SQLite connection
|
||||
pool + honker LISTEN loop + bootstrap migrations. Splitting into
|
||||
two crates later is a two-way door (additive).
|
||||
- **Schema shape committed** (one row per `PeerEntry` with JSON
|
||||
columns for `fingerprints`/`scopes`/`resources`; one row per
|
||||
`EncryptedData` blob keyed by `provider`); exact DDL is an
|
||||
implementation-detail two-way door in the adapter crate.
|
||||
- **Shared `StoreError`** (`#[non_exhaustive]`, `thiserror::Error`)
|
||||
in alknet-core for both adapters.
|
||||
|
||||
**What's deferred**: the concrete *persistence* adapter shapes — table
|
||||
schemas, backend choice (SQLite + honker vs. a key-value store vs. a
|
||||
remote service), indexing, caching, connection management. These are the
|
||||
separate-crate adapters (e.g., `alknet-peer-store-sqlite`,
|
||||
`alknet-credential-store-sqlite`) that implement the core traits against
|
||||
a specific backend. The project is iterating on adapter simplification;
|
||||
the trait shapes are the commitment, the persistence adapter shapes are
|
||||
not. When a concrete use case (peer identity persistence across
|
||||
restarts, credential persistence across restarts, ACL delegation graph)
|
||||
forces a persistence adapter build, the adapter shape gets reasoned
|
||||
through then.
|
||||
|
||||
This OQ exists so the deferral is deliberate, not accidental — the
|
||||
pattern is committed, the in-memory adapters ship with core, and the
|
||||
persistence adapter shapes are the open exploration.
|
||||
- **Cross-references**: ADR-030, ADR-031, ADR-033, OQ-34,
|
||||
The keypal adapter-factory pattern is **intentionally not ported** to
|
||||
Rust (runtime column-mapping/type-coercion is a TS affordance; in
|
||||
Rust each adapter is a concrete type, cross-cutting concerns are a
|
||||
shared helper module). Two trait families (not one generic
|
||||
`Storage<T>`) preserved per ADR-033 §4. Redis / Postgres / on-chain
|
||||
adapters are **not needed for current scope** — the trait shapes
|
||||
make them possible; the adapter crates get built when a use case
|
||||
forces them.
|
||||
- **Cross-references**: ADR-004, ADR-011, ADR-014, ADR-020, ADR-025,
|
||||
ADR-030, ADR-031, ADR-033, ADR-035, OQ-33, OQ-34,
|
||||
[auth.md](crates/core/auth.md), [config.md](crates/core/config.md)
|
||||
|
||||
## Theme: TLS Identity
|
||||
|
||||
Reference in New Issue
Block a user