A self-hosted, end-to-end encrypted chat, voice, and video application — servers, channels, DMs, voice, screen share, and remote-desktop access to your own machines, with the server built so it cannot read your messages, calls, or files.
Encryption is enforced, not advertised: anyone whose browser cannot encrypt live media is muted rather than quietly relayed through the server.
Status: 0.9.x, in daily use by its author and a handful of friends, and
not yet used at any scale beyond that. The protocol and storage format are
stable enough that releases upgrade in place, and migrations run automatically
at startup. Expect the rough edges of a young project rather than of an
abandoned one; CHANGELOG.md shows the pace.
What it costs to run: one small VPS. Two vCPUs and 2 GB of RAM is enough for a group of friends, and the only recurring costs are that box and a domain. There is no hosted version to buy and nothing phones home — voice beyond a couple of people needs a TURN relay, which the deployment guide sets up on the same machine.
This is built and run by one person, for their own use and their friends'. It is in daily use, and it is not a product.
Issues and pull requests are welcome and will be read. Responses are
best-effort, and there is no support commitment, no roadmap you can hold anyone
to, and no promise that a given feature will ever exist. Security reports are the
exception and are taken seriously: see SECURITY.md for the
private route.
Said plainly so nobody has to guess, and so the absence of a reply is never a mystery.
| If you want to | Read |
|---|---|
| Get a straight answer to an obvious question | docs/FAQ.md — including what works on Linux, macOS and iOS |
| Know what it actually does, feature by feature | docs/FEATURES_AND_TECHNOLOGY.md |
| Decide whether to trust it | docs/SECURITY_MODEL.md — written for a reader who does not |
| Understand the encryption | docs/E2EE.md, docs/E2EE_RECOVERY.md |
| Know what the server can see | docs/PRIVACY.md |
| Run your own server | deploy/README.md |
| Use the app | docs/USER_GUIDE.md |
Púca ships every release as two distinct builds:
| Púca (Full) | Púca Lite | |
|---|---|---|
| Chat, voice, video, screen-share viewing, file transfer, E2EE | ✓ | ✓ |
| Screen sharing, and Clips (the replay buffer) | ✓ | ✓ |
| My Devices: remote desktop, remote input, Wake-on-LAN, remote file browser | ✓ | absent |
puca-agent / puca-service helper binaries — the processes that capture a whole desktop and synthesise input |
bundled | not shipped |
Lite is not Full with features switched off — the remote-control code is
excluded at compile time and is not in the artifact, which you can verify
yourself (frontend/scripts/check-no-rc.mjs builds both and proves the
difference, with a positive control). The two installs are mutually exclusive
on one machine but share their data: switching between them keeps your
session, keys, and history.
Be precise about what Lite does and does not remove, because the tempting
summary is wrong. Lite still shares your screen and still records clips, so the
screen-capture library is necessarily in the Lite binary — it cannot be
otherwise. What Lite does not have is the part that makes a machine
controllable: the puca-agent sidecar, which is what captures an entire
desktop unattended and injects keyboard and mouse events, is not bundled at all,
and the code paths that would drive it are removed at compile time. That sidecar
is also the binary antivirus heuristics react to. If your objection is "I don't
want a remote-access host on this machine", Lite answers it exactly. If your
objection is "I don't want any screen-capture code on this machine", Lite does
not answer it, and no build that can share a screen could.
Neither installer is code-signed. There is no Authenticode certificate yet, so Windows SmartScreen shows "Windows protected your PC" the first time you run either build, and you have to choose More info → Run anyway. Antivirus heuristics sometimes go further than a warning: Defender quarantined v0.8.82 on a user's machine as a false positive. That is the other reason Lite exists — it compiles the screen-capture and input-injection code out of the binary entirely, which is the machinery those heuristics react to — but Lite is unsigned too and gets the same SmartScreen prompt. docs/SECURITY_MODEL.md has the detail, including how to verify a download against the published hash.
Because Púca is self-hosted, the app you install is built for the server it
talks to. If someone runs a Púca server for you, get the installer or APK
from their download page — it offers both variants side by side. If you're
setting up your own server, deploy/README.md is the
complete path, including building both variants of the clients
(npm run tauri:build / npm run tauri:build:lite) and publishing your own
download page with the Full/Lite picker.
This section exists because privacy-and-security software makes big claims
easily and cheaply. The rest of this README is a normal project README; this
part is instead of one. If you only read one section, read this one, and then
go read docs/SECURITY_MODEL.md,
which is the long, unflattering-where-it's-unflattering version of everything
below.
Púca's architecture, implementation, and this repository's documentation were built through AI-assisted development — primarily Claude (Anthropic), across several model generations during the project's life, with one round of independent review conducted separately via Google Antigravity (Gemini). Practically all of the code is AI-generated under human direction, not AI-suggested-and-human-typed.
What that honestly means: this is a single-maintainer project with no
external audit, and "AI-assisted" is not a substitute for one — it changes
how the code was written, not whether it has been independently checked by
a second party. Read docs/SECURITY_MODEL.md §8
for the honest version of that argument, including the parts that cut against
this project (a bus factor of one, and a test suite where CI runs a smaller
fraction of it than you'd want).
On the Antigravity/Gemini review, specifically: it happened, at an earlier point in this project's life, and it is not the basis for any claim made here about current code — the codebase has changed substantially since, and this project's own internal history includes at least one Antigravity session, on unrelated work, whose self-reported "verified" and "deployed" claims turned out to be partially fabricated when checked against the actual git history and production state (documented in this project's own working notes; nothing here is hidden because it happened to us). That doesn't mean the crypto review was wrong — it means an unverified claim from that tool, including one about this project, is not evidence on its own. Treat "a review happened" as the honest content of that sentence, not "the review found nothing", which nobody currently reviewing this repo is in a position to stand behind.
Two things you don't have to take on faith, because you can rerun them:
-
A real vulnerability, found and fixed in this project's own audit process, is documented rather than quietly patched. The most recent security pass found and fixed a genuine E2EE flaw — a malicious server could pick a channel's encryption key by attributing a key-wrap to a fabricated user id, because the client trusted the wrapper's identity on first contact with no membership check. The fix is in this tree (
frontend/src/api/channelKeys.ts): every key-wrap is now attributed to an identity before it is trusted. One that contradicts a pinned identity is refused outright. One that merely cannot be verified is still used to read existing history — refusing it would lock people out of their own messages — but never to encrypt anything new, and the channel rotates its key before the next message is sent. Tests fail if that distinction regresses.The full write-up — including the residual limitation the fix does not close — is deliberately held back, because it is exploit-level detail about a flaw that may still be live on a deployment nobody here can see. That is the one place where publishing everything immediately would make users less safe rather than more.
Status, 2026-09-02: still withheld, and there is no file to link yet — on purpose, rather than a promise left hanging. Withheld: the exploitation detail (how a fabricated key-wrap was attributed) and the residual limitation of the fix. Published already, and enough to audit the fix: the paragraph above, and
frontend/src/api/channelKeys.tswith the tests that pin the read/write distinction. Why it is still withheld: this is self-hosted software, so "everyone has the fix" is not a fact any release can establish — a server running a build from before it is a third party's machine, not this project's. Revisit: at the 1.0 release, or sooner if the deployment it was found on is confirmed past the fix. The actionable part is not withheld and is the whole of it: run a current release.This is what "radical transparency" is meant to buy you: not a claim that the crypto is perfect, and not a promise to publish faster than is responsible, but a paper trail when it wasn't — including this paragraph telling you what is being withheld and why, rather than a link that quietly goes nowhere.
-
A real static-analysis pass, triaged rather than reported raw. Semgrep with
--config auto, which resolves to whatever community rule packs the registry serves for the languages it detects.The counts are deliberately not quoted here any more. They were, and they were wrong: the itemised categories did not add up to the stated total, and the saved run they came from is not in this repository, so nobody — including the author — could reproduce them.
--config autoalso pulls a different rule set on a different day, so any number frozen into a README is stale the moment it is written. A number you cannot check is worth less than no number.What is durable is the triage itself, and you can verify every line of it by reading the code it points at:
- The large majority were false positives specific to this codebase, each
verified by
reading the flagged code, not by assuming the tool is wrong. "Insecure
HTTP" hits are local
e2e/*.mjstest harnesses talking to127.0.0.1:3000in dev, not production traffic. "Insecure WebSocket" hits are a log message documenting that TLS termination happens at the reverse proxy (the process itself never speaks TLS, by design — seedeploy/README.md) and awss://→https://string-replace for deriving an admin API URL, neither an actual insecure socket. "Unsafe format string" hits areconsole.log/util.formatcalls in JS, where format- string injection isn't the exploitable bug class it is in C. A ReDoS warning flags aRegExpbuilt from a function argument that is, at every real call site, a hardcoded literal, never attacker input. An Android "exported activity" warning is the launcher activity, which Android requires to be exported for the OS to start the app at all. - 1 was real and is fixed as of this commit: this repo's own new CLA GitHub Action referenced a mutable version tag instead of a pinned commit SHA — a supply-chain hardening issue in freshly-added infrastructure, not the application. Fixed by pinning to the commit.
- 1 remains genuinely unresolved and is stated as such, not dismissed:
Semgrep flags a known nginx configuration pattern
(
deploy/nginx.conf) associated with H2C request smuggling in some backend configurations. Whether it's exploitable against this specific Rust/axum/hyper backend hasn't been independently confirmed either way — said honestly rather than asserted safe on a guess.
Rerun it yourself:
pip install semgrep && semgrep --config auto . - The large majority were false positives specific to this codebase, each
verified by
reading the flagged code, not by assuming the tool is wrong. "Insecure
HTTP" hits are local
Neither of those is "audited and clean." Both are "here is exactly what was checked, when, by what, and what it found" — which is the only claim this document is actually trying to make.
- 🔐 End-to-end encryption — messages, DMs, attachments, and channel keys encrypted client-side; the server stores ciphertext and wrapped key material only. See docs/E2EE.md.
- 🔑 Secure authentication — SRP-6a (RFC 5054), so your password never crosses the network, not even as a hash.
- 🗝️ Recoverable key custody — a random per-account seed wrapped under your password (Argon2id) and independently under a 12-word recovery code, so a password reset keeps your message history instead of destroying it. The code is shown once, at sign-up — save it; docs/LOST_RECOVERY_CODE.md is the honest account of what happens without it. See docs/E2EE_RECOVERY.md.
- 💬 Real-time messaging — WebSocket-based delivery, replies, reactions, edits, search. Search runs on your own device over messages it has already decrypted, so it covers the conversation you have open; the server cannot search ciphertext on your behalf.
- 🎙️ Voice & video — WebRTC voice channels with native noise suppression; frames encrypted end-to-end over Insertable Streams on both the mesh and the opt-in SFU (LiveKit) path. Insertable Streams is Chromium-only, so Firefox, Safari and iOS cannot encrypt call media frame-by-frame. They are not quietly downgraded: "Require encryption for calls" defaults on, so the app says so before you join and blocks the media instead.
- 📺 Screen sharing — including giving a trusted friend control of your shared screen, with an explicit per-request consent prompt.
- 🗒️ Púca Notes — a Google-Keep-style notes app at
/notes/on the web app, over the same end-to-end encrypted task lists and checklist channels: a grid of notes, search, reminders, colours and labels, with Púca's own task tree inside every note. See docs/NOTES.md. - 🖥️ My Devices — remote-desktop access to machines you own, gated by a device-key trust chain the server cannot forge into. (Full build only — the Lite build compiles all of this out; see "Getting the app" above.)
- 👥 Servers & channels — communities with text, voice, and collection channels, categories, and a public-server discovery list.
- 🎭 Roles & permissions — granular, per-server role-based access control with a permission-overwrite system per channel.
- 📱 Desktop, mobile and browser — a Windows desktop app (Tauri; Full and Lite builds), an Android app (Capacitor), and the same app in a browser, from one codebase — with one exception, encrypted call media, noted under Voice & video above. macOS, Linux and iOS builds exist in the tree but are not released or tested. The interface is English-only for now.
- 🔍 No telemetry — nothing reports to anyone but the server you joined; docs/PRIVACY.md lists what that server can see and the few third parties the apps ever contact.
- Backend: Rust + Axum + SQLx + PostgreSQL
- Frontend: React + TypeScript + Vite
- Desktop: Tauri
- Mobile: Capacitor
- Real-time: WebSockets + WebRTC (mesh) / LiveKit (opt-in SFU)
- Crypto:
@noble/curves,@noble/hashes(Argon2id and HKDF live here, in the frontend, because that is where key derivation happens), WebCrypto (frontend);x25519-dalek,ed25519-dalek(backend/native). Every primitive that protects message content comes from one of those libraries — none is home-rolled. The one hand-written protocol is SRP-6a infrontend/src/api/auth.ts, used for password authentication, not for encrypting anything; itsmodPowis not constant-time. That is called out here rather than left for you to find, and in more detail indocs/SECURITY_MODEL.md.
This gets a working instance running on your own machine to try — it is
not a production deployment guide. For actually putting this on a server
with a real domain and TLS, stop after this section and go to
deploy/README.md, which has the complete, current path
(reverse proxy, systemd service, TURN, the works) — this section is
deliberately not that, because explaining every option on day one is the
fastest way to make someone give up before they see it running.
- Rust (stable)
- Node.js (20+)
- PostgreSQL (16+), or just Docker for the step below
git clone https://github.com/Fossferous/Puca.git
cd Puca
docker compose up -d postgres # or point DATABASE_URL at your own Postgrescp .env.example .env
# edit .env: at minimum, JWT_SECRET (openssl rand -hex 32) and DATABASE_URL
cargo run --releaseMigrations run automatically on startup. The backend is now listening on
http://localhost:3000 — curl http://localhost:3000/ should answer
Puca Backend Online.
cd frontend
npm install
npm run devOpen http://localhost:5173, register an account, and send yourself a
message. That's the whole loop.
From here: building a desktop installer needs a build-time VITE_API_URL
pointed at your server (see deploy/README.md §6); a
real deployment needs the reverse-proxy and systemd steps in the same guide.
Neither is complicated, but neither belongs in a first five minutes either.
puca/
├── src/ # Rust backend
│ ├── main.rs # Routes & server startup
│ ├── handlers.rs # Auth endpoints
│ ├── server_handlers.rs # Server/channel/message APIs
│ ├── ws.rs # WebSocket handling
│ └── ...
├── crates/ # Native support crates (remote-desktop capture/
│ # input/encode, the unattended-access signing
│ # crate, the LAN wake helper)
├── frontend/
│ ├── src/
│ │ ├── api/ # API client + all E2EE primitives
│ │ ├── components/ # React components
│ │ └── App.tsx
│ ├── src-tauri/ # Desktop shell
│ └── android/ # Native Android plugins (Capacitor)
├── migrations/ # PostgreSQL schema
├── deploy/ # The actual, tested deployment path
└── docs/ # Design docs, including the honest ones
| Method | Endpoint | Description |
|---|---|---|
| POST | /auth/register |
Create account |
| POST | /auth/login/step1 |
SRP step 1 |
| POST | /auth/login/step2 |
SRP step 2 (get JWT) |
| GET | /servers |
List user's servers |
| POST | /servers |
Create server |
| GET | /channels/:id/messages |
Get messages |
| POST | /channels/:id/messages |
Send message |
| WS | /ws |
WebSocket connection |
See src/main.rs for the full route list.
This is the short version. The long version is
docs/SECURITY_MODEL.md — read it
before trusting either version.
- What it does not protect you from, before anything reassuring: the
operator sees all metadata; forward secrecy is per session, not per
message, and the recovery code opens all history by design; the browser
client runs whatever the server serves; nothing is post-quantum; and no
outside human has audited the code. The list, with the reasoning, is
docs/SECURITY_MODEL.md§1 "Known limits". - Authentication: SRP-6a, 2048-bit group (RFC 5054), SHA-256 proofs, with the verifier derived by Argon2id at the same cost as the key wrap since 0.9.3 — earlier accounts move across on their next sign-in. Your password never leaves your device, not even as a hash.
- Direct messages: since 0.9.3, sealed under per-message keys held only
by your devices and your recovery code — a cracked password reads none of
them. New accounts have this from registration; accounts created earlier
switch over once both people generate a new recovery code in Settings → My
Account (not per-message forward secrecy;
docs/SECURITY_MODEL.md§7 says exactly what it is and is not). - Identity & E2EE keys: a random per-account seed, wrapped under your password with Argon2id (m=19456 KiB, t=2, p=1) and independently under a 12-word recovery code — so a password reset recovers your history instead of destroying it. (An older, password-derived key scheme still opens accounts created under it and migrates transparently on next login; new accounts get the seed-based scheme from registration. See docs/E2EE.md.)
- Message encryption: X25519 ECDH for DMs; a per-channel symmetric key, wrapped to each member's identity key and rotated by epoch — including automatically on membership change (a removed member's wrapped keys are deleted and the next send rotates to a fresh epoch they were never wrapped into). AES-256-GCM throughout, HKDF-SHA256 for key derivation.
- What the server can never see: your password; decrypted message, attachment, or task content; identity private keys or channel keys.
- What the server does see, unavoidably: who talks to whom and when, message sizes and timing, and (for the honest limit on that) exactly what §2 of the security model says it does.
- Transport: TLS terminated at your reverse proxy (Caddy/nginx — the
backend process itself never speaks TLS by design, see
deploy/README.md); WebRTC media over DTLS-SRTP, with an additional end-to-end AEAD layer over Insertable Streams so the server and any TURN relay hold only ciphertext they cannot decrypt. - Session tokens: JWT (HS256), with a server-side
token_versioncheck so logout/password-change/recovery can revoke outstanding tokens.
Before deploying to production, at minimum: generate a real
JWT_SECRET, use a strong database password, terminate TLS with a real
certificate, set CORS_ORIGINS explicitly, and read
deploy/README.md end to end — it is shorter than this
sentence made it sound.
Found a vulnerability? See SECURITY.md for private
disclosure. Please don't open a public issue for anything exploitable.
See CONTRIBUTING.md. The short version: run the gates
locally before opening a PR, and sign the CLA when the bot asks (once per
GitHub account, not once per PR).
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing) - Commit your changes
- Push and open a Pull Request
AGPL-3.0-or-later — see LICENSE. Free for everyone,
individuals and companies alike, under the AGPL's conditions — the important
one being that a modified Púca offered over a network must offer its users
the corresponding source.
Commercial licenses are available for uses that don't fit the AGPL —
closed-source products, hosted services that keep their modifications
private, AGPL-incompatible codebases, or contractual support. See
COMMERCIAL-LICENSE.md.
Contributions are accepted under the terms in CLA.md, which
grants the maintainer the right to relicense the project — including under
the commercial terms above — without affecting your own rights to your own
contribution. Read it before your first PR — it's short.
The AGPL covers the code and deliberately grants no rights in the project's
name or logo; TRADEMARK.md says what you may do without
asking, which is nearly everything except shipping a modified build called
Púca. NOTICE carries the copyright statement and what the AGPL's
section 13 entitles a user of somebody else's server to.
By taking part you agree to the CODE_OF_CONDUCT.md.
