Real-time one-to-one messaging with client-side encryption, built on Nuxt 3, Supabase, and a Socket.IO relay.
chattr is a private chat app: register with email and password, search for another user, and exchange messages that are encrypted in the browser before they ever reach the network. Presence, typing indicators, and delivery/read receipts arrive in real time over an authenticated WebSocket. A bold industrial-brutalist interface (shared with PulseLog) keeps it sharp and high-contrast.
- Real-time 1:1 chat -- messages, presence, typing, and delivery/read receipts over Socket.IO
- Client-side encryption -- RSA-OAEP per message, dual-encrypted so both sender and recipient can read their own copy
- Authenticated sockets -- every WebSocket connection is verified against the sender's Supabase access token; identity is never taken from the client
- Row-level security -- Postgres RLS scopes every row to its owner; public profile lookups go through a safe, column-limited view
- Email/password auth -- Supabase Auth with PKCE, email confirmation, and session refresh
- Industrial-brutalist UI -- Oswald + JetBrains Mono, flat surfaces, 2px borders, hard offset shadows, one orange accent
A small monorepo with three pieces:
chattr/
├── nuxt-app/ Nuxt 3 frontend + Nitro SSR (port 3001)
│ @nuxtjs/supabase, Tailwind, the chat UI & crypto
├── server/ Standalone Socket.IO relay (port 3002)
│ Pure message relay — no database, verifies Supabase JWTs
└── database-schema.sql Supabase Postgres schema: tables, RLS policies, RPCs
- nuxt-app renders the UI, talks to Supabase for auth and persistence, and does all encryption/decryption in the browser.
- server relays real-time events between online users. It holds no data and trusts no client-supplied identity — on connect it calls
supabase.auth.getUser(token)and pins the socket to the verified user id. - Supabase provides authentication and a Postgres database. Messages and profiles live here, protected by row-level security.
Messages take two paths: they are persisted (encrypted) to Supabase for history, and relayed live through the Socket.IO server for instant delivery.
- Encryption is client-side. Each user gets an RSA keypair generated in the browser. Messages are encrypted for the recipient (and a second copy for the sender) before being stored or sent.
- Keys are password-wrapped, not zero-knowledge. The private key is encrypted with an AES-GCM key derived from the user's password (PBKDF2, 600k iterations) and stored in Supabase. This is convenient key escrow — anyone who obtains the database still faces an offline password attack, but it is not end-to-end zero-knowledge encryption. Choose a strong password.
- Sockets are authenticated. Connections without a valid Supabase token are rejected; the server derives identity from the token, so a client cannot impersonate another user or forge receipts.
- RLS least privilege. Users can read only their own row; other users' public fields come from a restricted
public_profilesview. Messages can be inserted by their sender and updated only on thedelivered/readflags.
| Tool | Version |
|---|---|
| Node.js | >= 20 |
| npm | >= 10 |
| Supabase project | free tier is fine |
Create a Supabase project, then run database-schema.sql in the Supabase SQL Editor to create the tables, policies, and functions.
Two env files are needed — one per process. Copy the examples (or run ./scripts/setup-env.sh) and fill in your Supabase values:
cp nuxt-app/.env.example nuxt-app/.env # Nuxt app
cp server/.env.example server/.env # socket relayBoth need SUPABASE_URL and SUPABASE_ANON_KEY — the app uses them for auth and data, and the relay uses them to verify each connection's token.
npm run install-all # installs nuxt-app/ and server/ deps
npm run dev # runs the Nuxt app (3001) and socket server (3002) togetherOpen http://localhost:3001, register two accounts (two browser profiles), and start chatting.
Build the Nuxt app and run the two processes behind a reverse proxy that routes /socket.io/* to the relay and everything else to the Nuxt server:
# build (off the server if memory is tight)
cd nuxt-app && npm run build # produces .output/
# run
node nuxt-app/.output/server/index.mjs # web (PORT=3001)
node server/index.js # socket relay (PORT=3002)Both processes need the Supabase env vars. On the free Supabase tier, hit a lightweight endpoint daily to keep the project from pausing.
| Layer | Technology |
|---|---|
| Frontend / SSR | Nuxt 3, Vue 3, TypeScript, Tailwind CSS |
| Auth & data | Supabase (Auth, Postgres, Row-Level Security) |
| Real-time | Socket.IO (Express relay, helmet + rate limiting) |
| Crypto | Web Crypto API (RSA-OAEP messages, PBKDF2 + AES-GCM key wrapping) |
This project is licensed under the MIT License. See the LICENSE file for details.