No photos. No filters. Just words.
Features β’ Installation β’ Usage β’ Architecture β’ Roadmap β’ License
Dating apps broke the deal when they made appearance the first filter. Blindly flips that β your profile is text only, your match is singular, and the conversation is the whole point. Nothing to scroll past.
Blindly is a serverless, text-only blind dating progressive web app built on React 19, Firebase, and Framer Motion. There are no profile photos, no photo uploads, and no image fields anywhere in the data model. Users create a handle, a tagline, pick interest tags from a curated list, and write answers to personal prompts. Discovery is a physics-driven swipe deck. A mutual right-swipe triggers an atomic Firestore transaction that locks both users into a single shared chat. That's the entire product.
Blindly is a single-page React application with no custom backend. Firebase handles every server-side concern: authentication (Email/Password and Phone OTP), real-time data via Firestore onSnapshot, and authorization via server-enforced security rules. The entire app state flows through two React contexts β AuthContext (identity + live userDoc sync) and ThemeContext (AMOLED dark / light toggle). The match engine runs client-side inside a Firestore transaction.
blindly/
βββ public/ # Static assets (SVGs)
βββ assets/ # README diagram assets
βββ src/
β βββ components/
β β βββ onboarding/ # 6 discrete step components
β β βββ SwipeDeck.jsx # Physics swipe + match transaction
β β βββ ProfileCard.jsx # Text-only profile display
β β βββ ChatView.jsx # Real-time message stream
β β βββ ProfileView.jsx # Full profile modal
β β βββ BottomNav.jsx # Tab navigation
β β βββ AuthForm.jsx # Login / sign-up form
β β βββ OnboardingGate.jsx # Redirect if profile incomplete
β β βββ ProtectedRoute.jsx # Redirect if unauthenticated
β βββ context/
β β βββ AuthContext.jsx # onAuthStateChanged + onSnapshot
β β βββ ThemeContext.jsx # Dark / light mode
β βββ layouts/
β β βββ AppLayout.jsx # Shared chrome for app routes
β βββ pages/
β β βββ Home.jsx # Landing / marketing
β β βββ Auth.jsx # Auth page wrapper
β β βββ Onboarding.jsx # 6-step wizard coordinator
β β βββ Dashboard.jsx # Main app: swipe deck + chat
β βββ App.jsx # Router definition + guards
β βββ main.jsx # React root mount
β βββ constants.js # AVAILABLE_TAGS, AVAILABLE_PROMPTS
β βββ firebase.js # SDK init + db/auth exports
β βββ index.css # Design system, CSS custom properties
βββ scripts/
β βββ seed-profiles.mjs # Dev utility: populate test profiles
βββ firestore.rules # Server-side authorization
βββ firebase.json # Firebase project config
βββ vite.config.js
βββ package.json
See the architecture diagram for the full system view.
| Feature | What it actually does |
|---|---|
| π Blind Profiles | No photoURL field exists at schema level. Firestore rules reject image writes. You see handle, tagline, tags, and prompt answers β nothing else. |
| π One Match Rule | currentMatchId on both user documents is set atomically in a single Firestore runTransaction. You cannot swipe while matched. |
| π Physics Swipe Deck | useMotionValue drives card rotation and LIKE/NOPE overlay opacity in real time. AnimatePresence handles the spring-exit on swipe completion. |
| π¬ Real-time Chat | onSnapshot on matches/{matchId}/messages streams messages live. Auto-scroll via useRef. 500-character hard limit enforced on input. |
| π Dual Auth | Firebase Authentication β Email/Password for standard sign-up, Phone OTP for passwordless. Short-lived JWTs refreshed automatically. |
| π AMOLED Theme | Pure #000000 dark mode via CSS custom properties. ThemeContext toggles the data-theme attribute on the root element. |
| π§ 6-Step Onboarding | Discrete components for Handle β BasicInfo β Tagline β Tags β Prompts β Review. State collected in Onboarding.jsx, written to Firestore in a single setDoc on the final step. |
| π Firestore Rules | Profile writes isolated per UID. Swipe subcollection inaccessible to other users. Auth token required for all reads/writes. |
| βΏ Accessibility | All interactive elements carry aria-label / title. Swipe actions have keyboard-accessible Pass/Like buttons. WCAG AA contrast in both themes. |
Blindly has three logical layers. The browser layer is a React 19 SPA bundled by Vite 6, using React Router DOM 7 for client-side routing. Two route guards (ProtectedRoute, OnboardingGate) sit at the router level β an unauthenticated user never reaches the app shell, and a user with an incomplete profile can't skip onboarding by typing a URL. The Firebase SDK bridge is the only communication channel between the browser and the backend; there is no Express server, no REST API, no GraphQL layer.
The Firebase platform layer provides three services. Firebase Auth handles identity: it issues short-lived JWTs that the Firestore SDK attaches to every request. Cloud Firestore handles data: three collections (users, matches, and the messages sub-collection) store all app state. Security Rules act as the authorization layer β the client application never runs privileged code; everything it can and can't do is enforced server-side by Firestore before any data is touched. This means the frontend code is inherently limited: it can't escalate privileges by modifying the SDK.
The match engine runs inside SwipeDeck.jsx. When User A swipes right, it writes a swipe document and then reads whether User B already swiped right on A. If yes, it calls runTransaction β which atomically creates the match document and updates currentMatchId on both user documents. Both users' onSnapshot listeners fire within milliseconds, and AuthContext distributes the new state throughout the component tree without any manual refetch.
The primary match-engine path:
User A swipes right on User B
β
ββ setDoc(users/A/swipes/B, { direction: "right" })
β
ββ getDoc(users/B/swipes/A) ββββ direction != "right" βββΆ next card
β β
β direction == "right"
β β
ββ runTransaction() ββββββββββββββββββββββ
ββ create matches/{A_B} { users:[A,B], status:"active" }
ββ update users/A { currentMatchId: "A_B" }
ββ update users/B { currentMatchId: "A_B" }
β
ββ onSnapshot fires in AuthContext (both users)
β
ββ Dashboard renders match overlay
β
ββ ChatView streams matches/{A_B}/messages
| Requirement | Version | Why |
|---|---|---|
| Node.js | β₯ 18 | ESM support required by Vite 6 |
| npm | β₯ 9 | Workspace-aware lockfile format |
| Firebase project | any | Auth + Firestore must be enabled |
| Firebase CLI | β₯ 13 | For deploying Firestore security rules |
You need a Firebase project with Authentication (Email/Password and Phone providers enabled) and Cloud Firestore (native mode). Storage does not need to be enabled β Blindly doesn't use it.
-
Clone the repository
git clone https://github.com/Kaelith69/blindly.git cd blindly -
Install dependencies β Vite, React, Firebase, Framer Motion, React Router, Lucide icons:
npm install
-
Configure Firebase β Open
src/firebase.jsand replace thefirebaseConfigobject with your project's credentials from the Firebase Console β Project Settings β Your Apps:const firebaseConfig = { apiKey: "YOUR_API_KEY", authDomain: "YOUR_PROJECT.firebaseapp.com", projectId: "YOUR_PROJECT_ID", storageBucket: "YOUR_PROJECT.firebasestorage.app", messagingSenderId: "YOUR_SENDER_ID", appId: "YOUR_APP_ID" }
-
Deploy Firestore security rules β The rules in
firestore.rulesenforce write isolation per user. Deploy them before testing so your dev environment matches production:firebase deploy --only firestore:rules
-
Start the development server:
npm run dev
Vite starts at
http://localhost:5173with HMR.
The swipe deck needs other user profiles to show you. A seed script is included:
node scripts/seed-profiles.mjsThis writes synthetic profiles to your Firestore users collection. Don't run it against production.
- Sign up at
/authβ choose Email/Password or Phone OTP. - Complete onboarding β you'll be redirected to
/onboarding. Work through all 6 steps. Nothing is written to Firestore until you hit submit on the final Review step. - Discover β swipe right (or tap Like) to express interest. Swipe left (or tap Pass) to skip. Candidates who've already been swiped on don't reappear.
- Match β if the other person has already swiped right on you, a match overlay appears immediately. Both users are now locked into this conversation.
- Chat β messages appear in real time on both ends. 500-character limit per message.
- Unmatch β tap the unmatch button in the chat header to clear the match and return to discovery.
Pro tip: If the swipe deck shows "No more profiles," it means either everyone in your Firestore has been swiped or there are no other
onboardingCompleted: trueusers. Run the seed script (node scripts/seed-profiles.mjs) against your dev project to repopulate.
blindly/
βββ π public/
β βββ hero-banner.svg # SVG displayed in browser tab / OG
β βββ sparkle-icon.svg # Favicon
β
βββ πΌ assets/ # README diagram SVGs (not served)
β βββ hero-banner.svg
β βββ architecture.svg
β βββ data-flow.svg
β βββ capabilities.svg
β βββ stats.svg
β
βββ π¦ src/
β βββ components/
β β βββ onboarding/
β β β βββ HandleStep.jsx # Step 1: pick a unique handle
β β β βββ BasicInfoStep.jsx # Step 2: birth year, gender, city
β β β βββ TaglineStep.jsx # Step 3: one-line profile tagline
β β β βββ TagsStep.jsx # Step 4: pick up to N interest tags
β β β βββ PromptsStep.jsx # Step 5: answer personal prompts
β β β βββ ReviewStep.jsx # Step 6: review + single Firestore write
β β βββ SwipeDeck.jsx # Framer Motion swipe + match transaction
β β βββ ProfileCard.jsx # Text-only profile card component
β β βββ ChatView.jsx # Real-time message stream + input
β β βββ ProfileView.jsx # Full profile modal overlay
β β βββ BottomNav.jsx # Bottom tab navigation bar
β β βββ AuthForm.jsx # Combined login / sign-up form
β β βββ OnboardingGate.jsx # Redirect guard: onboardingCompleted?
β β βββ ProtectedRoute.jsx # Redirect guard: authenticated?
β β
β βββ context/
β β βββ AuthContext.jsx # onAuthStateChanged + userDoc onSnapshot
β β βββ ThemeContext.jsx # data-theme attribute toggle
β β
β βββ layouts/
β β βββ AppLayout.jsx # Shared header + Outlet for /app/*
β β
β βββ pages/
β β βββ Home.jsx # Public landing page
β β βββ Auth.jsx # Login / sign-up page
β β βββ Onboarding.jsx # 6-step onboarding coordinator
β β βββ Dashboard.jsx # Main app: deck, chat, match overlay
β β
β βββ App.jsx # BrowserRouter + Routes definition
β βββ main.jsx # ReactDOM.createRoot entry point
β βββ constants.js # AVAILABLE_TAGS, AVAILABLE_PROMPTS arrays
β βββ firebase.js # SDK init, exports db and auth
β βββ index.css # All styles via CSS custom properties
β
βββ βοΈ scripts/
β βββ seed-profiles.mjs # Dev-only: write test user documents
β
βββ firestore.rules # Server-side security rules
βββ firebase.json # Firebase CLI project config
βββ vite.config.js # Vite + React plugin config
βββ package.json
Blindly collects the minimum data required to function:
- No photo storage β The Firestore schema has no image fields. Firebase Storage is not enabled. Appearance is structurally excluded from the system.
- No real name β The profile has a handle, not a legal name.
- No precise location β Approximate city is an optional text field. GPS is never requested.
- Swipe history stays in your own
users/{uid}/swipes/subcollection. Other users can't read it. - Chat messages are plain text only. No files, images, or link previews.
- Auth tokens are short-lived JWTs managed by the Firebase SDK. Passwords are hashed by Firebase (bcrypt). Blindly never touches your raw password.
- No analytics, no ads, no third-party tracking scripts.
See wiki/Privacy.md for the full privacy model and known limitations.
- Text-only profile creation (6-step onboarding)
- Physics swipe deck (Framer Motion)
- Atomic match detection via Firestore transaction
- Real-time chat (onSnapshot)
- Unmatch flow
- Dark / light theme (AMOLED)
- Cloud Function for symmetric unmatch cleanup (delete match doc + both currentMatchIds)
- Report / block users (write-only
reportscollection) - Field-level Firestore rules (lock
trustLevel,onboardingCompleted) - Restrict match/message reads to participants only
- Profile editing after onboarding
- Push notifications on match and new message (Firebase Cloud Messaging)
- Conversation time-boxing (24-hour chat windows)
- Candidate filtering (age range, proximity)
- Account deletion self-service UI
# Production build (output to dist/)
npm run build
# Preview the production build locally
npm run preview
# Deploy to GitHub Pages
npm run deploynpm run deploy runs vite build first (via predeploy), then uses gh-pages to push dist/ to the gh-pages branch of the repository.
Fork the repo, create a feat/* or fix/* branch, and open a PR against main. There's no automated test suite β use the manual testing checklist in wiki/Contributing.md.
Found a vulnerability? Open a GitHub Issue with [Security] in the title, or contact the maintainer directly. Don't post exploits in public. See wiki/Privacy.md for known limitations in the current security posture.
MIT β see LICENSE.
Built with π by Kaelith69