Mobile · Expo / React Native

Mobile App

Folder intent

Project Structure

Path Role Why it exists
app/ React Native application All user-facing state, gestures, dialogs, preferences, and local UI features.
app/hooks/_useWorklet.ts Runtime bridge Starts the Bare bundle once and translates backend RPC messages into React state.
backend/ Embedded backend source Owns canonical list mutations, Autobase setup, Corestore, P2P join, and cleanup.
rpc-commands.mjs Shared command ABI Keeps frontend and backend in sync without string command names.
assets/data/ Grocery taxonomy source CSV inputs for category and translation generation.
scripts/ Build and generation helpers Bundles the Bare backend and generates multilingual grocery lookup files.
website/ Public marketing/static site Listam landing page, privacy page, and join link handling.
/wiki/ This study site (multi-app wiki) Human-readable architecture docs for all Listam apps; a sibling folder of listam-mobile.
Behavior

Runtime Flows

1

App boot

useWorklet starts a singleton Bare worklet, passing the Expo document directory as the backend base path. It also creates the frontend RPC client.

app/hooks/_useWorklet.ts
2

Backend boot

backend/backend.mjs is a thin shim that calls startBackend(createBareKitPlatform(...)) from @listam/backend. The shared engine loads saved keys, starts an RPC server, and initializes Autobase.

backend/backend.mjs
3

List mutation

Slices dispatch RPC commands — RPC_ADD (2), RPC_UPDATE (3), RPC_DELETE (4), RPC_MOVE (28). The shared backend serializes writes and reduces through @listam/domain before appending to Autobase.

listam-packages/packages/backend/lib/item.mjs
4

Replication

Hyperswarm joins the Autobase discovery key as a topic. Peer connections are passed to autobase.replicate(conn).

listam-packages/packages/backend/lib/network.mjs

Invite join sequence

  1. Frontend parses an invite code or listam.ch/join?invite=... link.
  2. Frontend sends RPC_JOIN_KEY to the worklet.
  3. Guest opens a temporary Hyperswarm/BlindPairing candidate using the invite.
  4. Host validates the invite, appends an add-writer operation, and confirms base credentials.
  5. Guest reinitializes Autobase with the host base key and encryption key.
  6. Guest replicates over the temporary connection, then waits until it becomes writable.
listam-packages/packages/backend/lib/network.mjs
React Native layer

Frontend Functionality

The app is no longer a single text-keyed shopping list. It now manages typed lists organized into groups through a synced list registry, with grocery, board, and todo surfaces. UI state lives in Redux Toolkit slices fed by RPC replies, not in React state colocated in app/index.tsx.

App composition

app/index.tsx is the composition root. It wires useWorklet, useSubscription, join handling, the multi-list navigation chrome, and paywall gating, then hands UI state to Redux Toolkit slices.

app/index.tsx

Redux Toolkit state

listsSlice, boardConfigSlice, and labelsSlice (plus registrySelectors and syncSlice) hold list contents, board configuration, synced labels, and the active surface. Slices are fed by RPC replies decoded in _useWorklet.ts.

app/store/listsSlice.ts app/store/registrySelectors.ts

Typed lists & groups

A synced list registry layers named lists, organized into groups, on top of the project base. Each surface is keyed by (listId, type) — grocery, board, and todo — using the shared list-registry and list-nav domain modules.

app/store/registrySelectors.ts

Categories and icons

Category lookup uses generated multilingual maps, keyword fallback, substring matching, and Levenshtein typo tolerance. Category glyphs now render via MaterialCommunityIcons from @expo/vector-icons (a category-default plus curated overrides, since no icon kit covers every grocery item).

app/components/categoryLookup.ts app/components/categoryConstants.ts

Connection-status header dot

A header dot reflects real P2P state — green/online, blinking grey/connecting, or grey/no-connection — derived by deriveConnectionStatus from the DHT online flag and live connection count (not flushed(), which resolves offline too).

app/components/connectionStatus.ts
Day-plan surface

Overview / Day-plan

A cross-list day-plan surface that pulls flagged items into a single “today” focus, shared across devices over a dedicated meta-item channel with no new RPC opcode.

Shared plan channel

Plan state syncs through @listam/domain/plan over a reserved __plan__ (PLAN_LIST_ID) meta-item channel — no new RPC, just an RPC_UPDATE (3) upsert. isPlanItem guards keep plan meta-items out of every list projection.

listam-packages/packages/domain/plan.mjs
Lead: The day plan is a projection, not a new list type. Flagging writes a pointer meta-item to the shared base, so the same focus list appears on desktop and mobile without any extra wire format.
Bare worklet layer

Backend Internals

Shared engine

The real P2P engine now lives in @listam/backend — the mobile backend/backend.mjs only calls startBackend with the bare-kit platform adapter. The same engine ships on desktop and headless via different RPC/filesystem adapters.

backend/backend.mjs

Storage lease

The backend takes a storage lease so only one instance touches storage at a time; a second instance backs off rather than corrupting keys. Storage is namespaced (lista on mobile) to keep concurrent platforms isolated.

listam-packages/packages/backend/lib/storage-lease.mjs

Autobase initialization

initAutobase tears down prior resources, opens Corestore, clears stale boot/encryption metadata when joining a different base, enables encrypted values, rebuilds the list view, starts Hyperswarm, sets up blind pairing, and sends an invite to the frontend.

listam-packages/packages/backend/lib/network.mjs

Apply function

The Autobase apply function is the reducer. It validates item payloads, writes a materialized JSON view, and emits backend-to-frontend RPC updates. All item mutations flow through @listam/domain’s id-keyed, last-write-wins reduction — identical code on every platform.

backend/backend.mjs
Recent additions

Shipped mobile features

Move items between lists

MoveItemSheet moves items across lists and types via RPC_MOVE (28). When a target board needs rigor fields the move replies move-rigor-missing over RPC_MESSAGE so nothing is deleted.

app/components/MoveItemSheet.tsx

Reorder items

A long-press “Move to top/bottom” overlay reorders items through the shared last-write-wins order field in @listam/domain/ordering (sortByOrder / computeReorder).

listam-packages/packages/domain/ordering.mjs

Encrypted backup & import

Settings export/import all data and export the instance seed, password-encrypted with Argon2id + XChaCha20-Poly1305, via RPC_EXPORT_DATA (24), RPC_EXPORT_SEED (25), and RPC_IMPORT (26). Envelopes move through the OS share sheet and expo-document-picker.

app/components/BackupSettings.tsx

WYSIWYG markdown editor

RichMarkdownEditor edits ticket descriptions and markdown/callout blocks as live WYSIWYG (TipTap in a webview via @10play/tentap-editor) over the DOM-free @listam/domain/markdown; the stored value stays markdown.

app/components/board/RichMarkdownEditor.tsx

Synced peer/device names

A device-name input in Settings self-asserts a legible name over @listam/domain/labels; those names show on the members screen (MembersDialog). Labels sync through a registry-style meta-item channel.

app/components/MembersDialog.tsx
Persistence

Storage and Local State

Data Storage Owner Notes
Typed-list log and materialized view {document}/lista-local Bare backend Corestore + Autobase, encrypted when Autobase has an encryption key. One base holds multiple typed lists (grocery / board / todo).
List registry, labels & plan Autobase meta-items Bare backend Named lists/groups, synced peer/device names, and the day plan all replicate as reserved-bucket meta-items in the shared base.
Autobase base & encryption keys @listam/secrets secure storage Bare backend No longer plain-hex files. Keys live in @listam/secrets secure storage (Expo SecureStore); legacy lista-*-key.txt files are migrated and deleted (LEGACY_SECRET_FILES).
Invite Runtime / secure storage Bare backend BlindPairing invites are single-use and ~10-minute expiry; minted at runtime.
UI preferences AsyncStorage keys like @lista_grid_view React Native app View mode, category switches, icon/list sizing, and icon variant.
Loyalty cards @lista_loyalty_cards React Native app JSON array containing card name, barcode/QR data, and type.
Trial start @lista_trial_start React Native app Local timestamp used for the 30-day trial calculation.
Dependency roles

Libraries

React Native / ExpoMobile shell, app lifecycle, camera, linking, haptics, file paths.
react-native-bare-kitEmbeds and starts the packed Bare backend inside the app process.
bare-rpcTyped-by-number IPC contract between frontend and worklet.
CorestoreLocal Hypercore storage used by Autobase.
AutobaseMulti-writer append log and deterministic materialized view.
HyperswarmPeer discovery and replication connections.
BlindPairingInvite flow that exchanges base and encryption credentials.
z32Shareable encoding for blind-pairing invites.
react-native-iapSubscription paywall, trial expiry, purchase and restore.
Redux ToolkitUI state slices fed by RPC replies (lists, board config, labels, sync).
@expo/vector-iconsCategory glyphs render via MaterialCommunityIcons.
@10play/tentap-editorTipTap-in-a-webview WYSIWYG markdown editor for tickets and blocks.
react-native-ble-plxBLE transport for “Pair a leaf” ESP32 provisioning.
expo-document-pickerPicks encrypted backup envelopes for import.
@listam/secretsSecure storage for base/encryption keys and loyalty payloads.
package.json
Lead: App version 1.0.1. UI locales are System / EN / ES / DE / FR / IT / PT, derived from the shared @listam/i18n LOCALE_CHOICES.
Maintainability

Conventions and Quality Notes

Conventions observed

  • Single quotes and no semicolon-heavy style in application files.
  • UI state lives in Redux Toolkit slices fed by RPC replies, not colocated React state.
  • Backend mutable singletons are centralized in backend/lib/state.mjs.
  • Generated lookup files are marked as auto-generated and sourced from CSV.
  • RPC command constants are numeric and re-exported from @listam/protocol.

Quality gaps to track

  • package-lock.json handling: install requires --legacy-peer-deps.
  • Committed generated bundles (base64 backend) make review noisy.
  • Several new surfaces (Overview, leaf pairing, markdown editor) are typecheck-clean but still need an on-device dev-client build.
  • Board tickets are still written with the legacy wire type 'kanban' for old-peer interop (isBoardType() dual-reads).
Reading guide

Study Path

01

Start with the promise

Read the README to understand why the architecture is local-first and P2P.

README.md
02

Follow the boot path

Read useWorklet, then backend lock/key loading, then initAutobase.

_useWorklet.ts backend.mjs listam-packages/packages/backend/lib/network.mjs
03

Trace a mutation

Start at a list action in app/index.tsx, dispatch through a slice, cross RPC, append an op, then watch the shared reducer send a backend update.

app/index.tsx listam-packages/packages/backend/lib/item.mjs
04

Trace joining

Read the link parser, RPC_JOIN_KEY, blind-pairing candidate/member code, then the writable wait loop.

app/index.tsx listam-packages/packages/backend/lib/network.mjs
06

Run the hardening checklist

Use the Global security notes and the Plans review findings as a review checklist before release or wider collaboration.

Security notes Review findings
Clickable map

File Index

app/index.tsxComposition root, UI preferences, join handling, list mutation handlers. app/hooks/_useWorklet.tsStarts the Bare worklet, owns frontend RPC handler, exposes backend state. app/hooks/useSubscription.tsIAP setup, 30-day trial, purchase/restore, paywall gate. backend/backend.mjsThin shim that boots @listam/backend with the bare-kit platform adapter. @listam/backend lib/network.mjsCorestore/Autobase lifecycle, Hyperswarm, BlindPairing, join flow (shared engine). @listam/backend lib/item.mjsAdd/update/delete/move operations and validation (shared engine). app/secrets.tsSecure-storage bridge over @listam/secrets (keys, loyalty payloads, legacy migration). @listam/backend lib/backup-crypto.mjsArgon2id + XChaCha20-Poly1305 backup envelope crypto (shared engine). app/store/listsSlice.tsRedux Toolkit slice for typed-list contents, fed by RPC replies. app/store/registrySelectors.tsSelectors over the synced list registry (lists, groups, active surface). app/components/OverviewScreen.tsxDay-plan / focus surface (with PlanSheet). app/components/MoveItemSheet.tsxMove items across lists/types over RPC_MOVE (28). app/components/BackupSettings.tsxEncrypted backup export/import and seed export (RPC 24/25/26). app/components/board/RichMarkdownEditor.tsxTipTap-in-webview WYSIWYG markdown editor. app/hooks/useLeafProvisioning.tsBLE leaf provisioning hook (with LeafPairingDialog). categoryLookup.tsDominant-language detection and category matching. itemIconMap.tsManual and generated item-to-icon lookup logic. wrap-bare-bundle.mjsWraps packed Bare bundle as chunked base64 module.
Verification

Testing

How an implementation agent tests the mobile app in isolation and proves it interacts with other instances. Pair this with the implementation plans and the cross-instance matrix.

Unit

Store slices and selectors (test:store); category lookup and grocery intelligence (test:grocery); backend secret-storage migration and crypto (test:security).

Integration

Worklet RPC round-trips (add/update/delete → backend → SYNC_LIST / *_FROM_BACKEND); join success/timeout/rollback; secure-storage migration of keys and loyalty cards; deep-link join requires confirmation.

Manual / E2E

Parity checklist on iOS and Android; offline edit then reconnect-sync; restart persistence.

How to run

npm test runs test:security + test:store + test:grocery with node:test (TypeScript transpiled on the fly; there is no Jest). Expo dev client for manual runs; two simulators for device-to-device.

app/store/listsSlice.test.mjs
Interaction: Use the shared harness (separate storage roots + a private --bootstrap) to test mobile ↔ mobile, mobile ↔ desktop, and mobile ↔ headless. Mobile rows of the cross-instance matrix: generated, edited, completed, and deleted content converge; stays-synced-after-reopen; link-join confirmation (H2); and id-based duplicate handling (M1).