Expansion roadmap
Multi-App Plan
Not implemented yet
Review findings (2026-05-29):
A security/architecture review of this plan against the current listam-mobile
code found that the plan layers a rich role / permission / revocation / "minimum-credential"
vocabulary on top of an Autobase + BlindPairing substrate that, as implemented, only supports
"full writer holding the encryption key" or "no access."
Several headline promises are not achievable without new work. Critical and High items below
should be treated as blocking acceptance criteria and resolved before package extraction or
the desktop/headless split begins.
Critical — substrate cannot honor the promises
- C1 — "Revoke" is impossible today. Autobase membership is append-only; there is no
remove-writer. Revoking an invite does nothing to a peer that already joined. Real removal needs an app-level ACL plus encryption-key rotation/re-encryption.
- C2 — Role-scoped credentials are unsupported. Pairing confirms with both
key and encryptionKey. You either hold the encryption key (full read, one append from writing) or hold nothing (opaque ciphertext). There is no read-only or sync-only credential.
- C3 — Any writer can add writers.
apply honors add-writer from any node and every writer can append. No owner authority, gate, or audit. Needs an owner key that alone authorizes membership, verified by signature.
High — riskiest new/inherited surfaces
- H1 — Owner-control admin channel underspecified. Remote
shutdown/export/import over the DHT with only "require pairing/auth." Needs per-device key pairs, signed commands with replay protection, and scoped capabilities — not bearer tokens.
- H2 — Deep links auto-join with no confirmation. A
listam.ch/join?invite= link calls RPC_JOIN_KEY directly and tears down the current base. Require explicit user confirmation before joining.
- H3 — Invites are reusable, non-expiring, pointlessly persisted.
INVITE_MAX_USES = 10, expires is never checked, and lista-invite.json is written but never read. Default to single-use + enforced expiry; delete the unused file.
Medium — design/sequencing fixes
- M1 — Backend keys items by
text, plan normalizes Redux by id. Migrate the replicated reduction to id-keying before Redux normalization or the two projections diverge.
- M3 —
loyaltyCards slice contradicts secure storage. Keep secret payloads out of Redux; store handles only and read secrets on demand.
- M4 — Corruption recovery silently wipes data. On an Autobase error the code deletes the base and recreates it. Require backup/consent before wipe — never auto-wipe a storage helper.
- M5 — Redaction is bypassable. Raw
console.error prints keys and item payloads today; committed log files exist. Add a no-console lint + CI secret-grep gate and remove committed logs.
| ID |
Finding (current code) |
Required fix |
C1 |
Joining appends add-writer → host.addWriter(key, {indexer:true}); membership is append-only with no removal. |
Separate "revoke invite" from "revoke access"; build ACL + key rotation/re-encryption, or state plainly that writers cannot be removed without re-keying. |
C2 |
Pairing shares autobase.encryptionKey; read access = full decrypt access. |
Re-scope roles: blind storage/relay = ciphertext only; any reader has full read. Resolve before headless co-invite. |
C3 |
apply trusts add-writer from any writer; no owner gate. |
Designated owner key authorizes membership, signature-verified in apply. Milestone deliverable. |
H1 |
Owner-control exposes shutdown/export/import with only "require pairing/auth." |
Per-device key pairs, signed commands + nonce/timestamp replay protection, scoped capabilities, rotatable device-bound tokens. |
H2 |
Linking handler calls startJoinWithInvite() with no confirm; joinViaInvite tears down the current base. |
Explicit user confirmation before any link-initiated join; do not tear down the current base until confirmed. |
H3 |
INVITE_MAX_USES = 10; expires never checked; lista-invite.json written but never read. |
Single-use, enforced expiry by default; delete unused persistence; UI must state that an invite grants permanent writer access. |
M1 |
Add/update/delete and rebuildListFromPersistedOps key by text, not id. |
Migrate reduction to id-keying (backfill legacy ids) before Redux normalization. |
M3 |
Plan stores loyalty cards in Redux and in secure storage (conflict). |
Redux holds non-secret handles only; read secret payloads on demand from secure storage. |
M4 |
Autobase ready() error deletes key file + base storage and recreates a fresh base. |
Backup/export and owner consent before wipe; never auto-wipe a storage-helper node. |
M5 |
Raw console.error prints base/writer/encryption keys and item payloads; logs committed to repo. |
no-console lint + CI secret-grep gate; remove committed logs and gitignore local logs plus generated P2P key/invite files. |
Implementation-plan check:
Every review finding now has a planned remediation path, an acceptance signal, and a test
expectation. The strategy is to harden the current mobile/backend substrate first, prove
the shared package boundaries second, and only then build desktop/headless trust
features on top of those boundaries.
| ID |
Implementation plan |
Acceptance signal |
C1 |
Rename current controls so "revoke invite" only stops future joins; do not ship "remove member" until membership epochs, owner authority, key rotation, re-encryption, and re-invite of remaining members exist. |
Invite revoke blocks new joins without false removal claims; true member removal completes a re-key flow and old members cannot decrypt or append accepted active-epoch operations. |
C2 |
Split current roles from future roles: today a device is either a trusted full participant or a blind helper with no encryption key. Add a separate blind-storage invite path before offering ciphertext-only helpers. |
Docs and UI never promise read-only or sync-only access with the current BlindPairing writer invite; tests prove blind helpers do not receive the Autobase encryption key. |
C3 |
Add owner-signed membership operations with owner key, target writer key, role label, operation id, timestamp, and signature; make apply reject unsigned or non-owner membership writes after migration. |
Non-owner writers cannot add writers; tests cover owner success, non-owner rejection, malformed signatures, replay rejection, and legacy migration. |
H1 |
Define owner-control as a separate signed capability protocol: per-device key pairs, command id, device id, scope, timestamp, nonce, payload hash, signature, replay tracking, and rotatable grants. |
Headless refuses unsigned, replayed, expired, or out-of-scope commands; diagnostics-only clients cannot call shutdown/export/import/topic configuration. |
H2 |
Parse link invites into pending state, show an explicit confirmation describing the base switch, and only call RPC_JOIN_KEY after the user confirms. |
Cold-start and foreground deep links cannot switch bases without confirmation; cancel leaves the current base untouched and failed joins roll back visibly. |
H3 |
Default invites to short-lived single-use credentials, enforce expiry/use count before BlindPairing confirm, delete unused plaintext invite persistence, rotate after use/revoke/expiration, and show permanent-writer warning until C1 is solved. |
Expired or exhausted invites cannot add peers; production does not create plaintext invite files; tests cover expiry, rotation, restart, revoke, and redacted logs. |
M1 |
Version list operations, backfill deterministic ids for legacy text-only entries, reduce by id when present, and emit id-bearing snapshots before Redux normalization. |
Backend and Redux agree for duplicate item names; existing lists migrate without losing order/done state; mixed legacy/new logs replay correctly. |
M3 |
Keep only non-secret loyalty-card handles/safe metadata in Redux; store barcode/QR payloads and sensitive details in platform secure storage, fetched only while rendering/scanning. |
Redux traces and persisted state never contain card payloads; AsyncStorage cards migrate; delete/export/redaction tests pass. |
M4 |
Replace auto-wipe with quarantine, backup/export, owner prompt, and headless owner notification. Headless/storage helpers must never destructively repair themselves without owner approval. |
Corruption never silently deletes storage; tests cover quarantine, backup-before-wipe, user cancel, approved fresh base, and redacted recovery logs. |
M5 |
Use a shared logger plus enforcement: no-console/banned API rule, CI secret-shape scan, committed log removal, explicit ignore rules for local logs and generated P2P key/invite files, redaction helpers, and debug/trace build gates. |
Raw production console logging fails lint; secret-shaped values fail CI if they appear in logs/exports outside explicit redaction tests. |
Lower-severity items (efficiency & correctness the milestone inherits)
Full-view replay on every sync
rebuildListFromPersistedOps replays the view from index 0 inside the 1-second join poll (up to ~120×). Add a materialized-view checkpoint to resume from; acceptance is bounded replay work during joins.
Full-list pushes instead of diffs
SYNC_LIST resends the whole list on poll ticks. Make per-item events the default and snapshots the exception; acceptance is no repeated full-list push during steady-state polling.
Fragile join state machine
Shared _writableCheckTimer across two pollers and a module-global _writeChain never reset across base teardown can send in-flight writes to the wrong base. Add per-base write contexts, reset write queues on teardown, and use distinct timers.
Singleton lock won't survive multi-app
lista.lock (wx, cleanup only on teardown) leaves a stale lock after a crash and cannot coordinate desktop + headless on one machine. Add a lease with owner pid/instance id, heartbeat, stale-lock recovery, and separate storage roots where appropriate.
No resource limits on headless relay
Store-and-forward of others' messages has no quotas/rate limits. Defer third-party relay/storage past milestone 1, then add per-topic quotas, queue caps, TTLs, rate limits, and visible storage usage.
No CI/test bootstrap
Zero tests exist and package-lock.json is gitignored. Stand up milestone-zero CI with a reproducible lockfile, test runner, lint, dependency hygiene, redaction scan, and backend reducer/join/security smoke tests.
Strategy improvements
Milestone zero first
Security hardening, test bootstrap, reproducible installs, and docs/wiki alignment should land before Redux/package extraction.
Repos and packages set up early
Stand up the separate app repositories (listam-mobile, listam-desktop, listam-headless) and the shared listam-shared package repo from milestone 1, and decouple @listam/backend from BareKit globals so the published backend package runs under mobile worklet, Pear Desktop, and headless Bare/Node.
Honest headless roles
The safe current tiers are trusted full participant and blind ciphertext helper. Richer read-only or sync-only roles require membership authority, key epochs, and a new credential model.
Contract tests everywhere
RPC numbers, protocol events, package exports, storage migrations, owner-control commands, and cross-app sync should all have boundary tests before desktop/headless are accepted.
Testing & cross-instance interaction:
Implementation agents should test each app in isolation and prove the instances interact.
Per-app test detail lives on the
Mobile,
Desktop, and
Headless testing sections; the shared harness and the
cross-instance matrix below are the source of truth for "instances interact properly."
Shared local test harness
Separate storage roots
Each instance starts with its own base dir (mobile = Expo document dir; desktop/headless take --storage <dir>). Never share lista-local between instances.
Private DHT bootstrap
Run a local hyperdht bootstrap node and pass it via BOOTSTRAP / --bootstrap so test peers discover each other without the public DHT.
Headless harness commands
Headless exposes scriptable primitives: create-base, print-invite, join <invite>, status, dump-list, add-item, edit-item, mark-done, delete-item, export, import, shutdown.
Content operation suite
Every shared harness run generates content, edits it, marks it done/undone, deletes it, and asserts the resulting snapshot syncs across peers. Assertions compare canonical item ids and deletion state, not display names, so duplicate-name content cannot collapse by text.
Deterministic teardown
Every test closes stores, destroys swarms, releases the lock, and removes temp dirs; seeded keypairs make assertions reproducible.
| Pairing |
Must prove |
| mobile ↔ mobile | invite/join; both devices generate items, edit text, mark done/undone, delete, and converge; duplicate-name handling by id (M1) |
| mobile ↔ desktop | parity sync both directions for generated, edited, completed, and deleted content; desktop stays usable and records edits/deletes while mobile is offline; reconnect sync converges |
| mobile ↔ headless | headless stays online while mobile closed; headless accepts generated/edited/deleted content while mobile is absent; reopen mobile → sync; export/import round-trip preserves ids, done state, edits, and deletions |
| desktop ↔ headless | invite created on headless, joined from desktop; owner-control from desktop; generated, edited, completed, and deleted content syncs both directions |
| 3-way | all converge after concurrent generate/edit/mark-done/delete operations; kill any one, others continue; rejoin reconciles without duplicates or resurrecting deleted items |
| membership (security) | owner add-member works; non-owner add-writer ignored (C3); member-removal re-key — removed instance cannot follow the new epoch (C1) |
| credential boundary | blind-storage instance replicates but cannot decrypt view.get() (C2); trusted participant can read |
| invite safety | link-join requires confirmation (H2); expired/exhausted invite rejected (H3) |
| owner-control | replayed/expired/out-of-scope command rejected; revoked device blocked (H1) |
| restart / persistence | each instance rebuilds identical state from disk after restart |
Decision:
After milestone-zero hardening, use Redux Toolkit before creating the desktop
and headless versions. Autobase/Corestore remains the durable local-first source of truth,
while Redux becomes the shared UI projection, command dispatcher, and app-state model for
mobile and desktop.
Why Redux Toolkit
Future domain fit
Redux Toolkit is planned because Listam is expected to grow into multiple list types:
to-dos, simple tasks, calendar-derived lists, kanban boards, configurable rules, and
state transitions.
Shared app state
Normalized entities, predictable actions, selectors, and audit-friendly event trails
give mobile and desktop one shared app-state model while Autobase/Corestore stays the
durable replicated source of truth.
Boundary discipline
Redux should be the UI projection and command dispatcher, not the database. Backend
snapshots and protocol events reconcile Redux after persistence and replication.
Mobile
Keep the current Expo/React Native app as the first implementation target. Refactor it
to use shared Redux slices, typed backend commands, and platform adapters without
changing the current simple-list behavior.
Desktop
Build an enhanced Pear Desktop app first, with Electron only as a fallback if Pear
blocks a concrete requirement. The first desktop release should match current Listam
behavior while adding large-screen density, keyboard actions, and clearer sync status.
UI implementation should follow listam-desktop/design-guide/.
Headless
Build a Pear Terminal/Bare personal server for always-on devices owned by the user. It
should usually run on another device, such as a Raspberry Pi, mini PC, NAS, or home
server, and act as a durable personal peer rather than a central cloud service.
| Repository |
Role |
First milestone |
listam-mobile |
Existing mobile app |
Current Listam parity after Redux Toolkit refactor. |
listam-desktop |
Pear Desktop app |
Shared backend, invite/join, list/grid views, and desktop-optimized UI. |
listam-headless |
Pear Terminal/Bare personal server |
Always-on owned device with P2P owner-control, setup/recovery CLI, invites, status, persistence, and topic services. |
listam-shared |
Versioned shared npm packages |
Domain, protocol, backend, client adapters, logging, secrets, and grocery intelligence published for all app repos. |
| Desktop detail |
Planned behavior |
| First milestone |
Current Listam list parity, invite creation/joining, peer and sync status, list/grid views, grocery grouping and icon intelligence, shared Redux/domain logic, and shared backend/client packages. |
| Large-screen improvements |
Denser list and grid layouts, keyboard-first actions, a larger multi-pane structure, clearer sync and peer diagnostics, and tray/status affordances where Pear Desktop supports them. |
| Design system |
Read listam-desktop/design-guide/ before building or changing desktop UI; its design system and example screens are binding references for layout, typography, color, spacing, states, and components. |
| Device operations |
Easier review of invites, peers, and owned headless-device connections from desktop diagnostics and management surfaces. |
Shared package plan
@listam/domain owns domain types, Redux slices, selectors, migrations, and business rules.
@listam/protocol owns command and event contracts for UIs, backend services, and future relay devices.
@listam/backend owns Bare-compatible Autobase/Corestore/Hyperswarm service code.
@listam/client owns mobile worklet RPC, Pear Desktop IPC, and headless P2P owner-control adapters.
@listam/logging owns append-only log writing, the shared log row schema, redaction, rotation, diagnostics readers, and export helpers.
@listam/secrets owns shared secret names, key fingerprints, migration contracts, redaction helpers, and platform secret-store interfaces.
@listam/grocery owns category, translation, grouping, and icon intelligence currently living in UI modules.
| Client adapter |
Where it runs |
Why it is needed |
| Mobile worklet RPC |
Expo/React Native app |
The backend runs inside the mobile process through BareKit IPC, so the app can command it without a local HTTP server. |
| Pear Desktop IPC |
Pear Desktop app |
The desktop app can use an embedded backend while sharing the same app-facing client API as mobile. |
| Headless P2P command stream |
Always-on personal server |
Encrypted request/response commands such as status, create invite, join invite, export, import, shutdown, topic configuration, and owned device management. |
| Headless P2P event stream |
Always-on personal server |
Encrypted live events for peer count, sync state, join progress, topic health, queue depth, backend errors, list updates, storage health, and owned device status. |
Headless difference:
Mobile and desktop are user-facing apps that can embed or start a backend while the user
is actively using them. The headless instance is an always-on service on another owned
device. After initial pairing, its default configuration path should be an encrypted P2P
owner-control channel, so the user's mobile or desktop app can configure topics, inspect
health, and attach it to lists without requiring shared LAN access, exposed ports,
Tailscale, or a screen UI.
| Headless first milestone |
Requirement |
| Long-lived peer |
Run as an owned always-on peer and persist the same Autobase/Corestore data model. |
| Control and status |
Create/join invites, expose peer count, sync state, base identity, storage status, and CLI commands for setup, status, invite, join, export, and shutdown. |
| Security defaults |
Require pairing/auth for control operations and never expose raw control endpoints publicly by default. |
| Setup guidance |
Ask which roles the device should perform and show the connection details needed to pair it with mobile or desktop. |
State-manager phase
- Add Redux Toolkit and create the initial mobile store first.
- Move list state, sync status, join status, peer count, invite key, preferences, and loyalty-card metadata handles into slices.
- Keep short-lived interaction state local when it only affects a modal, input, or gesture.
- Replace direct
useState ownership of replicated list data with Redux selectors and actions.
- Move backend command side effects into listener middleware or typed thunks.
- Keep the Bare worklet and RPC boundary as a platform adapter, not the owner of UI state.
| Redux slice |
State domain |
Why split it this way |
lists |
Replicated list entities, ordering, selected list, and optimistic list operations. |
Keeps durable app data separate from UI preferences and connection state. |
sync |
Backend readiness, peer count, invite key, join phase, sync health, and sync errors. |
Makes networking and backend lifecycle visible to mobile, desktop, and headless clients. |
preferences |
Grid/list mode, categories, category headers, icon size, text size, and icon style. |
Keeps local UI choices portable across mobile and desktop without mixing them into replicated data. |
loyaltyCards |
Local loyalty-card metadata handles until/unless records are later made replicated; barcode/QR payloads stay in secure storage. |
Allows sensitive local-only data to remain outside shared list state and outside Redux traces. |
ownedDevices |
Known headless instances and future dongles, trust status, supported roles, and last-seen status. |
Supports headless co-invite, personal servers, and later dongle onboarding without bloating list state. |
Why slices:
A Redux slice is a focused Redux Toolkit module with one domain's state, reducers/actions,
and selectors. Slices avoid one giant app state object, make shared mobile/desktop logic
easier to test, and leave clean extension points for tasks, calendar ingestion, kanban
boards, configurable rules, and future personal-life-management features.
Technical architecture notes
Package boundaries
Shared packages should separate pure domain code from platform code. Domain reducers,
selectors, protocol types, migrations, and grocery intelligence should stay usable in
any JavaScript runtime. Bare, Pear, React Native, P2P owner-control, and setup/recovery transport details
belong behind backend/client adapters so each app can swap transport without changing
list logic.
Replicated data authority
Redux should not become the durable database. Replicated list data remains authoritative
in Autobase/Corestore. Redux holds a local projection optimized for rendering,
optimistic interaction, and debugging. Backend snapshots and operation events reconcile
the Redux projection after persistence and peer replication.
Compatibility rule
Public command names, event names, operation versions, and RPC numeric values should be
treated as shared contracts. New behavior should append new commands or versioned fields
instead of changing old meanings, so mobile, desktop, and headless can update on
different schedules.
UI design system rule
Before implementing UI for any app or project, check for a project-local
design-guide/ directory. When it exists, its design system docs, tokens,
component rules, and example screens are the UI source of truth and must guide
implementation and review before generic visual preferences.
| Shared interface |
Expected shape |
Used by |
| Commands |
getStatus, requestListSnapshot, createInvite, joinInvite, addItem, updateItem, deleteItem, exportData |
Mobile, desktop, headless CLI, and trusted admin tools. Snapshot requests hydrate app state; P2P log replication remains automatic below this command layer. |
| Events |
statusChanged, listSnapshot, itemAdded, itemUpdated, itemDeleted, joinPhaseChanged, peerCountChanged, error |
Redux listener middleware, desktop status panels, headless monitors. |
| Storage identity |
Base key fingerprint, local writer key fingerprint, device id, role, and storage root. |
Sync status screens, diagnostics, owned-device management. |
| Owned devices |
Known headless instances and future dongles with trust status, supported roles, last seen, paired control channel metadata, and topic permissions. |
Headless co-invite flow and future relay/storage device onboarding. |
Shared command and event flow
- The UI dispatches a Redux action such as add, update, delete, create invite, or join invite.
- Redux listener middleware calls the active
@listam/client adapter instead of importing platform APIs directly.
- The adapter translates the command into mobile worklet RPC, Pear Desktop IPC, or the headless P2P owner-control stream.
- The backend validates writability, appends to Autobase, updates the materialized view, and emits protocol events.
- The adapter receives events through IPC callbacks or encrypted P2P owner-control events and dispatches normalized Redux events.
- Selectors derive screen-ready state for list views, grid views, sync indicators, device lists, and diagnostics.
| Headless surface |
Technical responsibility |
Notes |
| CLI |
Setup, status, invite creation, joining, export/import, shutdown, and service diagnostics. |
Useful over SSH on always-on devices and for recovery when no desktop UI is available. |
| P2P owner-control |
Default encrypted control plane for trusted user-owned apps after pairing. |
Use it to configure topics, roles, storage policy, invites, queues, and diagnostics without depending on LAN ports or Tailscale. |
| Setup/recovery transports |
Temporary pairing and recovery paths. |
CLI/SSH, QR code, terminal pairing code, LAN HTTP/WebSocket, Bluetooth, USB, or Tailscale/MagicDNS can help establish or repair the P2P owner-control channel. |
| Holepunch peer |
Durable personal peer for bootstrap, Autobase replication, encrypted storage, and async message relay. |
Continues helping selected topics while mobile and desktop apps are closed. |
P2P owner-control strategy
- Use QR code, terminal pairing code, LAN, Bluetooth, USB, Tailscale/MagicDNS, or CLI/SSH only to establish trust for the first connection.
- Create a private owner-control topic between the trusted mobile/desktop app and the headless instance.
- Send signed, nonce-protected, capability-scoped configuration commands over encrypted P2P streams instead of requiring a long-lived HTTP server to be reachable.
- Keep owner-control separate from list replication topics, relay topics, and storage topics.
- Use setup/recovery transports only when pairing is new, broken, or manually being repaired.
Headless first-run setup
- The headless app starts on an owned always-on device and asks which capabilities should be enabled.
- It generates or displays pairing information for the user's mobile or desktop app, such as device id, public key fingerprint, pairing code, and supported roles.
- The user pairs the headless instance from a trusted app and establishes an encrypted P2P owner-control topic.
- The headless instance receives credentials according to the honest current role boundary: full participant credentials for trusted devices, or no encryption key for blind helpers.
- It reports status back over the owner-control event stream: peer count, topic health, storage usage, queue depth, protocol version, and recent errors.
| Headless capability |
What it does |
Why it helps |
| Bootstrap helper |
Stays online on allowed topics and helps the user's devices find a stable owned peer. |
Improves reconnect behavior when phones and laptops are offline or changing networks. |
| Replication helper |
Joins selected Autobase discovery topics and replicates content for lists the user allows. |
Keeps the shared log available even when the primary UI apps are closed. |
| Storage helper |
Retains encrypted replicated content under a configured storage policy; as a blind helper it should not receive the list encryption key. |
Provides redundant user-owned storage without turning Listam into a hosted cloud service or implying read-only credentials that do not exist yet. |
| Async message helper |
Queues encrypted store-and-forward messages for peers in the same allowed topics. |
Lets peers exchange messages or wakeup hints even when they are not online at the same time. |
| Notification/reconnect helper |
Tracks topic activity, queue depth, and peer availability for trusted owner clients. |
Gives mobile and desktop better sync diagnostics and a future path to push-style notifications. |
| Diagnostics helper |
Reports protocol version, storage health, topic health, peer counts, and recent errors. |
Makes an always-on device understandable and maintainable without attaching a monitor. |
Headless co-invite flow
- A user joins someone else's list from mobile or desktop using the normal invite flow.
- After that device becomes writable or receives the allowed access mode, the app offers to add the user's known headless instances.
- The user selects one or more owned devices and chooses an access mode such as writer, storage, or relay.
- The joined device creates a short-lived delegated invite scoped to the target owned device and access mode.
- The headless instance accepts the delegated invite over its trusted owner-control channel.
- For writer mode, the joined device appends
add-writer. For trusted storage under the current substrate, the headless device must be treated as a full trusted participant if it receives list credentials. For blind storage or relay mode, it avoids list encryption credentials.
- Delegated co-invites inherit lifetime, use-count, expiration, revocation, audit logging, and redaction rules from normal invites.
- Future read-only or sync-only roles require the membership-authority and key-epoch work from C1–C3.
Logging principle:
Every app and service should write local append-only JSONL logs with the app or instance
name on each row. Logs are diagnostics, not replicated app state. They can be viewed from
mobile, desktop, headless CLI, or trusted owner-control diagnostics, and development builds
can request redacted log bundles from trusted peers for export.
| Log surface |
Append target |
Visible from |
| Mobile app |
mobile.log.jsonl plus embedded backend logs when available. |
Mobile diagnostics screen, desktop peer diagnostics in development, exported bundles. |
| Desktop app |
desktop.log.jsonl plus embedded backend logs when available. |
Desktop diagnostics screen, mobile peer diagnostics in development, exported bundles. |
| Headless app |
headless.log.jsonl and service/backend logs on the always-on device. |
Headless CLI, trusted mobile/desktop owner-control diagnostics, exported bundles. |
| Backend service |
backend.log.jsonl when the backend runs as an embedded or standalone process. |
The host app diagnostics view and merged development log exports. |
| Future dongle tooling |
dongle.log.jsonl in bridge or companion tooling when storage permits. |
Owner-control diagnostics and hardware debugging exports. |
| Log row field |
Purpose |
Example values |
ts, level |
Sort events and filter by severity. |
2026-05-27T12:00:00.000Z, info, warn, error, audit |
app, instanceName, instanceId |
Keep merged logs readable when several peers contribute rows. |
mobile, desktop, headless, Romme iPhone |
runtime, component, event |
Identify which layer emitted the event. |
react-native, bare, sync, p2p, peer_count_changed |
topicId, baseId, requestId |
Correlate commands, replication, topics, and backend responses without exposing raw keys. |
Short redacted fingerprints and command correlation ids. |
message, details |
Human-readable summary plus structured metadata. |
Redacted storage path, queue depth, peer count, join phase, or protocol version. |
Development peer log request flow
- The developer enables diagnostics sharing for a trusted device group or owner-control relationship.
- A mobile, desktop, or headless instance sends
requestLogBundle over the trusted debug or owner-control channel.
- Peers return bounded, redacted JSONL bundles for the requested time range, levels, and components.
- The requester merges rows by timestamp while preserving
app, instanceName, and instanceId.
- The app exports the bundle as a zip file, plain JSONL, email attachment, or platform share-sheet file.
Logging controls
Levels
Use trace for very detailed development-only command/event flow,
debug for development diagnostics, info for normal lifecycle
milestones, warn for recoverable problems, error for failed
operations, fatal for startup or persistence failures, and
audit for security-relevant owner actions.
Rotation and retention
Start with smaller mobile/desktop retention, such as 10 MB files with 10 rotations,
and larger headless retention, such as 50 MB files with 20 rotations. Development can
keep more debug or trace data; production should default to
info and above.
Redaction
Redact before writing and before export. Owner-control tokens, pairing codes, invite
codes, base keys, writer keys, encryption keys, raw topic keys, auth headers, and local
API tokens should become short fingerprints or omitted values. Full user content should
also be omitted when a diagnostic event is useful without it.
Diagnostics filters
Diagnostics views should filter by app or instance name, level, component, topic/list
fingerprint, request or operation correlation id, time window, and warnings/errors only.
Useful events
Log startup/shutdown, backend readiness, Redux commands, adapter requests, invite and
pairing phases, peer count changes, replication progress, Autobase apply/snapshot,
headless role changes, queue depth, storage usage, exports/imports, and unhandled
errors.
Production boundary
Peer log requests should be disabled by default for normal production users. The user's
own paired headless instance may expose logs through explicit diagnostics actions over
owner-control, but logs should never be fetched from public endpoints.
Key storage update:
Plaintext Autobase key and encryption-key text files are acceptable only as a
development/prototype shortcut. Production apps should migrate sensitive values into
platform secure storage and leave only redacted fingerprints in normal app files, logs,
and diagnostic exports.
| Secret or metadata |
Production handling |
Why |
| Autobase/base key |
Store securely or as protected metadata, and log only a fingerprint. |
It identifies or bootstraps a replicated base and should not appear raw in logs or exports. |
| Autobase encryption key |
Store in platform secure storage, never as a long-lived plaintext app file. |
It protects encrypted values; leaking it can expose replicated content. |
| Writer keys and owner-control tokens |
Use platform secure storage and strict redaction. |
They control write authority and trusted device administration. |
| Invite codes and pairing secrets |
Keep short-lived, redact everywhere, and persist only when explicitly needed. |
They can grant access during onboarding or recovery. |
| Corestore data, indexes, logs, snapshots |
Keep in normal app storage with encrypted values where appropriate. |
These are durable app files, but raw secret material should not live beside them. |
Secret storage plan
Mobile
Use iOS Keychain and Android Keystore-backed storage, such as Expo SecureStore or a
lower-level native adapter if the Bare bridge needs tighter control.
Desktop
Use the OS keychain where possible. If unavailable, use an encrypted key file unlocked
by a device-local key or user passphrase, with strict file permissions.
Headless
Use the OS keyring, TPM-backed secret storage, systemd credentials, encrypted local
key file, or user-provided passphrase depending on the device. Plain files should be
explicit development mode only.
Migration
On startup, detect legacy plaintext key files, validate them, write sensitive values
to the secure store, replace normal metadata with fingerprints, delete plaintext files
after success, and log only migration status. Provide a recovery path if secure storage
is unavailable or the user is moving to a new device.
Dongle tooling
Avoid storing raw application encryption keys on generic relay hardware unless the
device is explicitly acting as trusted storage for the owner.
Reference targets
Mobile secret storage should align with Expo SecureStore, Apple Keychain Services, and
Android Keystore-backed storage.
| Next implementation hardening |
Required update |
Acceptance signal |
| Plaintext secrets |
Move encryption keys, writer keys, owner-control tokens, invite secrets, pairing
secrets, and loyalty card barcode/QR payloads into platform secure storage.
|
Legacy plaintext files and AsyncStorage loyalty records migrate once, then raw secrets are deleted. |
| Production logs |
Replace verbose raw console logging with the shared logging layer,
redacted fingerprints, production log-level gates, and repository ignore rules for
local logs plus generated P2P key/invite files such as autobase-key.txt,
local-writer-key.txt, encryption-key.txt, invite.json,
lista-*.txt, and lista-invite.json.
|
Automated checks prove base keys, encryption keys, writer keys, invite codes, peer keys, item payloads, and loyalty card data do not appear in logs or exports. |
| Invite lifecycle |
Add revoke and rotate controls, visible lifetime, remaining use count, scoped access
mode, expiration, and audit events.
|
Bounded-use invites cannot add more peers after expiration, revocation, rotation, or use exhaustion. |
| Loyalty card privacy |
Treat card names, barcode values, QR values, and barcode types as sensitive
local-only data unless the user explicitly chooses future replication.
|
Diagnostics, logs, Redux traces, and exports redact or exclude card payloads by default. |
| Recovery coverage |
Add tests for item reduction, duplicate handling, category lookup, join rollback,
corruption recovery, storage migration, and invite revoke/rotate behavior.
|
CI exercises migration and recovery paths before desktop/headless extraction. |
| Dependency hygiene |
Add direct dependencies for modules imported by loyalty card rendering, including
react-native-svg and qrcode-terminal, and verify whether
@expo/vector-icons should be direct.
|
A dependency check fails when source files import undeclared runtime packages. |
Implementation constraints
Security boundary
Owner-control tokens, pairing codes, invite codes, base keys, encryption keys, and
writer keys must be redacted from production logs and should not remain in plaintext
app files. Public internet exposure should require an explicit future decision, not a
default headless behavior.
Failure modes
Adapters should report typed failures for backend unavailable, not writable, pairing
timeout, invite expired, incompatible protocol version, storage locked, and auth
rejected. Redux should surface these in sync or ownedDevices
instead of hiding them in console logs.
Migration order
Run milestone-zero hardening first, refactor mobile onto the shared
Redux/protocol/client packages second, then extract the backend package, then build
desktop and headless. That keeps the current app working while each shared boundary is
proven by the existing product.
Future direction
Near-term model rules
Use stable item ids when available, preserve compatibility with legacy text-only
entries, normalize entities in Redux, keep operation contracts append-only and
versioned, and avoid embedding UI-only concepts into replicated backend operations.
Personal-life-management lists
The first milestone should keep current simple Listam behavior, but the domain model
should be ready for to-dos, simple tasks, calendar-ingested lists, kanban-style boards,
configurable rules, and additional personal-life-management features.
Dongle compatibility
Future USB dongles should work with every app using the Holepunch stack, not only
Listam. Keep relay envelopes generic and encrypted so dongles can provide bootstrap,
async messages, blind relay, reliable redundant storage, store-and-forward storage,
push notification relay, reconnection help, media streaming/distribution improvements,
and Bluetooth configuration for relay topics without understanding Listam payloads.
Prototype hardware
The dongle prototype path currently considers Seeed Studio XIAO ESP32S3, ESP32-S3
DevKitC-1 N16R8, SPI microSD card reader modules, and 32 GB microSDHC cards. The plan
keeps those devices as relay/storage appliances around generic Holepunch topics rather
than Listam-only hardware.
First milestone boundary
Prove current Listam parity across mobile, desktop, and headless first. Do not add new
list domains in the first milestone; add the architecture that makes them possible.
All three app surfaces should consume the shared package versions and existing simple list
operations should remain compatible.
| Test area |
What to prove |
Examples |
| Redux and domain |
Reducers, selectors, preferences, join state, and legacy migration are stable. |
Add/update/delete, grouped selectors, peer status, and text-only item migration. |
| Backend and protocol |
Command/event contracts stay compatible across the separate app repositories. |
Snapshot sync, invite creation, join phases, peer count, errors, and RPC number compatibility. |
| Cross-app sync |
Mobile, desktop, and headless can all join and replicate with each other. |
Mobile-to-desktop, mobile-to-headless, desktop-to-headless, and headless restart persistence. |
| Manual acceptance |
The first milestone feels like current Listam across all app surfaces. |
Generate content on mobile, edit and complete it on desktop, delete content through headless, keep headless online, reopen mobile and verify sync, and compare implemented UI against any project-local design-guide/ examples. |
| Logging and diagnostics |
Append-only JSONL, redaction, rotation, export, and development peer-log requests are reliable. |
Mobile/desktop/headless labels, secret redaction, retention defaults, merged exports, and trusted owner-control/debug log bundles. |
| Key storage and secrets |
Legacy plaintext keys and AsyncStorage loyalty cards migrate into secure storage. |
Secure startup secret handoff, desktop/headless fallback paths, raw-secret redaction, secure delete/export for loyalty records. |
| Invite lifecycle |
Invite revocation, rotation, bounded use, expiration, and honest delegated headless co-invite modes work. |
Writer/full-participant and blind storage/relay delegated co-invite tests plus exhausted/expired invite rejection. |
| Data recovery |
Core list reduction and recovery paths survive replay, failed join, and corrupted local state. |
Item reduction, duplicate handling, category lookup/grocery intelligence in CI, join rollback, Autobase/Corestore corruption recovery, and undeclared dependency checks. |
Assumptions:
Redux Toolkit is chosen; current Listam parity is the first milestone; milestone 1 uses
separate app repositories (listam-mobile, listam-desktop,
listam-headless) with versioned shared packages from listam-shared;
Pear Desktop and Pear Terminal/Bare are primary targets; Electron is only a fallback; new
list domains come after the foundation; future relay dongles remain generic for
Holepunch-stack apps; plaintext key files are legacy/development-only and should be migrated
before production release.
listam-multi-app-plan.md