Blindy

An anonymous dating app for university students. You talk for seven minutes before you see a face, and names open only after a mutual yes.

Role
Solo developer: backend, Flutter app, admin panel, server
Status
In closed beta, getblindy.app
When
January 2026 to now
Stack
Java 17, Spring WebFlux, PostgreSQL, Redis, Flutter, React
The first chat round: both answers open as cards
Names revealed after a mutual yes
The candle-lit table on the home screen

Summary

Problem
Phones retry on flaky networks and drop their connections, yet a chat message must never be lost or stored twice. Matching runs only inside the evening window, and two workers must never pair the same person at once.
My part
Solo developer: the Java and Spring WebFlux backend, the Flutter app, the React admin panel and the server it all runs on.
Outcome and status
A retried message is stored once, and a phone that reconnects asks for what it missed. The closed beta runs on one rented server, deployed by git push and backed up every night.

An evening in five screens

From the lit candle to a revealed match. Every screen is the real app, rendered with made-up people.

Screen 1 of 5

  1. The evening

    The candle is lit
    1

    The candle is lit

    The evening is open: the user sees the table and the time left, then answers the daily questions.

  2. The match

    Match found
    2

    Match found

    Two candles burn and both pseudonyms are dealt, with shared interests shown.

  3. The chat

    Round 1: Answers
    3

    Round 1: Answers

    Both people's answers open side by side as cards and the user replies to them.

  4. The decision

    Decision
    4

    Decision

    The chat is over; within 30 seconds the user holds the button to say yes.

  5. Reveal
    5

    Reveal

    Both said yes: the pseudonym gives way to a real name and the user writes the first message.

The whole product flow31 screens

Every screen from sign-up to deleting the account, in the order a user meets them.

Screen 1 of 31

  1. Sign up

    Welcome
    1

    Welcome

    The user meets the candle, reads how an evening works and continues with Google.

  2. University email
    2

    University email

    The user enters a .edu.tr address to prove they are a student.

  3. Verification code
    3

    Verification code

    The user types the 6-digit code sent to their university inbox.

  4. Name and birth date
    4

    Name and birth date

    The user adds a first name and birth date; the name stays hidden until a mutual yes.

  5. Photos
    5

    Photos

    The user adds at least two photos that stay hidden during the anonymous chat.

  6. Interests
    6

    Interests

    The user picks interests; up to three appear as hints in the anonymous chat.

  7. The evening

    First evening
    7

    First evening

    On the first visit a note on the table explains what will happen tonight.

  8. The candle is lit
    8

    The candle is lit

    The evening is open: the user sees the table and the time left, then answers the daily questions.

  9. Tonight at nine
    9

    Tonight at nine

    Before the evening the candle is out; the user sees the countdown and can set a reminder.

  10. Special-day table
    10

    Special-day table

    On national and special days the table is decorated for the occasion.

  11. Today's questions
    11

    Today's questions

    The user answers short daily questions that later open card by card in the chat.

  12. Daily deck
    12

    Daily deck

    The day's deck opens with a short ceremony and its cards fan out.

  13. The match

    Setting the table
    13

    Setting the table

    The user takes a seat and waits while someone to sit across is found.

  14. Lighting the candle
    14

    Lighting the candle

    A match lights the candle as the user joins the queue.

  15. Match found
    15

    Match found

    Two candles burn and both pseudonyms are dealt, with shared interests shown.

  16. The chat

    Round 1: Answers
    16

    Round 1: Answers

    Both people's answers open side by side as cards and the user replies to them.

  17. Round 2: Choice
    17

    Round 2: Choice

    The user secretly picks an option; a matching pick sparks.

  18. Round 3: Free chat
    18

    Round 3: Free chat

    Free chat opens; the user writes freely and can play a card from their hand.

  19. Three more minutes
    19

    Three more minutes

    Near the end the other side asks for three more minutes and the user can agree.

  20. The decision

    Decision
    20

    Decision

    The chat is over; within 30 seconds the user holds the button to say yes.

  21. Waiting for the other side
    21

    Waiting for the other side

    The user said yes and waits; both decisions are shown at the same moment.

  22. Reveal
    22

    Reveal

    Both said yes: the pseudonym gives way to a real name and the user writes the first message.

  23. After

    Open chat
    23

    Open chat

    After the reveal the two keep talking by name, with no time limit.

  24. Not this time
    24

    Not this time

    If it is not a mutual yes, the user is told gently and can sit again the next evening.

  25. Evening envelope
    25

    Evening envelope

    The user holds the wax seal until it cracks open.

  26. Envelope rewards
    26

    Envelope rewards

    The envelope reveals new question cards and a card-back skin.

  27. Collection
    27

    Collection

    The user browses collected cards, past evenings and badges.

  28. Your account

    Profile
    28

    Profile

    The user reviews their own profile: about, interests and table cosmetics.

  29. Settings
    29

    Settings

    The user sets match preferences, age range and notifications.

  30. Privacy and account
    30

    Privacy and account

    The user reads the privacy texts, manages consent and finds account deletion.

  31. Delete account
    31

    Delete account

    The user confirms deleting the account; the dialog explains what is removed and when.

How it works

The parts worth reading if you build backends: how the system is laid out, how a message travels, how two people get matched and how the closed beta runs on one server.

The system at a glance

core-api is a modular monolith with fourteen modules, from identity to moderation. chat-gateway only holds WebSocket connections: it checks the Firebase token on connect, listens on Redis and never touches the database. Photos go straight between the phone and Cloudflare R2 through signed URLs.

The system as text

Each line is one box and the boxes its arrows point to.

  • Flutter app (iOS and Android) → Cloudflare (DNS, TLS, proxy), Cloudflare R2 (photos, backups)
  • Admin panel (React, in the browser) → Cloudflare
  • Cloudflare → Caddy (one entry, four hostnames)
  • Caddy → core-api (modular monolith, 14 modules), chat-gateway (sockets only, no database), panel + site (static files)
  • core-api → PostgreSQL 15, Redis 7 (queue, pub/sub, locks), Cloudflare R2, Firebase, FCM, Resend (sign-in, push, email)
  • chat-gateway → Redis 7
The system at a glanceArrows: Flutter app to Cloudflare, Cloudflare R2; Admin panel to Cloudflare; Cloudflare to Caddy; Caddy to core-api, chat-gateway, panel + site; core-api to PostgreSQL 15, Redis 7, Cloudflare R2, Firebase, FCM, Resend; chat-gateway to Redis 7.signed URLsFlutter appiOS and AndroidAdmin panelReact, in the browserCloudflareDNS, TLS, proxyCaddyone entry, four hostnamescore-apimodular monolith, 14 moduleschat-gatewaysockets only, no databasepanel + sitestatic filesPostgreSQL 15Redis 7queue, pub/sub, locksCloudflare R2photos, backupsFirebase, FCM, Resendsign-in, push, email
  1. Flutter appiOS and AndroidAdmin panelReact, in the browser
  2. CloudflareDNS, TLS, proxy
  3. Caddyone entry, four hostnames
  4. core-apimodular monolith, 14 moduleschat-gatewaysockets only, no databasepanel + sitestatic files
  5. PostgreSQL 15Redis 7queue, pub/sub, locksCloudflare R2photos, backupsFirebase, FCM, Resendsign-in, push, email

What happens when you send a message

An explanatory model you step through yourself. It does not talk to the real backend.

Your phonecore-apiPostgresRedisGatewayTheir phonePOST /messagesinsert oncepublish user:{uid}message.createdWebSocket pushGET missed messages

Pick what happens above, or press Next to go through it in order.

  1. Your phone → core-api POST /messages + clientMsgIdYou send a message. The app attaches its own id (clientMsgId), so a retry can never create a duplicate.
  2. core-api → Postgres insert oncecore-api stores it. A second request with the same id is answered from the stored row.
  3. core-api → Redis publish user:{uid}The event goes to a Redis channel named after the receiver, not after a server.
  4. Redis → Gateway message.createdWhichever gateway holds their WebSocket is subscribed to that channel and gets it.
  5. Gateway → Their phone push over WebSocketThe gateway pushes it to their phone. If they are offline, a Firebase notification wakes it.
  6. Their phone → core-api GET missed messagesAfter a reconnect the app asks core-api for what it missed. REST, not the socket, is the source of truth.

How two people get matched

Matching only runs inside the evening window. The queue lives in Redis, and a distributed lock makes sure two workers can never pair the same person at the same moment.

  • The same pair never meets twice.
  • A no is never shown to the other person.

Matching

  1. 5 daily questionsthe same for everyone
  2. Answer 3 to joinunlocks the queue
  3. Evening queueRedis, only inside the window
  4. Two-way checkboth preferences must fit
  5. Lock both peopledistributed lock
  6. match.foundpushed to both phones

Running the closed beta

One rented server runs everything with Docker Compose. Code reaches it by git push, and the database leaves it every night, encrypted.

  • Ports 80 and 443 accept only Cloudflare's IP ranges.
  • The data network is internal; only the API containers can reach the database.
  • Services run as a non-root user, and the app's database role is not a superuser.
  • A restore drill brought a backup back and compared it with the running database.

Deploy

  1. git push prodto a bare repo on the server
  2. post-receive hookchecks out the new commit
  3. docker compose buildimages built on the server
  4. Restartbehind Caddy

Nightly backup

  1. pg_dumpevery night at 04:30
  2. Encryptwith age
  3. Cloudflare R2off the server
  4. Keep 14 daysthen rotate

In numbers

962
backend tests
3,200+
Flutter tests, 256 of them golden images
237
admin panel tests
77
API paths in an OpenAPI contract
13
WebSocket event types
21
database migrations

Decisions I would make again

Next projectSokak EfsaneleriA slide-and-merge puzzle about Istanbul's street animals. Version 1.0 went to App Store review in October 2026.