MeetX (working title) — Phone-Number-Based Social Matching
Social matching built on mutual phone-number registration and a token economy
Background
A social matching platform for reconnecting people who have lost touch. A user anonymously registers the phone number of someone they miss, and a match forms only when the other person registers the same pair of numbers. Contact details could not be handled in plaintext, and mutual registration had to be decided in constant time. Payments, subscriptions, and social login had to cover several platforms. The project started as a Django prototype, then moved to FastAPI with async SQLAlchemy because it needed asynchronous APIs and testable layer boundaries.
System architecture
- Domain boundaries: Router (HTTP) to Service to Repository to UnitOfWork to DB. The 10 domain entities are declared as dataclasses so they do not depend on the ORM.
- Dual phone number protection: the stored column is reversibly encrypted with AES-256-GCM, and the lookup column is a one-way SHA-256 hash with a pepper. An index on the hash column makes mutual-registration detection O(1).
- Token economy:
TokenLedgerrecords every credit and debit as an append-only entry. Instant reveal on a match and trace reveal draw tokens from different slots. - Idempotent payments: Toss is implemented, and Apple IAP and Google Play have their structure defined. Four tables (
webhook_events,outbox_events,payment_ledger,entitlement_audit_logs) record a triple check of idempotency key, transaction ID, and amount.
How it was built
Django to FastAPI rebuild. The original structure mixed business logic into routers on top of a synchronous ORM. Async processing and testable boundaries were needed, so the service moved to FastAPI with async SQLAlchemy 2.0. Routers handle only HTTP entry, domain logic lives in services, and queries live in repositories, which makes each layer unit testable.
Dual phone number protection. Phone numbers are never stored in plaintext. The storage column is encrypted with AES-256-GCM and read only by logic that requires decryption. The lookup and matching column holds a one-way SHA-256 hash with a pepper under its own index. Match decisions read only the hash column, so they never touch plaintext.
O(1) mutual-registration detection. When user A registers number X, the hash of X is looked up in the x_registry.target_phone_hash index to see whether B registered first. If so, a Match is created (PENDING, unlock_time = now + 10h); if not, only a record in match_traces is left. The hash index keeps this constant time regardless of registration volume.
PENDING to REVEALED transition. A match becomes visible through two paths. (1) A batch job flips every match whose unlock_time has passed to REVEALED, so it opens naturally with the passage of time. (2) When a user spends an instant-reveal token, match_repository promotes that single match to REVEALED right away. Both paths update the same status column, so the client only has to read one field to know whether the match is revealed.
Transaction integrity through UnitOfWork. UnitOfWork wraps the SQLAlchemy session as a context manager. Operations that touch several repositories commit or roll back as one transaction. Those operations include signup (including linking the three social providers), payment approval, and account merging.
Idempotency across three payment providers. Toss, Apple IAP, and Google Play arrive through webhook_events. The retry queue is outbox_events, the final state is payment_ledger, and the audit log for granted entitlements is entitlement_audit_logs. Cross-checking idempotency key, transaction ID, and amount blocks duplicate grants and amount tampering.
Three social login providers and account merging. Kakao, Naver, and Google are unified behind an oauth_account abstraction. Accounts with the same email link automatically, and SMS verification on the same phone number allows manual linking.
Results
- A DDD-lite layered architecture with 49 backend endpoints, 18 tables, and 13 services and 13 repositories.
- Dual phone number protection (AES-256-GCM plus SHA-256 hash) with O(1) mutual-registration detection and a 10-hour delay before reveal.
- A TokenLedger-based token economy with separate slots for instant reveal and trace reveal.
- Four payment tables (webhook_events / outbox_events / payment_ledger / entitlement_audit_logs) providing idempotency and audit coverage across three providers.
- Three OAuth providers unified, with automatic merging on matching email and manual merging via SMS verification.
- Three GitHub Actions pipelines (CI / staging / production) and an EC2 staging server in operation.