Global · all Listam apps

Listam CodeWiki

github.com/.../listam

A local-first app family with a real embedded P2P backend.

Listam is a React Native shopping list app where the visible UI is deliberately thin. The durable list state lives in a Bare worklet that runs inside the app process, writes to Corestore/Autobase, and synchronizes peers over Hyperswarm. The same backend is planned to power desktop and headless surfaces.

Runtime React Native + Bare
State Authority Backend worklet
Replication Autobase + Hyperswarm
Tests Found 0
Core thesis

What this project is trying to prove

Offline is the default

The README frames the database as the source of truth and networking as opportunistic. That design appears in the code: UI actions optimistically change React state, then send RPC commands to the backend for canonical log append and replication.

README.md

The backend is not simulated

The mobile app starts a packed Bare bundle using react-native-bare-kit. That bundle creates its own RPC server, Corestore, Autobase, Hyperswarm, and blind pairing state.

app/hooks/_useWorklet.ts:81
System map

Architecture

React Native UI

Header, list/grid views, join dialogs, loyalty cards, paywall

app/index.tsx
RPC over BareKit IPC

Bare backend worklet

Owns mutable list state, writer membership, networking, persistence

backend/backend.mjs

Corestore

Local Hypercore storage under app documents

Autobase

Encrypted append log plus materialized JSON view

Hyperswarm

DHT discovery and peer replication

BlindPairing

Invite-based writer onboarding

Boundary

Frontend files never open Corestore directly. They read/write through numeric RPC commands defined in rpc-commands.mjs.

Reason for the split

React Native stays focused on interaction and rendering, while the Bare runtime can run Node-like P2P libraries that are a poor fit for normal RN components.

Important coupling

The RPC command numbers are a shared ABI, short for Application Binary Interface: the low-level contract that lets separately built pieces of software call each other correctly. In Listam, that contract is the fixed numeric vocabulary both runtimes use to understand each other over bare-rpc. The symbolic names in rpc-commands.mjs make the code readable, but the value that crosses the React Native/Bare boundary is the number itself. If RPC_ADD stopped meaning 2 in one bundle while the other side still dispatches command 2 as "add item," the frontend and backend would no longer agree on what request was sent. Treat existing numbers as stable, append new commands with new values, and rebuild/update both sides together when the table changes.

Shapes and contracts

Data Structures

ListEntry

{
  text: string
  isDone: boolean
  timeOfCompletion: EpochTimeStamp
}

This is the public frontend item type. The backend appends richer items with id, listId, updatedAt, and timestamp, but the UI only relies on these three fields.

app/components/_types.ts

Autobase operation

{ type: "add", value: item }
{ type: "update", value: item }
{ type: "delete", value: item }
{ type: "add-writer", key: hex }

Operations are appended to Autobase, then the apply function materializes them into a JSON view and notifies the frontend.

backend/backend.mjs:239

Invite

{
  id: hex,
  invite: hex,
  publicKey: hex,
  expires: number
}

Blind pairing invites are encoded with z32 for sharing. A runtime invite is reused up to ten times before rotation.

backend/lib/network.mjs:168
Identity rule: The effective item key is text, not id. Add/update/delete logic filters and maps by text, so duplicate grocery names collapse into one item. This is simple and human-friendly, but it makes quantities or duplicate stores hard to model later.
Local-first stack

Corestore, Autobase, Hyperswarm, and BlindPairing

Simple mental model: Corestore is the local disk container, Autobase is the shared append-only database, Hyperswarm is how devices find and connect to each other, and BlindPairing is the invitation handshake that lets a new device safely learn which Autobase to join.
Persistence layer

Corestore

Corestore is a storage manager for Hypercore feeds. In this project it is the durable home for the local writer core, the Autobase system cores, the materialized view, and any peer cores that are opened by key.

Why Listam uses it

The app needs offline-first data that survives restarts. Corestore gives Autobase a single local storage root under the app document directory, so the backend can rebuild the list from persisted operations without a server.

Implementation details

  • initAutobase creates new Corestore(`${storagePath}-local`).
  • The worklet receives baseDir from Expo FileSystem and derives storagePath from it.
  • The local core's user data is inspected and sometimes cleared when joining a different base.
  • Static peer writer cores can be opened with store.get({ key: peerKey }).
  • On teardown, the store is flushed and closed after Autobase and swarm cleanup.
backend/lib/network.mjs:301 backend/backend.mjs:50
Shared database layer

Autobase

Autobase is the multi-writer append log. Devices do not call a central API to change the list. They append operations to their local writer, replicate those operations, and let Autobase produce a deterministic view.

Why Listam uses it

A shopping list is a good fit for an operation log: add item, update item, delete item, add writer. That log can be replayed after a restart and merged with peer logs when the network becomes available.

Implementation details

  • Autobase is created with valueEncoding: 'json', encrypt: true, and the saved or paired encryption key.
  • The open function returns a JSON view named test.
  • The apply function reduces add, update, delete, and add-writer operations.
  • Frontend writes are rejected until autobase.writable is true.
  • A promise chain serializes writes to avoid concurrent append/flush races.
backend/backend.mjs:230 backend/lib/item.mjs:8
Network layer

Hyperswarm

Hyperswarm is the peer discovery and connection layer. It finds other devices that are interested in the same discovery topic, then gives the backend live connections that can be handed to Autobase replication.

Why Listam uses it

The project goal is no central sync server. Hyperswarm lets devices discover each other by topic and replicate directly when both sides are online.

Implementation details

  • The main swarm joins autobase.discoveryKey, not the raw base key.
  • Connections update peer count and call autobase.replicate(conn).
  • Both server and client modes are enabled for the topic.
  • A temporary swarm is also created during invite joining to bootstrap pairing and early replication.
  • The temp swarm is cleaned up once the main swarm connects or the join flow finishes.
backend/lib/network.mjs:404 backend/lib/network.mjs:494
Invitation layer

BlindPairing

BlindPairing is the controlled first-contact mechanism. A new device starts with only an invite code. The host uses that invite to decide whether to reveal the base key, encryption key, and writer permission path.

Why Listam uses it

A collaborator cannot replicate a private encrypted Autobase without the right base and encryption credentials, and the host must add the collaborator as a writer. BlindPairing provides that join handshake without a Listam-owned server.

Implementation details

  • The host creates an invite with BlindPairing.createInvite(autobase.key) and shares it as z32 text.
  • The guest sends its local writer key in userData.
  • The host appends { type: 'add-writer', key: writerKeyHex } when writable.
  • The host confirms with autobase.key and autobase.encryptionKey.
  • The invite is reusable for up to ten successful pairings before rotation.
backend/lib/network.mjs:168 backend/lib/network.mjs:182

How the four pieces compose in Listam

  1. Boot: the frontend starts the Bare worklet and passes the app document directory.
  2. Storage: the backend opens Corestore at {document}/lista-local.
  3. Database: Autobase opens or creates the encrypted multi-writer base inside that Corestore.
  4. Discovery: Hyperswarm joins the Autobase discovery key and replicates connections.
  5. Sharing: BlindPairing creates an invite so another device can receive base credentials.
  6. Authorization: the host appends add-writer, making the guest writable after replication.
  7. Rebuild: on restart, Listam replays the Autobase view and sends the rebuilt list to React Native.

Interesting implementation details

The temp swarm is doing real work.

During joining, the temporary Hyperswarm connection is intentionally kept alive after blind pairing. The code comments explain that closing it would kill the live Noise connection that can immediately bootstrap replication while the main DHT swarm is still finding peers.

Changing bases requires clearing stale Autobase boot data.

When the guest joins a host base, initAutobase clears local-core user data like autobase/encryption and autobase/boot if the stored referrer does not match the new base key. That prevents the guest from accidentally using an old encryption key for a new base.

Writable state is the real permission signal.

The guest may receive credentials before it is actually writable. The backend polls autobase.update() for up to 120 seconds, syncs any replicated items along the way, and only declares join success once write access or peer connection criteria are met.

The materialized view is intentionally simple.

The view stores entries with op fields and reconstructs the current list with a Map keyed by item text. This keeps rebuild logic small, but it also means duplicate item names collapse into one logical item.

Audit lens

Security and Privacy Notes

Strengths

  • No central list server is present in the app architecture.
  • Autobase is configured with encrypt: true.
  • Mutations are rejected when the local Autobase is not writable.
  • Backend duplicate instances are guarded by an exclusive lock file.
  • Camera permission is requested only for loyalty card scanning.

Review Carefully

  • Base keys, encryption keys, and invite data are stored as plain files, not Keychain/Keystore entries.
  • Verbose logs print base keys, local writer keys, invite codes, peer keys, and item payloads.
  • Invites are reusable up to ten pairings and do not have an obvious user-facing revocation control.
  • Joining shares the encryption key and adds the guest as a full writer/indexer. Membership is append-only — there is no remove-writer, so a peer cannot be removed once joined.
  • Any writer can add further writers via add-writer in apply; there is no owner-only membership authority.
  • Read access requires the encryption key, which also grants full decrypt — there is no read-only or sync-only credential to back "storage helper" / "relay-only" roles.
  • Deep links (listam.ch/join?invite=) auto-join with no confirmation and tear down the current base.
  • Invite expiry (expires) is stored but never enforced; lista-invite.json is persisted but never read back.
  • Loyalty card barcodes are stored in AsyncStorage rather than secure storage.
  • Autobase corruption recovery deletes the base and recreates a fresh one, silently destroying data and identity.
  • The app has no automated tests for join, corruption recovery, or storage migration.

Recommended Hardening

  • Design a membership-authority + encryption-key-rotation model so members can actually be removed; separate "revoke invite" (stop new joins) from "revoke access" (re-key and re-invite).
  • Restrict add-writer to an owner key, signature-verified in apply.
  • Require explicit user confirmation before any link-initiated join, and do not tear down the current base until confirmed.
  • Default invites to single-use with enforced expiry; delete the unused lista-invite.json persistence.
  • Move encryption keys, writer keys, owner-control tokens, invite secrets, pairing secrets, and loyalty card barcode data into platform secure storage.
  • Redact secrets and item/card payloads from logs, then add production log-level gates, a no-console lint rule, a CI secret-grep gate, and automated redaction tests.
  • Back up / export and require consent before any corruption-triggered wipe; never auto-wipe a headless storage node.
  • Add invite revoke/rotate controls, visible lifetime, bounded use counts, access-mode labels, expiration, and audit events.
  • Add tests for item reduction, duplicate handling, category lookup, join rollback, corruption recovery, storage migration, and invite lifecycle behavior.
  • Add direct dependencies for loyalty-card rendering imports such as react-native-svg and qrcode-terminal, then add an undeclared-import check.
Deeper analysis: The Review Findings in the Plans section rank these issues by severity (C1–C3, H1–H3, M1–M5) and pair each with a concrete fix, implementation plan, acceptance signal, and test expectation.