Two parties can discover if they mutually selected each other without revealing:
- Who the person selected (if not mutual)
- Who rejected whom
- That you even participated
Use cases: Dating, co-founder matching, hackathon team building, roommate search, mentor pairing, any N-to-N selection where privacy matters.
The key insight: Diffie-Hellman produces the same shared secret from either side.
Alice wants Bob:
shared = DH(alice_private, bob_public)
token = H(shared || pool_id || "match")
Bob wants Alice:
shared = DH(bob_private, alice_public) // Same shared secret!
token = H(shared || pool_id || "match") // Same token!
If both submit, the token appears twice → match detected. If only one submits, token appears once → no information leaked.
- Privacy-preserving matching - Only mutual matches are revealed
- Reveal on match - Encrypted contact info decryptable only by mutual matches
- Privacy delay - Random 30s-3min delay before match computation prevents timing analysis
- Decoy tokens - Hides your true selection count from the server
- Response padding - All API responses padded to 8KB blocks to prevent size analysis
- Ephemeral mode - Auto-delete participant profiles after pool closes
- Owner-held key PSI - Pool owners can use PSI to process match queries without learning joiner preferences
- WASM-based - Uses @openmined/psi.js for client-side intersection computation
- Cardinality-only mode - Option to reveal only match count, not identities
- Cross-instance pools - Discover and join pools across federated Rendezvous servers
- Peer-to-peer sync - Rendezvous instances connect directly via WebSocket (default port 3001)
- Automerge CRDTs - Pool metadata synced using conflict-free replicated data types
- Anonymous messaging - All federation messages use unlinkable Freebird tokens
- Timing noise - Random delays on federation messages to frustrate traffic analysis
- Freebird integration - Unlinkable eligibility proofs for pool creation and joining
- Witness integration - Timestamp attestation for match results
- Invite-gated pools - Require valid invite codes to join
- Owner signatures - Pool actions verified via Ed25519 signatures
- QR code invites - Share pools via scannable QR codes
- Multi-device sync - Transfer keys between devices via encrypted QR
- PWA support - Install as a mobile app, works offline
npm install
npm run buildRequires Node.js 20+.
# Seed the database with sample pools
npm run seed
# Start the web server
npm run server
# Open http://localhost:3000The seed script creates demo pools with participants who have pre-selected the test user. Use the keypair printed by the seed script to get guaranteed matches.
Environment variables:
| Variable | Description | Default |
|---|---|---|
PORT |
HTTP server port | 3000 |
RENDEZVOUS_DATA_DIR |
Database directory | ./data |
FREEBIRD_VERIFIER_URL |
Freebird verifier for invite codes | (disabled) |
WITNESS_GATEWAY_URL |
Witness gateway for timestamps | (disabled) |
FEDERATION_ENABLED |
Enable federation | false |
FEDERATION_PORT |
WebSocket port for federation | 3001 |
FEDERATION_PEERS |
Comma-separated peer endpoints | (none) |
FREEBIRD_ISSUER_URL |
Required for federation auth | (none) |
When FREEBIRD_VERIFIER_URL is set, pool creation requires a valid invite code. When unset, pool creation is open (development mode).
The web interface provides a complete flow:
- Pools - Browse and create matching pools
- Join - Register, browse participants, and make selections
- Discover - Find your mutual matches after pool closes
- Keys - Generate keypairs, manage identities, sync across devices
When confirming your selections, you can add contact info and a message. This data is encrypted using the match token as the key—only someone who mutually selected you can decrypt it. The server stores only encrypted blobs.
import {
createRendezvous,
generateKeypair,
deriveMatchTokens,
deriveNullifier
} from 'rendezvous';
// Create instance with optional adapters
const rv = createRendezvous({
dbPath: './data/rendezvous.db',
// freebird: new HttpFreebirdAdapter({ verifierUrl: '...' }),
// witness: new HttpWitnessAdapter({ gatewayUrl: '...' }),
});
// Create a pool
const pool = rv.createPool({
name: 'Team Matching',
creatorPublicKey: creatorKey,
creatorSigningKey: signingKey,
revealDeadline: new Date(Date.now() + 24 * 3600000),
ephemeral: true,
});
// Generate your keypair
const me = generateKeypair();
// Submit preferences
const theirKeys = ['abc...', 'def...'];
const tokens = deriveMatchTokens(me.privateKey, theirKeys, pool.id);
const nullifier = deriveNullifier(me.privateKey, pool.id);
rv.submitPreferences({
poolId: pool.id,
matchTokens: tokens,
nullifier,
revealData: [
{ matchToken: tokens[0], encryptedReveal: '...' },
],
});
// After pool closes, detect matches (async for witness attestation)
rv.closePool(pool.id);
const result = await rv.detectMatches(pool.id);
// Discover your matches locally
const myMatches = rv.discoverMyMatches(pool.id, me.privateKey, theirKeys);
for (const match of myMatches) {
console.log(`Matched with: ${match.matchedPublicKey}`);
}- Pool Creation: Operator creates pool with eligibility rules and deadline
- Commit Phase (optional): Participants submit H(tokens) to prevent timing attacks
- Reveal Phase: Participants submit actual tokens (+ optional encrypted contact info)
- Privacy Delay: Random 30s-3min delay before match computation
- Detection: Count token occurrences. Duplicates = matches.
- Discovery: Each participant locally checks which of their tokens matched
When submitting preferences, clients add random decoy tokens. This hides your true selection count from the server.
After a pool closes, match computation is delayed by a random 30s-3min interval. This prevents timing analysis that could correlate submission times with results.
All API responses are padded to 8KB block boundaries. This prevents attackers from inferring information based on response sizes.
Generate a fresh keypair for each pool. This prevents correlation of your identity across pools—even if someone is in multiple pools with you, they can't link your profiles.
Pool creators can enable ephemeral mode, which deletes all participant profiles after match detection. Only anonymous match tokens remain.
- Fishing attacks: Limited by
maxPreferencesPerParticipant - Timing attacks: Prevented by commit-reveal phases and privacy delay
- Sybil attacks: Freebird nullifiers ensure one submission per identity
- Eligibility gates: Freebird tokens, invite lists, composite rules
src/
├── rendezvous/
│ ├── types.ts # Core type definitions
│ ├── crypto.ts # DH tokens, encryption, signatures
│ ├── storage.ts # SQLite persistence
│ ├── pool.ts # Pool management
│ ├── submission.ts # Preference submission
│ ├── detection.ts # Match detection
│ ├── gates/ # Eligibility gates
│ ├── adapters/ # Freebird & Witness HTTP clients
│ └── index.ts # Public API
├── psi/
│ ├── types.ts # PSI type definitions
│ └── service.ts # PSI operations (@openmined/psi.js)
├── federation/
│ ├── types.ts # Federation message types
│ ├── manager.ts # CRDT sync & peer management
│ └── freebird-client.ts # Anonymous auth tokens
├── server/
│ └── index.ts # REST API & WebSocket server
├── scripts/
│ └── seed.ts # Demo data seeder
├── cli/
│ └── index.ts # CLI commands
└── index.ts # Main entry point
public/
├── index.html # Web UI
├── js/modules/ # Modular frontend components
├── css/ # Stylesheets
├── sw.js # Service worker for PWA
└── manifest.json # PWA manifest
npm testGET /api/pools- List poolsPOST /api/pools- Create pool (requires invite if Freebird configured)GET /api/pools/:id- Get pool detailsPOST /api/pools/:id/close- Close pool (owner-only, signed)
POST /api/pools/:id/participants- Register in poolGET /api/pools/:id/participants- List participants
POST /api/pools/:id/submit- Submit match tokensPOST /api/pools/:id/reveal- Reveal committed preferences
POST /api/pools/:id/psi/owner-setup- Owner creates PSI setupPOST /api/pools/:id/psi/request- Client submits PSI requestGET /api/pools/:id/psi/pending- Owner polls pending requestsPOST /api/pools/:id/psi/responses- Owner submits responsesGET /api/psi/response/:requestId- Client polls for result
POST /api/psi/create-request- Create PSI request from inputsPOST /api/psi/compute-intersection- Compute intersection locallyPOST /api/psi/compute-cardinality- Compute match count only (more private)
GET /api/federation- Federation statusGET /api/federation/pools- List federated poolsPOST /api/federation/announce/:poolId- Announce pool to federationPOST /api/federation/join/:poolId- Join federated pool
Apache-2.0