Privacy-preserving gay social app. End-to-end encrypted messaging, client-side encrypted photos, location privacy via geohash k-anonymity.
- Frontend: SvelteKit (Svelte 5 runes), static adapter (PWA)
- Backend: Bun + Hono
- Database: SQLite via Drizzle ORM
- Crypto: Ed25519 signing, X25519 encryption, double ratchet DMs, nacl.secretbox media
- Identity: Dual keypairs (Ed25519 for signing/DID + X25519 for encryption), stored in IndexedDB
git clone https://github.com/calsbot/proximity.git
cd proximity
# Install dependencies
cd server && bun install && cd ..
cd frontend && npm install && cd ..Two processes — server and frontend:
# Terminal 1: backend (port 3000)
cd server
bun run --watch src/index.ts
# Terminal 2: frontend (port 5173)
cd frontend
npm run dev -- --hostOpen http://localhost:5173
The --host flag is required for preview tools and mobile testing on LAN.
Vite proxies all API routes (/auth, /profiles, /messages, /groups, /moderation, /invitations, /media, /push, /newsletter, /ws) to http://localhost:3000 automatically via vite.config.ts. No VITE_API_URL needed for local dev.
All env vars have sensible defaults for local development. No .env file required to get started.
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Server port |
VAPID_PUBLIC_KEY |
built-in dev key | Push notification public key |
VAPID_PRIVATE_KEY |
built-in dev key | Push notification private key |
VAPID_SUBJECT |
mailto:admin@meetmarket.io |
Push notification contact |
LISTMONK_API_URL |
http://localhost:9000 |
Newsletter API (optional) |
LISTMONK_ADMIN_USER |
admin |
Listmonk API user (optional) |
LISTMONK_ADMIN_PASSWORD |
admin |
Listmonk API password (optional) |
Frontend uses VITE_API_URL (defaults to empty = same origin via Vite proxy) and VITE_WS_URL (defaults to auto-detect from window.location).
SQLite file lives at server/data/proximity.db. Created automatically on first run.
cd server
# Generate migrations after schema changes
bun run db:generate
# Apply migrations
bun run db:migrate
# Visual DB browser
bun run db:studioThe server/data/ directory and server/drizzle/ (migration files) are gitignored.
# Seed 500 test profiles across European cities
bun run seed-profiles.ts
# Seed 9 demo profiles in Berlin with avatars + groups + backup export
bun run scripts/seed-demo-profiles.ts/frontend SvelteKit app
/src/lib/crypto Identity, messaging, media, sealed sender encryption
/src/lib/services Chat orchestration, WebSocket client, notifications
/src/lib/stores Svelte stores (identity, conversations, location)
/src/routes Pages (grid, chat, profile, setup, settings)
/static PWA manifest, service worker, icons
vite.config.ts Dev proxy config (routes API calls to backend)
svelte.config.js Static adapter, SPA fallback
/server Bun + Hono API
/src/index.ts Server entry, WebSocket upgrade, browser session management
/src/db/schema.ts Drizzle ORM schema (profiles, messages, groups, blocks, reports, media)
/src/routes API route handlers
/data SQLite database (gitignored, created on first run)
/drizzle Generated migrations (gitignored)
/test-e2e.ts End-to-end test suite (47 tests)
/seed-profiles.ts Test profile seeder (500 European profiles)
/scripts/ Demo setup scripts
| Path | Description |
|---|---|
/auth |
Register, challenge-response verify |
/profiles/discover |
Find nearby profiles (POST, geohash cells) |
/profiles/:did |
Get/update profile |
/messages |
Send (POST), poll (GET with since timestamp) |
/messages/sealed |
Sealed sender relay (server can't see sender) |
/invitations/dm |
DM invitations — send, accept, decline, block |
/groups |
CRUD, invite, join requests, admin transfer |
/media |
Encrypted blob upload/download |
/moderation |
Block, unblock, report |
/location/ip |
IP-based approximate location (Cloudflare headers) |
/newsletter/subscribe |
Listmonk newsletter signup |
Identity: Each user generates Ed25519 (signing) + X25519 (encryption) keypairs on device. Identity is a did:key derived from the Ed25519 public key. Never leaves the device unless exported as a backup.
Messaging: DMs use X25519 Diffie-Hellman key agreement + a chain key ratchet (nacl.secretbox). Every message generates a fresh key. Group messages use sender keys (nacl.secretbox) distributed to members.
Sealed sender: After initial key exchange, messages are relayed through the server without the sender's identity attached. The server only sees the recipient's delivery token.
Location privacy: The client generates the user's geohash locally, mixes it with decoy geohash cells (k-anonymity), and sends the mixed set to the server. The server processes all cells and cannot distinguish which one is real.
Media: Photos are encrypted on-device with nacl.secretbox before upload. The server stores opaque blobs it cannot decrypt. Recipients receive the key out-of-band via the encrypted message channel.
cd frontend
npm run build
# Output: frontend/build/ — deploy as static files behind a reverse proxyProduction runs on a VM behind Cloudflare Tunnel + Caddy:
- Caddy (port 8080): reverse proxies API routes to Bun, serves static frontend
- Cloudflare Tunnel: terminates TLS, routes
meetmarket.ioto Caddy - Listmonk (port 9000, Docker): newsletter service at
mail.meetmarket.io
# Deploy frontend (zero downtime — just static file swap)
scp -r frontend/build/* user@host:~/proximity/frontend/build/
# Deploy server (2-3s restart, WebSocket auto-reconnects)
scp -r server/src/* user@host:~/proximity/server/src/
ssh user@host "kill \$(pgrep -f 'bun run src/index.ts') && sleep 2 && \
cd ~/proximity/server && nohup bun run src/index.ts > /tmp/server.log 2>&1 &"Messages and data are persisted in SQLite on disk — nothing is lost during server restarts.
bun run test-e2e.tsCovers auth, profiles, messaging (DM + group), sealed sender, encryption, invitations, moderation, media, and key ratcheting.