Status: Draft / implementation specification
Reference implementation: Nosflare fork, TypeScript, Cloudflare Workers, Durable Objects, D1, R2
Protocol working name: OpenDating
Target: Open-source dating protocol built on Nostr
Initial deployment model: One Cloudflare deployment providing multiple logically independent OpenDating service roles
Long-term deployment model: Multiple interchangeable relays, discovery providers, matchmakers, verification providers, moderation providers, and clients implementing the same protocol
OpenDating is an open dating protocol built on top of Nostr.
The objective is not to create “Tinder running on a Nostr relay.”
The objective is to define an interoperable dating layer in which:
- users own their identity;
- clients are replaceable;
- relay operators are replaceable;
- matching providers are replaceable;
- discovery algorithms are replaceable;
- verification providers are replaceable;
- moderation providers can be trusted selectively;
- private dating activity is not broadcast publicly;
- basic dating and safety functionality does not depend on payment;
- another developer can build a completely different dating client without permission;
- another operator can deploy an independent compatible backend without copying implementation-specific Cloudflare architecture.
The initial project will convert Nosflare into the reference OpenDating relay/backend, while deliberately keeping the OpenDating protocol independent from Nosflare and Cloudflare.
Nosflare already supplies a strong Cloudflare-native foundation: a TypeScript Nostr relay, D1 persistence, multi-region Durable Objects for WebSockets, WebSocket Hibernation, NIP-42 authentication, rate limiting and existing Nostr event processing.
The resulting system should feel operationally like a modern centralized dating app while preserving the ability for the ecosystem itself to decentralize.
The north star is:
A dating network no single company owns, without sacrificing the privacy and safety controls that dating requires.
A Nostr event being signed MUST NOT imply that the event is public.
Traditional Nostr applications frequently use relays as openly queryable event stores. Dating cannot safely use that model for all data.
OpenDating therefore distinguishes between:
- public Nostr information;
- candidate-visible dating information;
- service-private dating information;
- peer-to-peer private information;
- highly restricted moderation information.
The generic Nostr relay is the transport layer.
OpenDating services consume and produce Nostr events over that transport.
Initially those services run inside one Cloudflare deployment:
┌─────────────────────────────────────────────┐
│ Mobile Client │
│ │
│ identity · onboarding · discovery · chat │
└─────────────────────┬───────────────────────┘
│
│ Nostr WebSocket
│ NIP-42 / EVENT / REQ
▼
┌─────────────────────────────────────────────┐
│ OpenDating Reference Relay │
│ │
│ Generic Nostr Layer │
│ │ │
│ ┌────────────┴─────────────┐ │
│ │ OpenDating Event Router │ │
│ └────────────┬─────────────┘ │
│ │ │
│ ┌────────────────┼─────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Profile Discovery Matcher│
│ Service Service Service│
│ │
│ ▼ ▼ ▼ │
│ Safety Verification Media│
│ Service Service Service│
│ │
│ D1 + R2 │
└─────────────────────────────────────────────┘
Later, without changing the wire protocol:
Nostr
│
┌────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
Generic Relay Discovery Relay DM Relay
▲ ▲ ▲
│ │ │
└──────────┬─────┴─────┬───────────┘
│ │
▼ ▼
Matcher A Matcher B
│
┌───────┼─────────┐
▼ ▼ ▼
Verifier Safety Other Clients
The logical architecture MUST support the second diagram even while V0.1 physically deploys the first.
OpenDating MUST:
- use ordinary Nostr transport wherever practical;
- reuse existing NIPs instead of replacing them;
- avoid custom WebSocket message types unless absolutely unavoidable;
- keep Cloudflare implementation details outside the protocol;
- support multiple independent implementations;
- permit users to use multiple providers simultaneously;
- permit clients to change providers;
- explicitly version every OpenDating schema;
- provide conformance tests;
- document privacy and security semantics, not merely JSON schemas;
- eventually be implementable from the specification without reading the reference source code.
A user must be able to:
- create or import a Nostr identity;
- create a dating profile;
- configure who they want to discover;
- control who can discover them;
- discover geographically relevant candidates;
- like or pass on candidates privately;
- mutually match;
- exchange encrypted messages;
- unmatch;
- block;
- report;
- pause discovery;
- delete their OpenDating profile;
- request deletion from supporting relays;
- move to another compatible client without creating an entirely new identity.
The reference implementation MUST be designed around a hostile dating threat model.
Safety defaults should specifically account for forms of harassment, stalking, unsolicited sexual content and coercive behavior that disproportionately affect women and other higher-risk users, while making the protections available to every user.
Safety functionality MUST NOT be paywalled.
At minimum:
- no unsolicited DMs;
- no public likes;
- no public block graph;
- no public reports;
- no exact location exposure;
- no arbitrary profile enumeration;
- immediate blocking;
- immediate unmatching;
- report-after-unmatch;
- moderator tooling;
- anti-spam and anti-Sybil controls;
- configurable verification requirements;
- account-level moderation restrictions;
- encrypted evidence handling;
- moderator audit logs.
Modern dating products increasingly expose verification, message-safety prompts, explicit-image protection, date sharing, blocking, reporting and unmatching as core safety tools. These should define the competitive safety benchmark even where some are post-MVP.
V0.1 will NOT attempt to provide:
- fully trustless matchmaking;
- private-set-intersection cryptography;
- zero-knowledge age verification;
- DAO moderation;
- cryptocurrency monetization;
- premium boosts;
- pay-to-message;
- voice or video calling;
- AI romantic compatibility;
- global reputation scores;
- biometric verification operated directly by the reference relay;
- government ID document storage;
- public dating profiles on arbitrary Nostr relays;
- exact real-time location;
- decentralized legal enforcement;
- guaranteed deletion from every Nostr relay or recipient device;
- prevention of screenshots or manual copying;
- a universal moderation policy every Nostr relay must follow.
Those can be considered later without compromising the V0.1 architecture.
The specification SHALL use:
- MUST / MUST NOT for interoperability, privacy or security requirements;
- SHOULD / SHOULD NOT when implementations may differ but a strong default exists;
- MAY for optional behavior.
Implementation-specific decisions should be labeled REFERENCE IMPLEMENTATION.
This distinction should be maintained from the first commit so the eventual protocol specification can be extracted cleanly.
OpenDating SHOULD build on existing standards rather than invent equivalent mechanisms.
Basic Nostr events, signatures, EVENT, REQ, CLOSE and relay behavior.
Relay information document.
NIP-11 explicitly allows clients to ignore extra fields they do not understand, which provides a clean place for the reference relay to advertise OpenDating capabilities during V0.x.
Relay authentication.
All private OpenDating operations MUST require NIP-42 authentication. NIP-42 specifically exists so relays can authenticate users before granting access to restricted resources.
Encryption primitives.
Do not invent custom encryption.
Gift wrapping.
NIP-59 can encapsulate any Nostr event while obscuring the real sender and most metadata from public observers. This makes it the preferred transport for OpenDating service requests such as profile updates, discovery requests, likes and reports.
Private DMs.
NIP-17 already defines NIP-44/NIP-59 encrypted direct messaging. Relays are specifically encouraged to use NIP-42 and return gift wraps only to the authenticated p-tagged recipient.
Private lists.
NIP-51 permits list items to be stored privately using NIP-44. OpenDating SHOULD use this where appropriate for portable private user lists such as mutes/blocks rather than inventing a public block graph.
Reports.
OpenDating reports SHOULD preserve NIP-56 semantics where possible.
Importantly, NIP-56 explicitly warns relays against automatically moderating users solely from raw report counts because those reports can be gamed. OpenDating MUST honor that constraint.
Request to Vanish.
Supporting relays SHOULD implement full OpenDating cleanup when receiving a valid NIP-62 request. NIP-62 instructs supporting relays to delete the user's events and gift wraps addressed to that user and prevent re-ingestion of deleted events.
Before permanent OpenDating event kinds exist, V0.x SHOULD use NIP-78's kind:30078 and kind:78 namespaces for experimental application-specific structures.
NIP-78 intentionally provides application-specific data kinds. It is appropriate for prototyping but explicitly does not itself establish interoperability, so permanent OpenDating kinds should only be proposed after schemas stabilize.
- NIP-85 — trust calculations/assertions;
- NIP-98 — Nostr-authenticated HTTP requests for private media;
- NIP-B7/Blossom — public or encrypted content-addressed media;
- NIP-13 — optional proof-of-work anti-spam;
- NIP-46 / NIP-55 — external signers;
- NIP-49 — encrypted Nostr key backup.
NIP-70 does not make an event private.
The ["-"] tag controls who is allowed to publish an event to a relay; it does not create read authorization. OpenDating MUST NOT use NIP-70 as a substitute for private storage or NIP-42 access control.
Every piece of information MUST belong to a privacy class.
| Class | Name | Example | Visibility |
|---|---|---|---|
| P0 | Public | relay capabilities, optional generic Nostr identity | anyone |
| P1 | Candidate Restricted | dating profile, profile photos, coarse distance, badges | authorized candidates only |
| P2 | Service Private | location cell, discovery preferences, likes, pending matches, blocks | selected OpenDating service |
| P3 | Peer Private | NIP-17 messages and encrypted attachments | conversation participants |
| P4 | Safety Restricted | reports, evidence, moderator notes | authorized safety personnel/services only |
No developer should add a new field without assigning it one of these classes.
The reference implementation MUST NOT publicly publish:
- exact coordinates;
- GPS history;
- street address;
- workplace location;
- home location;
- raw date of birth;
- government ID;
- verification selfie;
- biometric template;
- phone contacts;
- phone number unless explicitly user-shared;
- email unless explicitly user-shared;
- likes;
- passes;
- rejection history;
- block graph;
- report history;
- moderation evidence;
- safety notes;
- match history;
- exact last-active timestamps;
- discovery preferences;
- sexual-orientation preferences unless explicitly chosen for profile display;
- message plaintext;
- nsec/private keys.
OpenDating cannot stop:
- someone a user matched with from taking screenshots;
- an authorized candidate from manually copying a profile;
- a malicious recipient from publishing information they legitimately received;
- a malicious external relay from violating OpenDating recommendations;
- all metadata observation by the user's chosen relay/provider;
- users from lying about self-declared information;
- global copies of information that a user deliberately published publicly.
NIP-09 itself warns that deletion requests cannot guarantee deletion from every relay and client.
The product MUST never promise impossible global erasure.
The objective is instead to:
prevent casual public discovery, bulk scraping and protocol-level amplification of sensitive dating information.
The architecture MUST explicitly defend against the following actors.
Attempts:
- enumerate all dating profiles;
- collect photographs;
- map dating membership;
- enumerate gift wraps.
Mitigation:
- NIP-42;
- no public discovery index;
- no raw profile
REQ; - candidate batches only;
- short-lived media access;
- request quotas.
Creates a valid Nostr identity and attempts large-scale discovery.
Mitigation:
- discovery quotas;
- candidate batch limits;
- rate limits;
- trust tiers;
- randomized candidate ordering;
- candidate grants;
- coarse location;
- no exact candidate counts;
- abuse detection.
Attempts to locate a specific user or triangulate their location.
Mitigation:
- no exact coordinates;
- no exact distance;
- distance buckets;
- profile visibility restrictions;
- private blocks;
- verified-only discovery mode;
- origin-change rate limits;
- no search-by-name endpoint in MVP;
- no raw geographic query interface.
Obtains a legitimate match, then harasses or threatens the recipient.
Mitigation:
- instant unmatch;
- instant block;
- report after unmatch;
- cryptographic DM evidence;
- transport-level DM denial after block;
- safety escalation;
- account restrictions.
Creates thousands of Nostr keys.
Mitigation:
- NIP-42;
- connection/IP/device-level rate signals;
- optional PoW;
- account-age/social signals;
- verification;
- per-account quotas;
- per-network abuse limits;
- behavior-based restrictions.
Nostr keys prove control of a key, not humanity.
Attempts to mass-report an innocent user.
Mitigation:
- reports are evidence, not votes;
- raw count cannot automatically produce irreversible bans;
- reporter reputation may influence triage;
- evidence quality considered;
- manual review for serious permanent actions.
Attempts to learn user behavior.
Mitigation:
- NIP-59 for service requests;
- peer E2EE for DMs;
- no message plaintext;
- no exact coordinates;
- minimal logs;
- application-level encryption for highly sensitive DB fields;
- ability to choose other providers later.
OpenDating cannot hide all network metadata from the user's chosen provider.
Attacker obtains D1 contents.
Mitigation:
- no exact GPS;
- no nsecs;
- pseudonymous internal member IDs;
- encrypted sensitive JSON;
- separate encryption keys;
- minimized retention;
- private media in separate object storage.
Attempts to browse private information unnecessarily.
Mitigation:
- least-privilege RBAC;
- evidence only accessible within relevant cases;
- moderator audit logs;
- no arbitrary DM browsing;
- strong operator authentication;
- severe-action review policies.
Attempts to enter an 18+ network.
Mitigation:
- mandatory 18+ declaration;
- age-verification provider interface;
- suspected-underage reporting path;
- immediate restriction while high-confidence safety review is pending;
- no raw DOB stored by the relay.
Mitigation:
- photo/human verification interface;
- Nostr social history;
- NIP-05 as a weak identity signal only;
- signed verification assertions;
- impersonation reporting;
- scam reporting;
- account-risk controls.
OpenDating defines roles, not one mandatory server.
V0.1 roles:
relay
profile
discovery
matcher
dm-policy
moderation
verification
media
The reference implementation hosts all roles.
Each role MUST have an independent Nostr service keypair from day one.
Example:
OD_PROFILE_SERVICE_PUBKEY
OD_DISCOVERY_SERVICE_PUBKEY
OD_MATCHER_SERVICE_PUBKEY
OD_MODERATION_SERVICE_PUBKEY
OD_VERIFICATION_SERVICE_PUBKEY
OD_MEDIA_SERVICE_PUBKEY
Private keys MUST be Cloudflare Worker secrets.
They MUST NOT live in:
config.ts
wrangler.toml
git
D1
logs
Separate service identities are required even when one Worker controls all of them.
This allows:
V0.1
one Worker
├ profile key
├ discovery key
├ matcher key
├ moderation key
└ verification key
to become:
V1+
Profile Operator A
Discovery Operator B
Matcher Operator C
Moderation Operator D
Verifier E
without changing event semantics.
The reference relay SHOULD extend NIP-11 with an opendating field.
NIP-11 requires clients to ignore unknown additional fields, making this backwards compatible.
Example:
{
"name": "OpenDating Reference Relay",
"supported_nips": [1, 11, 17, 42, 44, 51, 56, 59, 62, 78],
"opendating": {
"protocol_versions": ["0.1"],
"roles": {
"profile": {
"pubkey": "<hex>",
"relay": "wss://dating.example.com"
},
"discovery": {
"pubkey": "<hex>",
"relay": "wss://dating.example.com"
},
"matcher": {
"pubkey": "<hex>",
"relay": "wss://dating.example.com"
},
"moderation": {
"pubkey": "<hex>",
"relay": "wss://dating.example.com"
}
},
"features": {
"match_only_dms": true,
"private_profiles": true,
"coarse_location": true,
"private_reports": true,
"vanish": true
}
}
}A Nostr-native service manifest SHOULD later be added so roles can be discovered without relying on the HTTP relay document.
V0.x MUST NOT prematurely reserve arbitrary permanent event kinds.
Use:
kind 30078
for addressable/state-like application documents.
Use:
kind 78
for experimental commands/responses where multiple events may exist.
Sensitive OpenDating events MUST normally exist inside NIP-59 envelopes, not as publicly queryable raw kind:78/30078 events.
All OpenDating application events MUST share a common inner schema:
{
"protocol": "opendating",
"version": "0.1",
"type": "discovery.query",
"request_id": "random-128-bit-id",
"created_at": 1786050000,
"payload": {}
}Required:
protocol
version
type
request_id
created_at
payload
Properties:
request_idMUST be random and unique;- services MUST support idempotency;
- duplicate requests MUST NOT create duplicate likes/matches/reports;
- unknown major protocol versions MUST be rejected;
- unknown optional fields SHOULD be ignored;
- clients MUST place sensitive values inside encrypted content rather than outer tags.
A normal private service interaction:
Alice
│
│ OpenDating rumor
│ NIP-44
│ NIP-59 seal + gift wrap
▼
Relay
│
│ outer p = Discovery Service
▼
Discovery Service
│
│ decrypt
│ authenticate semantics
│ process
▼
D1
Response:
Discovery Service
│
│ encrypted candidate result
│ NIP-59
▼
Relay
│
│ outer p = Alice
▼
Alice
The ordinary Nostr relay sends OK when the outer event is accepted.
That is only a transport acknowledgement.
Domain operations MUST return a separate OpenDating acknowledgement/result where appropriate.
At minimum:
profile.upsert
profile.pause
profile.resume
profile.delete
visibility.update
location.update
discovery.query
discovery.result
intent.like
intent.revoke
match.created
match.list
match.state
unmatch
block.add
block.remove
report.submit
report.received
verification.list
verification.result
account.delete
service.ack
service.error
Later:
media.consent
date.share
date.checkin
call.signal
account.migrate
provider.preference
Every OpenDating WebSocket session MUST support NIP-42.
OpenDating-specific operations MUST require successful NIP-42 authentication.
The reference implementation SHOULD:
- issue a random challenge per socket;
- bind challenge to the exact relay URL;
- expire unconsumed challenges after approximately 60 seconds;
- allow each challenge to be used only once;
- preserve authenticated session state through Durable Object hibernation;
- require reauthentication after reconnect;
- never persist the AUTH event as an ordinary relay event.
Nosflare already persists authenticated pubkeys/challenge state with the Durable Object WebSocket attachment mechanism.
Authentication answers:
“Which key controls this connection?”
Authorization answers:
“What is this key allowed to do?”
They MUST remain separate.
Reference member states:
onboarding
active
paused
limited
quarantined
suspended
banned
deleted
Authorization examples:
active:
profile ✓
discovery ✓
like ✓
DM matched users ✓
paused:
profile retained
discovery hidden
existing matches/chat optionally ✓
limited:
discovery ✓
outbound likes reduced/disabled
quarantined:
profile hidden
no new likes
no new matches
existing account access ✓
suspended:
no dating operations
banned:
writes denied
The mobile client MUST:
- generate or import a standard Nostr identity;
- never send nsec to OpenDating infrastructure;
- store locally generated secrets using OS secure credential storage;
- support export/backup;
- support external signers where practical.
The backend MUST only ever receive public keys and signed/encrypted material.
Loss of the Nostr private key can mean loss of identity and encrypted-message recovery.
That limitation MUST be disclosed.
MVP onboarding MUST collect:
18+ confirmation
display/first name
age
gender identity
who the user wants to see
relationship intent
bio
interests
2–6 profile photos
location permission or manually selected location
distance preference
age preference
Optional:
pronouns
orientation display
profile prompts
NIP-05
social links
Raw birthdate SHOULD remain device-local.
The profile service receives:
age: 27
not:
dob: 1999-03-14
unless a future verification provider requires DOB outside the relay.
Verified age should eventually come from a signed verification claim such as:
age_over_18 = true
rather than broadcasting DOB.
Candidate-visible profile:
{
"profile_version": 4,
"display_name": "Alice",
"age": 27,
"gender": ["woman"],
"pronouns": ["she/her"],
"relationship_intents": ["long_term"],
"bio": "...",
"prompts": [
{
"prompt_id": "perfect_sunday",
"answer": "..."
}
],
"interests": [
"running",
"coffee",
"travel"
],
"media": [
{
"media_id": "...",
"order": 0,
"type": "image"
}
],
"location_label": "Tampa Bay",
"verification_claims": [
"human_verified"
]
}The following MUST be stored separately and MUST NOT be included in the candidate-visible profile unless explicitly chosen:
exact search preferences
who-can-see-me settings
location cell
raw verification data
moderation state
blocks
likes
reports
internal trust signals
The user MUST have:
active
paused
MVP.
Later:
incognito
verified-only
matches-only
Reference safe defaults:
profile discoverable only through authorized candidate queries
no public profile endpoint
no search-by-name
no global member directory
no direct "fetch arbitrary profile by npub" unless viewer is authorized
Location is one of the highest-risk dating data types.
Exact GPS MUST NOT leave the device for normal discovery.
The mobile client should:
GPS
↓
coarse spatial cell
↓
send spatial cell
not:
GPS
↓
latitude / longitude
↓
server
Use:
scheme: geohash
maximum precision: 5
Example:
{
"scheme": "geohash",
"cell": "dhvqx",
"precision": 5
}The exact implementation MAY evolve, but the protocol MUST identify the spatial scheme and precision.
Clients MAY intentionally add stable short-term location fuzz before calculating the cell.
The reference service SHOULD:
- accept a new cell only when location materially changes;
- rate-limit rapid origin switching;
- distinguish normal travel from repeated manual triangulation attempts;
- never expose stored cell identifiers to another candidate;
- never return raw coordinates;
- never return exact distance;
- never expose location history.
Location history SHOULD NOT be retained.
Only the most recent location cell is required.
Return coarse values:
nearby
within 5 mi
5–10 mi
10–25 mi
25–50 mi
50+ mi
or an equivalent locale-aware unit.
Do NOT return:
2.13 miles away
This substantially reduces triangulation value while preserving useful dating UX.
Users MAY choose:
hide distance entirely
The service MUST implement defenses including:
- minimum useful search radius;
- rate-limited origin changes;
- coarse stored location;
- coarse returned distance;
- no exact sorting by distance;
- randomized ordering within similar score bands;
- no raw geographic profile queries;
- no arbitrary
#glocation enumeration; - candidate-query quotas;
- low-density privacy handling.
If very few users exist in a geographic cell, the service SHOULD broaden location presentation rather than revealing uniquely identifying proximity.
Discovery is a service, not a generic Nostr REQ.
The client sends:
{
"protocol": "opendating",
"version": "0.1",
"type": "discovery.query",
"request_id": "...",
"payload": {
"origin_cell": "dhvqx",
"radius_miles": 25,
"age_min": 24,
"age_max": 32,
"genders": ["woman"],
"relationship_intents": ["long_term"],
"limit": 20,
"exclude": ["..."]
}
}This entire event is encrypted to the discovery provider.
The generic relay MUST NOT translate these fields into publicly queryable Nostr tags.
Before scoring, every candidate MUST pass:
candidate is active
candidate is 18+
viewer is active
not same user
no block in either direction
no moderation exclusion
candidate allows viewer's category
viewer allows candidate's category
age preferences compatible
gender/orientation compatibility
relationship-intent compatibility where required
within coarse geographic policy
verification requirement satisfied
candidate has complete profile
Eligibility MUST happen before ranking.
A user should not merely specify:
“Who do I want to see?”
They should also be able to specify:
“Who am I willing to be shown to?”
This is particularly important for privacy and harassment reduction.
Examples:
Only show me to users 24–40
Only show me to verified users
Only show me to users within 30 miles
Only show me to compatible relationship intents
These restrictions are P2 service-private data.
A discovery response SHOULD contain no more than 20 candidates.
Example:
{
"type": "discovery.result",
"request_id": "...",
"payload": {
"candidates": [
{
"pubkey": "...",
"profile": {},
"distance_bucket": "5-10mi",
"candidate_grant": "...",
"service_hints": {
"matcher": [],
"dm_relays": []
}
}
],
"cursor": "opaque-value"
}
}The result itself is NIP-59 wrapped to the requesting user.
The reference discovery service SHOULD issue a short-lived candidate grant.
Purpose:
prove to a matcher that the sender was legitimately authorized to interact with this candidate.
This prevents attackers from obtaining a list of arbitrary pubkeys and bypassing discovery policy by directly constructing likes.
Conceptually:
viewer
target
discovery_provider
issued_at
expires_at
permission = like
V0.1 MAY use an opaque service token.
For cross-provider interoperability, the protocol SHOULD evolve toward a short-lived service-signed Nostr object that independent matchers can verify.
Expiration recommendation:
15–60 minutes
Ranking is an implementation policy and MUST NOT become a mandatory protocol algorithm.
Reference implementation SHOULD optimize for compatibility and healthy exposure rather than maximum swiping time.
Suggested score components:
relationship-intent compatibility 25
distance bucket 20
shared interests 15
profile completeness 10
recent activity bucket 10
verification/trust signals 10
exposure balancing 10
Then randomize candidates within narrow score bands.
Do NOT initially use:
- attractiveness rankings;
- hidden desirability/Elo ratings;
- wealth ranking;
- paid boosts;
- engagement-maximization loops.
The protocol should eventually permit alternate discovery algorithms.
D1 charges by rows read/written, so discovery MUST be index-driven. Current free D1 includes 5 million rows read/day, 100,000 rows written/day and 5 GB storage.
Do NOT store a database row for every impression.
Do NOT store a server-side row for every pass.
Instead:
Keep normal passes primarily client-local.
The encrypted discovery request can send a bounded recent exclusion set.
Optional portable pass history MAY later be stored as an encrypted compact list snapshot.
Likes MUST be persisted because matching requires them.
Maintain approximate aggregate exposure counts per profile rather than per-viewer impression logs.
This dramatically lowers write volume.
Likes are P2 service-private data.
A like MUST NEVER appear as a publicly queryable event.
Flow:
Alice
│
│ NIP-59 intent.like → Matcher
▼
Matcher
│
├ validate Alice
├ validate candidate grant
├ check block state
├ store A → B
│
└ check B → A
Payload:
{
"type": "intent.like",
"request_id": "...",
"payload": {
"intent_id": "...",
"target_pubkey": "...",
"candidate_grant": "...",
"expires_at": 1790000000
}
}Pending likes SHOULD expire.
Reference default:
90 days
A pass does not need to become a global Nostr event.
Reference implementation:
normal pass → local device state
Only explicit actions requiring cross-device persistence should be synchronized.
This minimizes:
- sensitive rejection history;
- D1 writes;
- provider knowledge;
- storage.
When:
A likes B
AND
B likes A
the matcher creates a match.
No party learns about a one-way like before reciprocity.
The matcher sends:
match.created → Alice
match.created → Bob
through separate NIP-59 gift wraps.
Match object:
{
"match_id": "...",
"participants": [
"<alice>",
"<bob>"
],
"created_at": 1786051000,
"matcher": "<matcher-pubkey>",
"dm_relays": [
"wss://..."
]
}For cross-provider deduplication, match IDs SHOULD eventually be deterministic using the reciprocal intent IDs.
Conceptually:
SHA256(
"opendating-match-v1"
+ sorted(intent_a_id, intent_b_id)
)
Multiple matchmakers observing the same reciprocal intents can therefore produce the same logical match.
V0.1 can implement this immediately.
V0.1 matchmakers are trusted to correctly attest:
both parties expressed interest.
Likes remain encrypted to the chosen matcher.
Fully trustless private-set intersection is explicitly outside MVP scope.
This trust must be transparent:
This match was attested by Matcher X.
Clients should eventually be able to choose multiple matchmakers.
Every candidate result MAY contain the target's preferred matcher services.
When Alice likes Bob:
send intent to:
Alice's selected matcher(s)
+
Bob's advertised matcher(s)
When Bob likes Alice, his client does the same.
This gives both providers the opportunity to observe both intents.
The client deduplicates resulting matches using deterministic match IDs.
This provides meaningful decentralization without requiring advanced cryptographic private-set-intersection in V1.
OpenDating MUST use NIP-17 for private messages.
NIP-17 uses NIP-44 and NIP-59 and defines:
kind 14 = text DM
kind 15 = file message
kind 1059 = gift wrap
MVP SHOULD support:
text messages only
File/media messages SHOULD be introduced after content-consent and explicit-image protections are implemented.
The reference OpenDating DM relay MUST enforce:
authenticated sender
+
recipient
+
active match
+
no block
=
accept gift wrap
If not:
restricted: od:not-matched
restricted: od:blocked
The relay does not need to decrypt the DM.
It has:
authenticated sender from NIP-42
recipient from outer p tag
and can therefore verify pair authorization without reading message contents.
This is one of the core OpenDating safety properties.
Gift wraps MUST only be returned to the authenticated p-tagged recipient.
NIP-17 and NIP-59 explicitly recommend this protection.
Generic:
REQ kinds:[1059]
MUST NOT reveal everyone else's wraps.
OpenDating MUST NOT solve moderation by routinely decrypting private messages.
Instead, reporting uses recipient-selected evidence disclosure.
When Alice reports Bob's message:
Alice locally decrypts message
│
▼
Alice chooses what evidence to submit
│
▼
evidence encrypted to Moderation Service
Where available, evidence SHOULD contain:
original kind-13 seal
decrypted rumor
message ID
surrounding context selected by reporter
media hash
reporter statement
NIP-17 requires clients to verify that the pubkey signing the seal matches the pubkey in the inner rumor. That same relationship allows a moderation service receiving voluntarily disclosed evidence to validate message authorship.
This provides:
E2E privacy during ordinary use
+
verifiable evidence after abuse
without mass message surveillance.
Moderation evidence SHOULD be classified:
cryptographic
media_hash
service_attested
screenshot
user_statement
A cryptographically verified sender-signed seal has greater evidentiary value than an unauthenticated screenshot.
This should inform moderator triage, not automatically decide guilt.
Either participant can unmatch at any time.
Flow:
Alice → matcher: unmatch
Immediate effects:
match state = unmatched
future DMs denied
match removed from normal active list
pending media permissions revoked
The other participant SHOULD receive a neutral state update such as:
"This match is no longer available."
Do not disclose whether the cause was:
unmatch
block
moderation
account deletion
unless policy specifically requires it.
A user MUST still be able to report a previous match after unmatching.
Blocking is stronger than unmatching.
A block MUST immediately:
- remove the target from discovery;
- prevent the blocker from being shown to the target by the blocker's discovery providers;
- revoke an active match;
- revoke pending likes;
- prevent new likes;
- prevent new DMs through conforming relays;
- hide existing normal UI surfaces;
- preserve the blocker's ability to report;
- not notify the blocked user.
Block graph is P2 private.
The client SHOULD maintain:
local immediate block
+
private NIP-51 list
+
encrypted block command to user's OpenDating providers
This provides:
- instant client safety;
- cross-device portability;
- server enforcement;
- no public block graph.
OpenDating SHOULD reuse NIP-56 report semantics inside a NIP-59 envelope.
Example conceptual flow:
NIP-56 kind 1984 report
│
│ private
▼
NIP-59 gift wrap
│
▼
Moderation Service
This preserves existing Nostr reporting semantics without publicly exposing:
who reported whom
why
when
evidence
The reference moderation system SHOULD support:
harassment
threat
stalking
location abuse
doxxing
sexual harassment
unsolicited sexual content
impersonation
catfishing
scam
extortion
hate
fetishization
suspected underage user
offline violence
spam
other
Map these to NIP-56's existing broad report types and/or NIP-32 labels where possible until a standardized taxonomy exists.
submitted
↓
validated
↓
triaged
↓
in_review
↓
┌──────────────┬─────────────┐
│ │ │
dismissed actioned escalated
│ │ │
└──────────────┴─────────────┘
↓
closed
↓
optional appeal
Reference levels:
Examples:
credible immediate threat
credible stalking/location threat
suspected exploitation of minors
serious doxxing
Action:
immediate protective restriction
urgent human review
sexual harassment
threatening behavior
repeat targeted harassment
extortion
impersonation
scams
repeated unwanted conduct
spam
profile-policy issues
minor conduct complaints
Severity only controls urgency.
It does not automatically determine guilt.
OpenDating reference moderation MUST follow:
Ten unverified reports are not automatically stronger than one cryptographically supported report.
The accused MUST NOT receive:
reporter identity
private evidence
reporter's written narrative
moderator notes
unless legally required.
Temporary quarantine can occur before permanent adjudication where user safety requires it.
Permanent bans should not be produced solely by automatic report counts.
This aligns with NIP-56's warning that reports can be gamed.
Moderators cannot simply browse all conversations.
They only see evidence voluntarily included in an authorized case.
Supported actions:
warning
outbound_like_limit
outbound_like_disabled
dm_restricted
discovery_hidden
verification_required
quarantine
temporary_suspend
permanent_ban
profile_media_remove
profile_remove
Each action MUST contain:
action_id
target
action_type
issuer
created_at
expires_at if temporary
reason_code
case_id
appeal_status
Internal details remain P4.
Users SHOULD be able to appeal serious actions.
Appeal flow:
user submits appeal
↓
different moderator or senior reviewer
↓
uphold / modify / reverse
The protocol does not need to standardize the full human workflow, but reference infrastructure should support it.
Because moderation policy should not necessarily become a NIP, OpenDating SHOULD define a separate:
OpenDating Safety Profile
A relay claiming Safety Profile V1 MUST support:
18+ network
NIP-42 authentication
private profile discovery
no exact location disclosure
match-only DMs
private blocks
immediate unmatching
private reports
report-after-unmatch
moderation workflow
moderator audit trail
account deletion
NIP-62 handling
no safety paywalls
This lets the ecosystem decentralize without treating:
“anything technically allowed by Nostr”
as automatically acceptable for dating.
Verification is a provider role.
Do not build one centralized OpenDating identity authority into the protocol.
Potential claims:
human_verified
photo_verified
age_over_18
age_range_verified
id_verified
account_established
social_graph_established
A verification claim contains:
subject
issuer
claim
issued_at
expires_at
proof reference if appropriate
Clients MUST display the issuer.
Do NOT show a universal:
Safety Score: 94/100
Verification means only what its claim says.
Even Tinder explicitly warns that photo verification is not a guarantee of identity or safety.
NIP-85 MAY later provide useful external trust signals such as account age, activity or web-of-trust calculations. It allows independent providers to publish signed assertions rather than forcing clients to compute everything themselves.
However:
NIP-85 score != human verification
NIP-85 score != safety guarantee
report count != guilt
The reference implementation MUST keep those distinctions explicit.
No single technique solves Sybil attacks.
OpenDating SHOULD combine signals.
Tier examples:
new Nostr key
established account history
or
valid NIP-05
or
reasonable Nostr social graph
human/photo verification
stronger optional identity/age verification
Relays may apply different limits by tier.
Example:
Tier 0: 20 likes/day
Tier 1: 50 likes/day
Tier 2: 100 likes/day
Exact limits are operator policy, not protocol.
Rate limiting MUST use the authenticated NIP-42 pubkey where available.
Additional network/device risk signals MAY supplement it.
Separate buckets are required for:
AUTH
profile updates
location updates
discovery queries
likes
gift wraps
DM sends
reports
media uploads
Do not use one global event limit for everything.
The upstream Nosflare configuration currently excludes kind:1059 from its normal event rate limiter.
That is inappropriate for a dating relay.
NIP-59 explains why gift wraps are an attractive spam vector: wrapper events use random one-time keys, preventing ordinary pubkey reputation/rate-limiting unless the relay uses NIP-42 authentication.
OpenDating MUST:
remove 1059 from the unlimited/excluded path
and rate-limit gift wraps according to the authenticated session identity.
This is a P0 security requirement.
The current Nosflare Durable Object implements local/global query caches based primarily on filters/bookmark.
OpenDating MUST introduce cache classifications:
PUBLIC_CACHEABLE
AUTH_SCOPED
PRIVATE_NO_CACHE
The following MUST NEVER use a shared public/global cache:
kind 1059 recipient queries
dating profile responses
matches
blocks
reports
verification-private results
anything whose answer depends on authenticated identity
Prefer:
PRIVATE_NO_CACHE
for privacy-critical operations.
A response produced for Alice MUST never become retrievable by Bob because they issued syntactically identical filters.
This is another P0 requirement.
Dating photos MUST NOT simply be uploaded to a public permanent bucket URL.
Reference storage:
private R2 bucket
The client SHOULD resize/compress images before upload.
Recommended limits:
2–6 images
<= 3 MB per image accepted by server
client target <= 2 MB
max dimension approximately 2048 px
JPEG / PNG / WebP
No video in MVP.
Client
│
│ encrypted media.ticket request
▼
Media Service
│
│ short-lived upload authorization
▼
Client
│
│ authenticated upload
▼
Private R2
Private retrieval:
authorized candidate
│
▼
short-lived media token
│
▼
Worker
│
▼
R2
R2 currently includes a free allocation of 10 GB-month storage plus free request allowances and zero Internet egress fees, making it practical for an MVP if image sizes are constrained.
Blossom/NIP-B7 SHOULD remain an interoperability target for portable media because it uses content-addressed blobs and server lists.
However:
normal public Blossom retrieval is not sufficient access control for private dating photos.
Possible future portable model:
photo
↓
client encrypts blob
↓
ciphertext uploaded to Blossom
↓
authorized candidate receives decryption material privately
Do not delay MVP for this.
MVP DM:
text only
This intentionally removes unsolicited DM images from the first version.
Post-MVP:
- encrypted NIP-17 file messages;
- recipient media consent;
- client-side explicit-image detection;
- automatic blur until recipient chooses to reveal;
- one-tap report/block;
- sender-side warning before potentially sexual content.
Bumble currently uses automatic blur controls for potentially explicit images, illustrating the safety bar a competitive dating product should eventually meet.
These classifiers SHOULD run client-side where practical so the relay does not need plaintext.
Post-MVP clients SHOULD support on-device prompts such as:
"This message may be disrespectful. Send anyway?"
and recipient prompts such as:
"Did this message bother you?"
Equivalent patterns are currently used by major dating products.
Again:
client-side classification preferred
server plaintext inspection prohibited by default
Post-MVP SHOULD include an optional Share Date feature.
The user may share:
match display info
planned venue
planned date/time
optional check-in time
with a chosen trusted contact.
Do NOT make real-time location sharing mandatory.
The protocol should permit expiring encrypted safety-share payloads.
Modern apps including Bumble and Tinder provide date-sharing features as a safety control.
Three different actions must exist:
Stops OpenDating discovery and deletes application-specific dating state.
Requests deletion of specific Nostr events.
Requests supporting relay deletion of everything for the user.
When a user invokes full OpenDating deletion:
profile deleted
discovery index deleted
pending intents deleted
matches closed
blocks may be removed after safe transaction
normal media deleted
service responses pruned
DM gift wraps addressed to user deleted where required
Safety/legal evidence MAY require separate retention according to operator policy and applicable law.
That exception MUST be transparent and purpose-limited.
NIP-62 requires supporting relays to prevent deleted events from simply being rebroadcast back into the relay.
Therefore maintain a minimal tombstone:
pubkey/internal-id
vanish cutoff timestamp
request id/hash
Do not preserve the deleted profile itself.
The reference backend SHOULD avoid using raw pubkeys as every internal database foreign key.
Generate:
member_id =
HMAC-SHA256(
OD_INDEX_KEY,
nostr_pubkey
)
Private tables can use member_id.
Store the actual public key only where necessary, preferably encrypted in the membership table.
Benefits:
- database breach does not immediately expose which known Nostr keys use the dating network;
- joins remain fast;
- indexes remain deterministic.
The HMAC key MUST be a Worker secret and MUST support key-version migration.
Highest-sensitivity structured data SHOULD use AES-GCM envelope encryption before D1/R2 storage.
Separate keys:
OD_DATA_KEY_V1
OD_MODERATION_KEY_V1
OD_MEDIA_TOKEN_KEY_V1
OD_INDEX_KEY_V1
Moderation evidence SHOULD use a separate key domain from ordinary dating data.
Every encrypted row/object stores:
key_version
nonce
ciphertext
No encryption key belongs in Git.
Retain generic Nosflare tables for generic Nostr behavior, but OpenDating MUST use separate domain tables.
member_id PK
encrypted_pubkey
status
trust_tier
created_at
updated_at
last_active_bucket
protocol_version
Do not store exact presence timestamps for client display.
member_id PK
profile_version
encrypted_profile_payload
age
gender_category
relationship_intent
visibility_state
completeness
created_at
updated_at
Only normalized discovery fields required for filtering should remain queryable.
media_id PK
member_id
r2_object_key
sha256
mime
sort_order
moderation_state
created_at
member_id PK
geo_cell_p5
geo_cell_p4
age
gender_category
intent_category
visible
trust_tier
activity_bucket
updated_at
No latitude/longitude.
member_id PK
encrypted_policy
verified_only
age_min
age_max
distance_max_bucket
updated_at
intent_id PK
sender_member_id
target_member_id
state
created_at
expires_at
matcher_id
Unique active constraint:
sender + target
match_id PK
member_a
member_b
state
created_at
updated_at
matcher_id
blocker_member_id
blocked_member_id
created_at
source
UNIQUE(blocker, blocked)
report_id PK
reporter_member_id
target_member_id
category
severity
status
evidence_strength
created_at
updated_at
assigned_to
evidence_id PK
report_id
r2_object_key
key_version
sha256
created_at
action_id PK
target_member_id
case_id
action_type
reason_code
created_at
expires_at
issuer
appeal_status
appeal_id PK
action_id
status
created_at
reviewed_at
reviewer
claim_id PK
subject_member_id
issuer_pubkey
claim
status
issued_at
expires_at
proof_reference
service
request_id
member_id
result_hash
expires_at
member_id
cutoff_timestamp
request_hash
created_at
audit_id
actor
action
resource_type
resource_id
timestamp
metadata_redacted
Never put raw DM plaintext into the audit log.
The free tier makes index design part of product architecture.
Cloudflare counts rows scanned, and indexes reduce rows read.
Minimum indexes:
od_discovery_index(
geo_cell_p5,
visible,
age,
activity_bucket
)
od_discovery_index(
geo_cell_p4,
visible,
age
)
od_intents(
sender_member_id,
target_member_id
) UNIQUE
od_intents(
target_member_id,
state,
created_at
)
od_matches(
member_a,
state
)
od_matches(
member_b,
state
)
od_blocks(
blocker_member_id,
blocked_member_id
) UNIQUE
od_blocks(
blocked_member_id,
blocker_member_id
)
od_reports(
target_member_id,
status,
created_at
)
Avoid creating an index for every filter because D1 also counts index writes.
Primary SQL prefilter:
geo
visibility
age
Then evaluate less-common compatibility fields in Worker code over a small bounded candidate set.
Target:
<= 100–200 rows considered
per normal discovery query
Never full-table scan.
The architecture should deliberately target the free tier first.
Current Workers Free limits include approximately:
100,000 requests/day
10 ms CPU per HTTP request
128 MB memory
Current D1 Free:
5,000,000 rows read/day
100,000 rows written/day
5 GB storage
R2 includes:
10 GB-month
1M Class A operations/month
10M Class B operations/month
free Internet egress
Durable Object WebSocket Hibernation should remain enabled because connected clients can stay connected while the object sleeps.
The existing fork needs immediate configuration hardening.
Current upstream defaults:
PAY_TO_RELAY_ENABLED = true
Reference OpenDating MUST set:
PAY_TO_RELAY_ENABLED = false
Basic dating and safety cannot depend on payment.
Monetization may be layered on later without affecting protocol interoperability.
Upstream currently begins pruning around 9 GB.
Free D1 currently provides 5 GB total.
Reference free-tier defaults should target approximately:
start pruning: 4.0–4.25 GB
target after prune: 3.5–3.75 GB
with configuration rather than hardcoded constants.
The upstream wrangler.toml currently specifies a much larger CPU allowance than the Workers Free limit.
Create explicit:
development
free-production
paid-production
configuration profiles.
V0.1 MUST continuously benchmark cryptographic/event paths against the free target.
The current relay performs substantial schema creation/migration logic at runtime in relay-worker.ts.
OpenDating SHOULD move toward explicit migration files:
migrations/
0001_nosflare_base.sql
0002_opendating_members.sql
0003_opendating_profiles.sql
0004_opendating_matching.sql
0005_opendating_safety.sql
Production requests MUST NOT perform major schema migrations.
Deployment should explicitly apply migrations.
Refactor the fork toward:
/
├── src/
│ ├── index.ts
│ │
│ ├── worker/
│ │ ├── handler.ts
│ │ ├── routes.ts
│ │ ├── nip11.ts
│ │ └── health.ts
│ │
│ ├── relay/
│ │ ├── nostr/
│ │ │ ├── events.ts
│ │ │ ├── filters.ts
│ │ │ ├── signatures.ts
│ │ │ └── errors.ts
│ │ ├── auth/
│ │ ├── websocket/
│ │ ├── subscriptions/
│ │ ├── storage/
│ │ ├── cache/
│ │ └── policy/
│ │
│ ├── opendating/
│ │ ├── protocol/
│ │ │ ├── version.ts
│ │ │ ├── message-types.ts
│ │ │ ├── envelope.ts
│ │ │ ├── schemas/
│ │ │ ├── errors.ts
│ │ │ └── capabilities.ts
│ │ │
│ │ ├── router/
│ │ │
│ │ ├── services/
│ │ │ ├── profile/
│ │ │ ├── discovery/
│ │ │ ├── matcher/
│ │ │ ├── messaging-policy/
│ │ │ ├── blocks/
│ │ │ ├── moderation/
│ │ │ ├── verification/
│ │ │ ├── media/
│ │ │ └── deletion/
│ │ │
│ │ ├── storage/
│ │ │ ├── interfaces.ts
│ │ │ └── d1/
│ │ │
│ │ ├── crypto/
│ │ └── privacy/
│ │
│ ├── admin/
│ └── shared/
│
├── packages/
│ ├── protocol/
│ └── conformance/
│
├── migrations/
│
├── schemas/
│
├── tests/
│ ├── unit/
│ ├── protocol/
│ ├── conformance/
│ ├── security/
│ ├── integration/
│ └── load/
│
├── docs/
│ ├── PROTOCOL.md
│ ├── THREAT-MODEL.md
│ ├── PRIVACY.md
│ ├── MODERATION.md
│ ├── FEDERATION.md
│ ├── SAFETY-PROFILE.md
│ └── NIP-DRAFT.md
│
└── PRD.md
MUST contain no Cloudflare imports.
Contains:
schemas
message types
capabilities
versioning
validation
errors
test vectors
Another Go/Rust relay should be able to implement the protocol solely from this package's specification/schema equivalents.
Black-box tests any compatible relay/service.
Examples:
can authenticate
cannot enumerate profiles
can create profile
can discover candidates
likes remain private
mutual likes produce match
unmatched users cannot DM
blocks immediately deny DM
reports remain private
vanish removes data
This package is strategically important.
Business logic MUST depend on interfaces.
Example conceptual interfaces:
MemberRepository
ProfileRepository
DiscoveryRepository
IntentRepository
MatchRepository
BlockRepository
ReportRepository
ModerationRepository
VerificationRepository
MediaRepository
Cloudflare D1 is merely the reference adapter.
Do not import:
D1Database
throughout domain logic.
Instead:
DiscoveryService
↓
DiscoveryRepository
↓
D1DiscoveryRepository
Later:
PostgresDiscoveryRepository
SQLiteDiscoveryRepository
RedisDiscoveryRepository
can exist without rewriting the protocol.
Likewise:
ProfileService
DiscoveryService
MatcherService
ModerationService
VerificationService
MediaService
Each receives a domain request and returns a domain result.
The OpenDating Nostr router performs:
decrypt
validate envelope
resolve service
authorize
invoke service
wrap response
This is the main seam that allows future extraction.
Do NOT add:
["DATE_MATCH", ...]
["DISCOVER", ...]
["SWIPE", ...]
to the Nostr protocol.
Use ordinary:
EVENT
REQ
AUTH
with NIP-59 application events.
This maximizes compatibility with normal relay/client infrastructure.
Use existing relay prefixes whenever possible:
auth-required:
restricted:
rate-limited:
invalid:
blocked:
OpenDating code follows the prefix:
restricted: od:not-matched
restricted: od:blocked
restricted: od:membership-required
restricted: od:verification-required
invalid: od:profile-schema
invalid: od:expired-request
invalid: od:unsupported-version
rate-limited: od:discovery
rate-limited: od:likes
Service-level errors are also returned in encrypted OpenDating responses.
Every OpenDating type MUST have:
JSON Schema
TypeScript type
runtime validator
example fixture
invalid fixture
conformance test
JSON Schema is canonical for cross-language implementations.
Do not make TypeScript itself the specification.
Every application event includes:
version
Policy:
0.x = experimental
1.x = stable
A service manifest advertises supported versions.
A client selects the highest mutually supported compatible version.
Breaking schema changes require a major protocol version.
Every mutating command MUST carry:
request_id
Service stores bounded idempotency state.
Repeated:
intent.like request_id=123
cannot create duplicate intents.
Repeated report submission with same request ID cannot create multiple cases.
Services MUST reject:
- stale commands;
- expired candidate grants;
- expired service tokens;
- duplicate request IDs;
- invalid NIP-59 seals;
- invalid sender/rumor identity relationships;
- expired auth challenges.
Expiration windows should be command-specific.
A single rule should exist in code:
if response authorization depends on identity:
sharedCache = forbidden
This MUST be enforced centrally rather than relying on individual developers remembering it.
Reference implementation requires a minimal admin UI.
It MAY be:
Cloudflare Pages
or
Worker-served application
protected by strong operator authentication.
Prefer Cloudflare Access or an equivalent identity layer rather than a shared password.
Roles:
reviewer
moderator
senior_moderator
administrator
Screens:
Dashboard
Report Queue
Report Detail
User Safety History
Profile Review
Moderation Actions
Appeals
Verification Claims
System Health
Audit Log
Report detail may show:
report category
report severity
reporter-provided evidence
cryptographic verification result
previous relevant moderation actions
target profile version
It MUST NOT offer:
"Browse all DMs"
Every moderator action MUST record:
moderator identity
action
case
timestamp
reason code
before state
after state
Audit records MUST be append-oriented and protected against ordinary moderator editing.
Allowed aggregate metrics:
new users
active users
profiles created
discovery queries
candidate impressions aggregate
likes
matches
match rate
messages sent count
blocks
unmatches
reports
report categories
moderation resolution time
verification adoption
Forbidden analytics:
DM plaintext
exact GPS
raw contact lists
nsec
moderator evidence contents in analytics
detailed individual romantic preference dossiers
Prefer internal pseudonymous IDs rather than raw pubkeys in telemetry.
The backend must support a client capable of:
- create/import Nostr key;
- authenticate;
- delete account.
- name;
- age;
- gender;
- pronouns optional;
- dating intent;
- bio;
- prompts;
- interests;
- 2–6 photos;
- coarse location.
- age filter;
- distance filter;
- compatible gender/preference;
- relationship intent;
- candidate pagination;
- pause discovery.
- like;
- pass;
- mutual match;
- match list.
- NIP-17 encrypted text messages.
- block;
- unmatch;
- report;
- report after unmatch;
- profile pause;
- verification architecture;
- moderator review;
- deletion/vanish.
Anything beyond that is not necessary for first release.
Priority order:
- photo/liveness verification;
- ID/age provider integration;
- verified-only discovery;
- verified-only inbound interactions.
- encrypted image messaging;
- explicit-image blur;
- media consent;
- on-device message safety nudges.
- Share Date;
- safety check-in;
- Block Contacts with privacy-preserving implementation.
- voice/video calls;
- multiple matcher providers;
- multiple discovery algorithms;
- cross-relay federation.
- personalized ranking;
- optional Lightning features;
- richer trust providers.
Safety features always remain free.
Do NOT upload an entire address book in plaintext.
If Block Contacts is implemented later:
- user opts in;
- selected contacts only where possible;
- normalize locally;
- use privacy-preserving tokenization/HMAC through a dedicated service;
- never expose raw contact lists to discovery or matcher services;
- delete source contact information immediately after tokenization where practical.
Manual blocking by Nostr identity can ship first.
MVP MUST NOT have:
Search dating users by legal name
Search dating users by phone
Search dating users by email
Global directory
These create unnecessary stalking surfaces.
Discovery should be compatibility-based.
Do not return:
online now
active 3 minutes ago
last seen 8:42 PM
by default.
Use broad buckets:
active recently
active this week
or omit activity entirely.
Exact activity timestamps remain internal only where technically necessary.
Reference defaults:
current profile: while active
old profile versions: remove after replacement unless safety case requires snapshot
location history: none
current location cell: latest only
pending likes: 90 days
client passes: not server persisted by default
unmatched relationship state: minimal tombstone
service command gift wraps: delete shortly after successful processing
user DM wraps: configurable rolling retention
closed report evidence: bounded safety retention
deleted media: remove promptly
Retention MUST be documented in relay policy.
Do not force all generic Nostr events into OpenDating semantics.
Architecture:
Nostr Core
│
├ normal generic event
│ ↓
│ generic relay pipeline
│
└ OpenDating service envelope
↓
OpenDating router
Dating logic should not infect every generic query/storage function.
One Worker can initially expose:
wss://dating.example.com
while internally owning:
relay role
profile role
discovery role
matcher role
dm-policy role
moderation role
verification registry
media role
Deployment flag:
OD_ROLES=
relay,profile,discovery,matcher,dm-policy,moderation,verification,media
Later a deployment may run:
OD_ROLES=matcher
or:
OD_ROLES=relay,dm-policy
No protocol rewrite should be required.
Reference configuration should move away from source-edited constants for sensitive/operational values.
Example:
OD_ENABLED
OD_PROTOCOL_VERSION
OD_PROFILE_SERVICE_PRIVKEY
OD_DISCOVERY_SERVICE_PRIVKEY
OD_MATCHER_SERVICE_PRIVKEY
OD_MODERATION_SERVICE_PRIVKEY
OD_VERIFICATION_SERVICE_PRIVKEY
OD_DATA_KEY_V1
OD_MODERATION_KEY_V1
OD_INDEX_KEY_V1
OD_MEDIA_TOKEN_KEY_V1
OD_GEOHASH_PRECISION
OD_DISCOVERY_BATCH_SIZE
OD_MAX_DISCOVERY_REQUESTS
OD_MAX_LIKES_PER_DAY
OD_DM_RETENTION_DAYS
OD_INTENT_RETENTION_DAYS
OD_REPORT_RETENTION_DAYS
OD_FREE_TIER_MODE
Public nonsecret configuration may remain normal environment variables.
Secrets use Worker Secrets.
Responsibilities:
HTTP routing
NIP-11
WebSocket upgrade routing
private media HTTP routes
health endpoint
admin gateway
Responsibilities:
WebSocket lifetime
NIP-42 session state
subscriptions
connection-level rate limiting
real-time delivery
broadcast
Keep WebSocket Hibernation.
Responsibilities:
generic relay event store
OpenDating private data
matching
discovery index
moderation state
Responsibilities:
profile photos
encrypted moderation evidence
future encrypted message attachments
Workers Free currently has a 10 ms CPU target per HTTP request.
Do not put:
large image classifiers
LLMs
face recognition
heavy recommendation ML
inside the first Worker.
Prefer:
client-side inference
external optional provider
simple deterministic backend logic
The MVP should remain cheap enough to operate for free.
Reference engineering targets:
AUTH response < 500 ms p95
profile mutation < 1 s p95
discovery result < 1 s p95
like acknowledgement < 1 s p95
mutual match notification < 2 s p95
block enforcement next request / < 1 s
DM delivery < 1 s p95 while online
report acknowledgement < 2 s p95
These are engineering targets, not contractual SLAs.
During MVP:
normal development use should remain $0
small closed beta should target $0
The system should expose usage telemetry for:
Worker requests
Worker CPU
D1 rows read
D1 rows written
D1 storage
R2 storage
R2 operations
Durable Object requests
A development build SHOULD fail tests that introduce obvious full-table discovery scans.
Automated tests MUST verify:
- invalid signature rejected;
- wrong challenge rejected;
- wrong relay URL rejected;
- expired challenge rejected;
- replayed challenge rejected.
- unauthenticated discovery denied;
- suspended user denied;
- wrong recipient cannot read gift wraps.
- arbitrary generic
REQcannot enumerate profiles; - direct arbitrary profile fetch denied.
- exact GPS never persisted;
- raw location cell never returned to candidate;
- distance exactness unavailable.
- one-way like invisible to target;
- duplicate like idempotent;
- blocked users cannot like.
- unilateral like does not create match;
- reciprocal like does;
- duplicate matcher processing does not duplicate match.
- unmatched sender denied;
- blocked sender denied;
- authenticated matched sender accepted;
- Bob cannot retrieve Alice-to-Carol wrappers.
- takes effect immediately;
- removes discovery;
- revokes match;
- does not disclose blocker reason.
- report is not publicly queryable;
- evidence encrypted at rest;
- fake seal evidence fails verification;
- report count alone cannot trigger permanent automatic ban.
- deleted user removed from discovery;
- media deleted;
- stale profile cannot be reintroduced after valid vanish cutoff.
Fuzz:
Nostr EVENT parsing
tags
filters
NIP-42 AUTH
OpenDating envelopes
JSON schemas
unexpected Unicode
oversized payloads
invalid timestamps
duplicate fields
malformed gift wraps
Property tests:
block(A,B) ⇒ no candidate B→A
block(A,B) ⇒ no DM B→A
match(A,B) ⇒ reciprocal intents existed
unmatch(A,B) ⇒ future DM denied
vanish(A) ⇒ A never returned by discovery
The conformance suite MUST run against a relay URL rather than importing internal implementation functions.
Example:
opendating-conformance \
--relay wss://localhost:8787 \
--profile-service <pubkey> \
--matcher-service <pubkey>This is what will eventually allow:
Nosflare TypeScript implementation ✓
Khatru Go implementation ✓
Rust implementation ✓
to prove interoperability.
Do NOT immediately submit:
“NIP-XX Dating.”
The current NIPs repository states that accepted proposals should generally be fully implemented in at least two clients and one relay when applicable, remain optional/backwards-compatible, and avoid duplicative ways of doing the same thing.
The correct progression is:
OpenDating 0.1 experimental
↓
reference relay
↓
reference client
↓
protocol conformance suite
↓
second independent client
↓
independent/second relay implementation
↓
protocol revisions
↓
NIP proposal
Do NOT attempt to standardize the entire dating product.
A future NIP should standardize the smallest interoperable layer required for independent clients/providers.
Likely candidates:
How an eligible profile is represented and requested privately.
How users privately express intent to a matcher.
How a matcher signals reciprocal intent.
Existing NIPs already handle:
authentication
encryption
gift wrapping
DMs
private lists
reports
deletion
Reuse them.
Ranking algorithms, app UI and operator moderation rules should remain outside the core NIP.
Before proposal, an independent developer who has never seen the source code should be able to read:
PROTOCOL.md
schemas/
test-vectors/
and successfully implement:
create profile
discover profile
express intent
match
message
block
report
against the reference relay.
If they need to inspect Cloudflare Worker code to understand the protocol, the specification is not ready.
Create:
PRD.md
PROTOCOL.md
THREAT-MODEL.md
PRIVACY.md
MODERATION.md
SAFETY-PROFILE.md
FEDERATION.md
CONFORMANCE.md
NIP-DRAFT.md
Normative wire specification only.
Attackers and mitigations.
Exactly who sees each data type.
Reference operator policy and evidence handling.
Minimum safety requirements for compatible relays claiming OpenDating safety conformance.
Multiple relay/provider architecture.
Maintained from early development but explicitly marked:
NOT A NIP
EXPERIMENTAL
until implementation maturity.
Before dating features:
- make existing Nosflare tests/baseline reproducible;
- disable pay-to-relay;
- change branding/config to OpenDating Reference Relay;
- move secrets/config to environment;
- remove
1059from rate-limit exclusions; - implement authenticated-user rate limits;
- prevent shared caching of private/auth-scoped results;
- set free-tier D1 pruning thresholds;
- establish explicit migrations;
- add CI;
- add unit/integration test harness;
- update advertised NIPs only when actually tested.
Definition of done:
A standard Nostr client can still connect, AUTH, publish/query appropriate generic events, and NIP-17 recipient privacy tests pass.
Implement:
@opendating/protocol
versioning
envelope
schemas
errors
service manifest
service keys
OpenDating router
NIP-59 service request helpers
idempotency
Definition of done:
A test client can send an encrypted service.echo request to an embedded service identity and receive an encrypted response.
Implement:
od_members
od_profiles
profile.upsert
profile.pause
profile.resume
profile.delete
private profile media
visibility settings
Definition of done:
Alice can create a profile.
Bob cannot fetch it directly.
An authorized internal candidate operation can retrieve it.
Implement:
coarse geohash
discovery index
visibility policy
bilateral eligibility
discovery.query
discovery.result
distance buckets
candidate grants
cursor pagination
anti-enumeration limits
Definition of done:
Alice receives geographically relevant eligible candidates without receiving coordinates or arbitrary profile-directory access.
Implement:
intent.like
intent.revoke
od_intents
reciprocal detection
deterministic match IDs
match.created
match.list
Definition of done:
Alice likes Bob:
Bob learns nothing.
Bob likes Alice:
both receive match.
Implement:
match-only DM gate
NIP-17 delivery
recipient AUTH
private 1059 query
retention
Definition of done:
Matched users can talk.
Unmatched users cannot send a DM through the conforming dating relay.
Relay never learns DM plaintext.
Implement:
block.add
block.remove
unmatch
private NIP-51 synchronization
immediate authorization revocation
cross-discovery enforcement
Definition of done:
One block action prevents:
discovery
likes
match
DM
in both relevant directions.
Implement:
private NIP-56 reporting
evidence bundle
cryptographic DM evidence validation
report queue
case states
moderation actions
audit
appeals foundation
admin UI
Definition of done:
A user can report abusive encrypted content without the relay having continuously decrypted conversations.
Implement:
profile deletion
media deletion
NIP-09
NIP-62
tombstones
data-retention jobs
privacy tests
Perform an explicit privacy review.
Implement provider interface first.
Then optionally integrate:
photo verification
human/liveness verification
age verification
ID verification
The reference relay should ideally consume claims rather than becoming the permanent owner of raw biometric/ID material.
Extract one service from the monolith.
Recommended first extraction:
Matcher Service
Run it as a separate Nostr-speaking service.
If that requires major client changes, the original architecture failed.
Then implement a second matcher.
Build a minimal second implementation, potentially:
Khatru / Go
It does not need feature parity.
It merely needs enough functionality to prove the protocol is not secretly dependent on Nosflare.
Only after:
protocol stable
reference client works
second client works
reference relay works
independent relay/service works
conformance suite passes
security model documented
privacy model documented
should dedicated event kinds and formal NIP text be proposed.
The backend MVP is complete when all of the following are true:
- Nostr identity is the account identity.
- NIP-42 AUTH is mandatory for OpenDating private operations.
- No exact GPS is stored.
- Dating profiles cannot be globally enumerated.
- Profile media is not publicly enumerable.
- Discovery supports location, age and compatibility filters.
- Discovery has bilateral visibility.
- Candidate results expose only coarse distance.
- One-way likes remain private.
- Reciprocal likes create matches.
- NIP-17 text chat works.
- DMs require an active match.
- Gift wraps are only queryable by authorized recipients.
- Block takes effect immediately.
- Unmatch terminates DM permission.
- Reports are private.
- Users can report after unmatching.
- DM abuse can be reported with verifiable evidence.
- Raw report counts cannot automatically permanently ban users.
- Moderators cannot arbitrarily browse conversations.
- Moderation actions are audited.
- Users can pause discovery.
- Users can delete their dating profile.
- NIP-62 vanish is handled.
- Private/auth responses are never shared through global caches.
- Kind 1059 is rate-limited by authenticated identity.
- Core flows have black-box conformance tests.
- Cloudflare-specific code does not exist in
@opendating/protocol. - Service roles use separate Nostr keys.
- A service can theoretically move to another process without changing its wire contract.
- Core functionality remains free.
Any coding agent working on this repository MUST follow these rules:
- Preserve protocol/implementation separation.
- Do not introduce a REST API for a feature that can cleanly use the OpenDating Nostr service envelope.
- Do not create public tags containing sensitive dating information.
- Never store exact GPS.
- Never store nsec/private user keys.
- Never log decrypted DMs.
- Never log moderation evidence.
- Never disable auth to make a feature easier.
- Never bypass blocks in a secondary code path.
- Never expose a direct global profile enumeration query.
- Never add an OpenDating service without a documented privacy classification.
- Never add a schema without a version and fixture.
- Never add a persistent table without a retention policy.
- Never add an auth-dependent query to a shared cache.
- Never make safety functionality conditional on payment.
- Never implement custom cryptography when an appropriate Nostr standard/library exists.
- Keep every logical service behind an interface.
- Every mutation must be idempotent.
- Every important privacy property must have a negative test proving unauthorized access fails.
- Optimize for correctness and safety before clever decentralization.
OpenDating should begin operationally centralized enough to be buildable:
one Cloudflare account
one Worker
one D1 database
one R2 bucket
one Durable Object architecture
but protocol-level decentralized enough that none of those become permanent requirements:
different relay
different database
different storage
different discovery provider
different matcher
different moderator
different verifier
different client
should all remain possible.
The reference implementation is therefore not the OpenDating network.
It is only:
the first reference implementation of the OpenDating protocol.
The protocol is the valuable asset.
The ideal long-term outcome is not that everybody connects to one OpenDating server.
It is:
Client A
Client B
Client C
↕ OpenDating
Relay A
Relay B
Relay C
Discovery A
Discovery B
Matcher A
Matcher B
Safety Provider A
Safety Provider B
Verification Provider A
Verification Provider B
with users free to choose among them.
That architecture preserves what Nostr is good at—portable identity, signed events, open transport, replaceable infrastructure and encrypted communication—while deliberately adding the privacy, discovery, matching, authorization, anti-abuse and moderation systems that a real dating network requires.