Peer-to-peer · early beta

Peuple is a peer-to-peer social app.

Posts, direct messages and contacts are stored encrypted on your own device and sync directly with the people you choose — there's no company server in the middle. This page documents what's actually built: every feature below, and the architecture underneath it.

Status — early beta, v0.1.0 Platforms — desktop companion ready, Android beta soon Identity — a keypair, not an account Servers — none hold your content
Feed HotRecent
M
Mara
2h ago
Finally got the whole family off the group-chat app and onto Peuple.
💬 3 replies
🏛 Neighborhood Watch
J
Jonas
4h ago
Street lights back on since this morning — thanks to everyone who reported it.
💬 No replies yet

The real PostCard layout — Recent/Hot are both formulas you can read, not a hidden ranking.

Feature reference

Everything Peuple does, organized by area.

Eight areas cover the shipped feature set. Each card lists what's there today; open "Technical detail" for the implementation underneath.

Identity & contacts

Because an account is something a company can suspend, sell, or lose — a keypair isn't.

  • Ed25519 identity keypair, generated on-device from a 128-bit master seed
  • 12-word BIP39 recovery phrase — the only backup, and the only thing that restores you
  • Profile (name, about, avatar) stored locally, shared with contacts you add
  • Contacts added by scanning a one-time QR invite; pairing is mutual
  • Removing a contact rotates your feed's encryption key going forward
Technical detail
The seed lives in the device keystore (expo-secure-store). Every other key — identity, corestore, account, feed epochs, DM channels — is derived from that one seed via blake2b(seed, purpose), so the recovery phrase alone is enough; there's no separate backup file. An invite is z32(version | inviterPk | oneTimeSecret), exchanged over a swarm topic derived from the secret and MAC'd + signed by the inviter's identity key.

Feed & posts

Because you deserve to see what people actually posted — not what an algorithm decided to show you.

  • Text, photo and video posts; media stored as Hyperblobs (64KB blocks)
  • Threaded replies (comments) on any post
  • Two sort modes — Recent, or Hot (interactions per hour, cooling over time)
  • Multi-photo posts
Technical detail
Posts and media are encrypted Hypercores. Every contact holding a block serves it to other contacts on each connection (corestore.replicate) — torrent-style, so distribution scales with the audience rather than your device's own upload bandwidth.

Direct messages

Because a private conversation should stay between the two people having it — not sit on a company's servers.

  • Pairwise end-to-end encryption (X25519 ECDH between identities)
  • Reply/quote, edit (marked "edited"), delete for me or for everyone
  • Emoji reactions, star/pin messages, in-chat search
  • Delivery ticks (sent/delivered) — deliberately no read receipts, typing indicators or online status
  • Photos and video in DMs
Technical detail
Edit/delete/reaction ops are authorship-checked in core/lib/dms.js, so one side can never rewrite or erase the other's message. The DM key is static per pair — derived once from X25519(a,b) — so there's no per-message forward secrecy yet; see Known limitations below.

Spaces

Because a community you built shouldn't disappear the day a company decides to pull the plug.

  • Built on Autobase: every member writes to their own core, linearized into one shared view
  • Public or private — private Spaces are encrypted, invites carry the key
  • Threaded posts and replies
  • Invite existing contacts, or share an invite
  • Seven-level role ladder: banned → guest → member → trusted → moderator → admin → owner
  • Moderators can remove individual posts
Technical detail
Roles are enforced cryptographically inside spaces.js's apply() — the client never trusts a role value it didn't get back from the core. A Space keeps two views: the posts log (with remove-tombstones) and a separate role-change audit log.
$

Subscriptions

Because you should be able to support a page you like without being tracked for it.

  • Subscribe to a company's public feed — no handshake required
  • Sponsored posts appear only from accounts you subscribed to, labeled "Subscribed"
  • Publish your own public feed
  • Unsubscribing just stops replication — there's no subscriber list to delete, because there's no server holding one
Technical detail
A company identity is a normal Peuple identity plus an unencrypted public feed and a signed manifest, verified via a .well-known/peuple.json file on their domain — no central registry.

Linked devices

Because working from a laptop shouldn't mean trusting it with your entire identity.

  • Authorize a browser/desktop session by scanning a QR code from your phone
  • Each session is scoped (e.g. read-feed, send-dm) and time-limited
  • Revoke any session instantly from the phone
  • The desktop companion runs locally and never transmits your keys anywhere, including to Peuple
Technical detail
The phone signs a short-lived delegation — device public key, scopes, expiry — and peers accept a connection from that device key only while it's valid. Revocation is an op appended to the phone's account log, which replicates like everything else; peers reject revoked subkeys.

Moderation

Because keeping a community safe shouldn't require handing one company the power to silence anyone in it.

  • Report a user straight from the app — a signed, encrypted, one-way report
  • An opt-out default blocklist; blocked identities simply stop rendering locally
  • No content is deleted off anyone's device — moderation removes reach, not history
  • Moderators can remove individual Space posts (tombstoned for that Space)
Technical detail
Reporting opens a one-shot connection to a fixed rendezvous topic derived from the moderation identity's public key — there's no prior relationship to route through, unlike DMs. The blocklist reuses the Subscriptions primitive itself: "bans" are posts from an official moderation identity, filtered out of every timeline read. Known gap: live-pushed posts aren't filtered until the next read-based refresh.

Offline delivery

Because your messages should still arrive when you're offline — without anyone in between being able to read them.

  • Blind peers: always-on relays that store-and-forward ciphertext for offline contacts
  • They hold no read capability — encrypted blocks only
  • Also mirror your encrypted account log, which is what makes phrase-only recovery able to restore your contact list
Technical detail
See Holepunch's blind-peering explainer for the underlying mechanism. infra/blind-peer in the repo has instructions to run one.
Under the hood

Architecture & tech specs

One UI shell, one P2P core, connected by a typed RPC seam. No server ever sees plaintext or the social graph. This is the full technical picture: architecture, cryptography, data model, threat model, stack, and current limitations.

Architecture

The React Native app renders UI only. All networking, storage and crypto run in a Bare worklet, off the UI thread, talking to the UI over bare-rpc.

┌──────────────── phone ────────────────┐ │ React Native UI │ │ │ bare-rpc over IPC │ │ Bare worklet (core/app.js) │ │ ├─ Corestore (encrypted container) │ │ ├─ Hyperswarm (Noise, hole-punched) │──── contacts' phones │ └─ blind-peering client │──── blind peers (ciphertext only) └───────────────────────────────────────┘

Key material

Everything derives from one 128-bit master seed, backed up as a 12-word BIP39 phrase.

KeyDerivationPurpose
Identity keypairblake2b(seed, "peuple/identity") → ed25519Swarm auth, invite signatures
Corestore primary keyblake2b(seed, "peuple/corestore")Deterministic core keys → recovery
Account keyblake2b(seed, "peuple/account")Encrypts the account log
Feed epoch keyblake2b(seed, "peuple/feed-enc/<epoch>")Encrypts feed + media cores
DM keyblake2b(X25519(a,b), "peuple/dm", sort(pkA,pkB))Encrypts both DM cores of a pair

Data model

All Hypercores, all encrypted at rest.

CoreContents
accountLocal append-only log: profile, contacts, feed epochs, read state. Mirrored as ciphertext to blind peers for recovery.
feed / mediaPosts (JSON) and media blobs, one epoch per key generation.
dmOne core per side of a conversation, merged by timestamp client-side. Key rotations ride the same channel as system messages.
Space (Autobase)One writer core per member, linearized into a shared posts view plus a separate roles audit view.

Threat model

AdversaryMitigation
Server/relay operatorThere is no server holding plaintext; blind peers see ciphertext + discovery keys only
Network observerNoise-encrypted transport (Hyperswarm); the DHT reveals discovery keys, not content
Non-contact who obtains blocksBlocks are encrypted with feed/DM keys they don't hold
Removed contactFeed key rotation on removal
Stolen recovery phraseFull compromise — communicated prominently in onboarding/settings
Stolen, unlocked phoneOut of scope for this build; the keystore protects the seed at rest, the OS lock screen is the rest

Known limitations (current build)

  • Invites are exchanged live — both phones need to be online during pairing
  • DMs use a static per-pair key — no per-message forward secrecy yet (a Double Ratchet is a candidate future addition)
  • Network metadata — who's talking to the DHT — is not hidden
  • Live-pushed Feed/Space events aren't blocklist-filtered yet; only reads are (next refresh corrects it)

Stack

LayerTechnology
UI shellExpo / React Native 0.86, React 19 — renders UI only, never touches the network directly
Core runtimeBare worklet (core/app.js), runs off the UI thread
StorageCorestore 7 / Hypercore 11 — encrypted-at-rest, append-only logs
NetworkingHyperswarm 4 — Noise-encrypted transport, hole-punched P2P connections
Multi-writer forumsAutobase 7, used for Spaces
MediaHyperblobs 2 — chunked blob storage for photos/video
RPC seambare-rpc, typed command surface in packages/rpc (~50 commands)
Desktop companionNode.js — joins via the same device-delegation model as any other linked device

Repository layout

PathWhat it is
apps/mobileExpo (React Native) app — UI only, no P2P logic
coreThe P2P core (Bare worklet): identity, contacts, feeds, DMs, Spaces
apps/desktopDesktop companion, authenticated by device delegation
packages/rpcTyped RPC seam between UI and core
infra/blind-peerHow to run a blind-peer relay
docsArchitecture, threat model, phase-2 design notes

apps/mobile, apps/desktop and packages/rpc are public, MIT-licensed, on github.com/hottens/peuple. core, infra and docs stay private — that's the P2P protocol implementation itself.

Run it yourself

Core logic is plain JS, tested with brittle against a local hyperdht testnet — no network needed to run the test suite.

# from the repo root pnpm install # one bootstrap command pnpm test # core protocol tests, offline pnpm bundle:core # pack the worklet for ios/android cd apps/mobile pnpm exec expo prebuild # generate native projects pnpm android # or: pnpm ios (on macOS)
Download

Two ways to run it.

The phone app is the whole product — identity, feed, DMs, everything. The desktop companion lets you read and reply from a laptop, authenticated by your phone, without your keys ever leaving it.

🤖

Android

Beta APK available

Signed release build — identity, feed, DMs, Spaces, everything. Not on the Play Store yet, so Android will ask you to allow installs from this source the first time.

💻

Desktop companion

Ready to run

A local app for your laptop. It runs on your own machine, pairs with your phone over an encrypted device delegation you can revoke any time, and never sends your keys anywhere — including to us.

# unzip, then (needs Node.js 22+) ./start.sh # or start.bat on Windows # open the printed localhost link, then scan # the QR from Peuple → You → Linked devices