Build Progress · as of 6 June 2026
~59 hours across ~24 sessions
Phases 1–6 complete · Phase 7 (Matches & Chat) in progress — the core loop (discover → match → chat) is live and demoable.
By phase: Phase 3 SwipePage ~5–6h · Phase 4 Mutual Match ~3 sessions · Phase 5 Profile & Settings ~45 min · Phase 6 GAMEON ~7h · Phase 7 Matches & Chat ~34h and counting (22 May–5 June — ~2h a night early on, ~4h the last two nights). Phases 1–2 (Firebase foundation + profile) were built earlier and not separately timed, so the true total is a little higher. 5–6 June added infrastructure + research rather than new app-build phases: the domain lettennis.app was bought and wired, a server-side Cloudflare Access login was stood up over the hub, and an 18-agent competitor + Apple-submission audit was run.
What is LET?
LET is a mobile app that lets tennis players discover, match with, and message
other players at their skill level — the same way a dating app connects people.
You swipe on player profiles. When two players both swipe right on each other,
it's a match. From there, you chat directly inside the app and arrange a time to play.
The core loop
Sign up → Build your profile → Discover players → Swipe right to like → Mutual match → Chat → Book a game
Core Features (MVP)
Email / Password Auth
Secure sign-up and login. User profile created on first sign-in.
Player Profiles
Name, age, suburb, city, club, bio, skill level, match type, availability.
Swipe Discovery
Full-screen player cards with Pass and Like buttons. Swipe gestures supported.
Mutual Match Detection
When both players like each other, a match is created automatically.
Match Modal
A match screen appears when two players like each other. Jump straight into chat from there.
In-App Chat
Direct messaging per match. Arrange the game without leaving the app.
Why LET — The Gap in the Market
Existing tennis apps fall into two categories: structured league platforms (UTR, Tennis Australia's competition system) or court booking tools (CourtReserve, ClubSpark). Neither solves the most common problem casual players face — finding someone to hit with this week. LET sits in the space no one has properly built for: spontaneous, social, player-to-player matchmaking. The Tinder mechanic is deliberately chosen — it's a pattern millions already understand, applied to a sport context for the first time at consumer scale in Australia.
MVP Success Metrics
- ✓ Two test accounts can sign up, match, and chat end-to-end
- ✓ App passes Apple TestFlight review
- ✓ App passes public App Store review
- ✓ 20+ real Melbourne players active in beta
- ✓ At least 5 games arranged through the app in beta period
Monetisation Roadmap
MVP launches free — the priority is users and validation, not revenue. Post-launch options include:
- → LET Premium — unlimited swipes, profile boost, read receipts
- → Club partnerships — featured placement for tennis clubs
- → Court booking affiliate — commission on bookings made post-match
What LET Is Not
Staying focused on the core problem means saying no to a lot of adjacent ideas.
LET is deliberately not any of the following:
A league or competitive ranking platform
A court booking platform
A social media feed or content app
A coaching or training app
A tournament organiser
A stats tracker (wins/losses deferred)
A group organiser (1-on-1 only at launch)
A replacement for UTR or club systems
Tech Stack
LET is built on a modern, low-code foundation that allows rapid iteration
without sacrificing reliability or scalability. Every component is a managed
service — there are no servers to maintain.
Frontend
FlutterFlowVisual app builder on top of Flutter. Produces native iOS code. Handles UI, navigation, Firestore queries, and user actions without custom code.
Auth
Firebase AuthenticationEmail/password sign-in. Each user gets a unique UID used as their document identifier throughout the database.
Database
Cloud FirestoreGoogle's scalable NoSQL document database. Real-time updates, offline support, and fine-grained security rules.
Distribution
Apple App StoreiOS first. Android deferred to post-MVP. TestFlight for beta testing before public release.
Hosting
NetlifyStatic hosting with one-command CLI deploys. Serves the partner playbook and the interactive prototype. Personal plan — $9/month (from June 2026).
Data Model
Four Firestore collections power the entire app:
users
| Field | Type |
| uid | String — Firebase Auth UID |
| display_name | String |
| age | String |
| suburb / city | String |
| club | String (optional) |
| bio | String |
| level | Beginner / Intermediate / Advanced / Elite |
| matchType | Singles / Doubles / Both |
| availability | List — Mon–Sun |
| photo_url | String deferred |
matches
| Field | Type |
| user1Id | String — UID |
| user2Id | String — UID |
| createdAt | Timestamp |
swipes
| Field | Type |
| swiperId | String — UID of swiper |
| swipedId | String — UID of target |
| direction | "left" (pass) or "right" (like) |
| timestamp | Timestamp |
messages (subcollection of matches)
| Field | Type |
| senderId | String — UID |
| text | String |
| timestamp | Timestamp |
Security
Firestore security rules govern every read and write: profiles are editable
only by their owner, swipes are write-once by the swiper, and match records
are readable only by the two matched players. As part of the current safety
phase — before any TestFlight build — conversation messages are being locked
to match participants only, with Firebase App Check and authentication
hardening (email-enumeration protection, password policy) added on top.
Build Phases
LET is built one phase at a time. Each phase is completed and tested before
the next begins — this keeps the codebase stable and the product shippable
at every milestone.
-
✅
Phase 1 — Firebase Foundation
Complete
- Firebase Auth enabled with email/password
- Firestore collections created: users, swipes, matches, messages
- Security rules written and published
- LoginPage confirmed creating user documents on sign-up
-
✅
Phase 2 — Create Profile
Complete
- Full profile form: name, age, suburb, city, club, bio
- Chip selectors: skill level, match type, availability (Mon–Sun)
- Save Profile writes all fields to Firestore users collection
- Navigates to SwipePage on save (replace route)
-
✅
Phase 3 — Discover / Swipe Page
Complete
- SwipeableStack showing live player cards from Firestore, filtered so users never see their own profile
- Pass (✗) and Like (✓) buttons trigger left/right swipe gestures on the stack
- On swipe: writes swipe document to Firestore (swiperId, swipedId, direction, timestamp)
- App State variable currentPlayerId created for upcoming match logic
- Swipes collection schema added to FlutterFlow project
-
✅
Phase 4 — Mutual Match Detection
Complete
- App State variable lastSwipeDirection created — bridges button taps to On Page Swipe event
- Pass (✗) and Like (✓) buttons wired to set direction ("left"/"right") before triggering swipe gesture
- Swipe direction field fixed — was hardcoded "left", now reads from App State variable
- On right-swipe: conditional action queries swipes collection for a reciprocal right-swipe (3 filters: swiperId, swipedId, direction)
- If mutual match found: creates match document in matches collection (user1Id, user2Id, createdAt via Firestore server timestamp)
- Match alert fires: Informational Dialog — "It's a match! 🎾"
- LoginPage routing fixed — Sign In now navigates to SwipePage, Create Account navigates to CreateProfile
- End-to-end test passed 16 May 2026 — two accounts confirmed mutual match detection, Firestore document created, alert dialog fired correctly
-
✅
Phase 5 — Profile & Settings
Complete
- ProfilePage created by duplicating CreateProfile — instant layout and visual consistency
- Firestore backend query wired to page Column: users collection → Single Document → uid Equal To Authenticated User ID
- All text fields pre-populated from Firestore: display_name, age, suburb, city, club, bio
- Skill Level, Match Type, and Availability ChoiceChips pre-populated via Initial Option from users Document
- Save Changes button — Update Document action mapping 9 fields back to Firestore
- Log Out button — Firebase Auth Log Out → Navigate to LoginPage (Replace Route)
- Navigation wired to person icon in SwipePage bottom nav bar
- End-to-end tested: data loads, save updates Firestore, logout returns to LoginPage
-
✅
Phase 6 — GAMEON Page
Complete
- Full-screen GAMEON page (not a bottom-sheet) — split-screen photo layout with both players' faces filling the screen
- Two profile photos side by side, each taking half the screen width and full screen height
- Dark rounded card overlaid at the bottom centre with subtle transparency
- "GAME ON" large bold white headline + "You and [Name] are ready to hit the court" subtitle (sport-first, deliberately avoiding Tinder-style "MATCHED!" language)
- "Plan a Hit" primary button (filled, lime green) — stubbed for Phase 7 with snackbar, will navigate to chat thread once chat is built
- "Find More Players" plain text link below the button — Navigate Back to SwipePage
- Page parameters: matchedUserName (String), matchedUserPhoto (ImagePath, required), matchedUserId (String)
- SwipePage wired: replace "It's a match!" informational dialog with Navigate To GAMEON action, passing swiped user's display_name, photo_url and uid
GAME ON
You and Alex are ready
to hit the court
Plan a Hit
Find More Players
Design Mockup
GAMEON Page
Full-screen takeover that fires on a mutual right-swipe. Replaces the placeholder "It's a match!" alert dialog from Phase 4 with a branded experience.
Split-screen photos of both players, sport-first headline ("GAME ON" not "MATCHED!"), a single primary CTA ("Plan a Hit") that opens chat in Phase 7, and an escape-hatch link ("Find More Players") that returns the user to SwipePage. Photo, name, and matched user ID are passed in as page parameters from SwipePage.
-
🏗️
Phase 7 — Matches & Chat
In Progress
Phase 7 — Progress Report (updated 4 June 2026)
Matchmaking foundations (22–25 May): Three days of prep work shipped 9 binding/configuration fixes across the swipe-action chain. The matchmaking flow is now fully working end-to-end — verified by a two-account live test where both accounts mutual-matched and the GAMEON page loaded cleanly on both sides with correct names, photos, and no security-rules errors.
MatchesListPage complete (25–29 May): Page scaffolded via FlutterFlow's Designer AI, page-level Firestore Backend Query wired (OR filter: user1Id == auth.uid OR user2Id == auth.uid), and the eight static placeholder rows replaced with a single MatchRow inside a ListView generated from the query. Each row now resolves "the other user" (the participant who isn't you) and binds the displayed name, photo, and level to that user's real profile. The matches list is functionally done.
ChatThread complete + verified (29–31 May): bound message timestamps and sent/received bubble alignment, fully wired the send-message button (writes sender + text + a Firestore server timestamp to the match's messages subcollection, then clears the input), wired navigation from a match row into its chat, and built the chat header (the other user's real name + photo). The whole chat experience was then verified end-to-end in a two-account live test — both players saw the correct person in the header, and messages sent and arrived correctly in both directions. The core app loop — discover → match → chat — is now functionally complete and demoable.
GAMEON link + Report user shipped (1 June): the GAMEON "Plan a Hit" button now opens the right chat thread — and building it surfaced a genuine FlutterFlow bug (a freshly-created match reference can arrive empty through navigation and crash the destination screen). Fixed by routing the reference through a stable app-wide value; verified in a two-account live test — both sides land in the correct conversation with no crash. Report user is also live and verified end-to-end: an info button in the chat header opens a confirmation dialog, and confirming writes a report to a locked-down Firestore reports collection (with a link straight to the reported conversation for review). That's LET's first Apple Guideline 1.2 safety requirement, done. Still to build in Phase 7: a shared Report / Unmatch / Block safety menu, a report reason-picker, a security-hardening pass folded in before Block (participant-only Firestore rules on messages + matches, Firebase App Check, auth hardening), unmatch, block, an account-deletion screen (Apple-mandatory), and an 18+ age gate on signup. Honest remaining estimate: ~9–15 focused hours; block-user, account-deletion, and the security pass are the heavy items. Update 4 June: the SafetyMenu — the shared Report / Unmatch / Block sheet — is now wired and verified working: the chat-header info button opens it as a bottom sheet (passing the other player, the match, and their name), and the Report flow was relocated onto the menu's Report row and re-wired to the menu's parameters. Confirmed in a live test — info button → menu → Report → confirmation → report saved. Update (4 June, later): the report doc was re-verified writing correctly after the menu move; work then began on the report reason-picker (a dedicated reason sheet, with the first reason wired) but was parked mid-build to protect the working report, and a first slice of the security pass landed — an auth password policy (enforced complexity + minimum length). Next: finish the reason-picker, then the main security-hardening pass (participant-only Firestore rules + App Check), then Unmatch and Block.
- Prep work — done 22–24 May 2026: A blocking bug in the matchmaking flow was discovered before Phase 7 build could begin. Mutual swipes weren't creating
matches documents in Firestore, which would have made the chat feature impossible to test. Root cause: four separate field bindings in the SwipePage swipe-action chain were reading an unreliable uid field on user documents instead of the document's own reference ID. All four bindings rebound to use Document Reference ID; mutual match flow now verified end-to-end with two real test accounts. Junk data cleared, clean slate for Phase 7 build.
- Action 4 navigation fix — 25 May 2026: The Navigate-to-GAMEON action's
matchedUserId parameter had the same broken pattern as the prep-work bindings. Rebound to Document Reference ID. Two-account live test confirmed mutual match → GAMEON now passes the correct matched user ID, ready for chat hand-off.
- MatchesListPage scaffold + query — 25 May 2026: Generated via FlutterFlow's Designer AI (page, reusable MatchRow component, sport-first subtitle, placeholder rows + empty state). Page-level Backend Query also wired on the matches collection with an OR filter (user1Id == auth.uid OR user2Id == auth.uid). The page now has its data source; rows still need to be bound to dynamic results.
- MatchesListPage dynamic rows — 28 May 2026: Replaced the eight static MatchRow placeholders with a single MatchRow inside a ListView, wired to the matches query via "Generate Children From Variable" (loop variable:
matchDoc). The row's name parameter is bound to a field on the loop variable, confirming real Firestore data flows row-by-row.
- MatchesListPage — other-user resolution complete (29 May 2026): each row now resolves the participant who isn't the current user and binds the row's name, photo, and level to that user's real profile (conditional value + a nested users Backend Query inside MatchRow). The matches list is functionally done.
- ChatThread — message list live (29 May 2026): messages render dynamically from the match's
messages subcollection via FlutterFlow's Generate Dynamic Children (loop variable messageDoc), with each bubble's text bound to its own message. Incoming messages display in real time.
- ChatThread — timestamps, bubbles & send button (29 May eve): each message's timestamp renders in 12-hour time, sent vs received bubbles align by comparing the message's sender to the logged-in user, and the send button is fully wired (writes sender + text + a server timestamp to the messages subcollection, then clears the input). The AI-generated "input" turned out to be a non-interactive box — a real text field had to be added before send could work.
- ChatThread — match-row navigation (29 May eve): tapping a match opens that match's chat thread, passing the match reference through so the right conversation loads.
- ChatThread — chat header complete + verified (31 May 2026): the header shows the other user's real name and photo, resolved the same way the match rows are (a conditional picks the participant who isn't you, feeding a single-document users query). Verified in a two-account live test — both sides showed the correct person, and messages sent/received correctly in both directions.
- GAMEON "Plan a Hit" → chat (1 June 2026): the button now opens the matched conversation. The match reference is captured when the match is created, carried through a stable app-wide value (working around a FlutterFlow bug where a fresh reference goes empty through navigation and crashes the screen), and passed into the chat. Two-account live test passed — both sides open the correct thread, no crash.
- Report user — done & verified (1 June 2026): an info button in the chat header opens a "Report this player?" confirmation; confirming writes a report (who, whom, reason, time, plus a link to the conversation) to a locked-down
reports Firestore collection, then shows a thank-you message. Verified end-to-end with a real report landing in the database. Satisfies Apple Guideline 1.2's report-objectionable-content requirement. Updated 3 June: the report now also stores the reported player's display name, so the moderation queue reads as real names instead of anonymous IDs — the first piece of the report reason-picker upgrade (a Harassment / Spam / Safety / Other reason menu lands next).
- Safety menu — built & wired (4 June 2026): the chat-header info button now opens a shared Report / Unmatch / Block menu (a bottom sheet), so all three safety actions hang off one structure instead of being built separately. The Report flow was moved onto the menu's Report row and re-wired to the menu's parameters; verified in a live test — the info button opens the menu and Report runs end-to-end. Unmatch and Block are the next two rows to wire.
- Report reason-picker — in progress (started 4 June): let the reporter choose a reason (Harassment / Inappropriate / Spam / Safety / Other) instead of the current hardcoded value. A dedicated reason sheet has been cloned and the first reason wired; the remaining reasons and the hand-off from the safety menu are still to do
- Security hardening (before Block) — partly started: the auth password policy is now enabled (4 June) — enforced complexity (upper, lower, number, symbol) and an 8-character minimum. Still to do, as a focused session: tighten Firestore rules so conversations are readable and writable only by the two matched players (today any signed-in user could reach a conversation through the data layer), enable Firebase App Check, and switch on email-enumeration protection — then re-verify chat. Done before Block so the block is enforced by the rules, not just hidden in the UI
- Unmatch — either player can remove a match at any time
- Block user — removes the match, prevents future matching, hides profiles from each other; enforced server-side by the hardened rules
- Account deletion — in-app settings screen that permanently deletes Firestore profile and Firebase Auth account (Apple mandatory since 2022 — guaranteed rejection without it)
- Date of birth field — added to CreateProfile to enforce 18+ age gate at signup
Ways of Working — lessons banked in Phase 7
Phase 7 has run to ~26 hours so far — about 2 hours a night, nearly every night from 22 May to 4 June — against a 3-hour estimate. The gap is scope that surfaced during the build, not slipping pace: the original guess never priced in Apple's mandatory safety features or the wiring of AI-generated UI (which hands over polished layout but zero working logic). The overrun produced five working rules now applied to every phase:
- AI-generated screens are layout only. FlutterFlow's AI generation produces visual scaffolding — placeholder text, fake buttons, sample data. Connecting it to live data and actions is the real work, now priced as the bulk of each build rather than an afterthought.
- Audit before building. Confirm what's actually wired versus placeholder (real input field or a styled box? real query or sample data?) at the start of a phase. "Looks finished" never means "is finished."
- Finish, don't defer. Stuck problems get solved in the same session rather than pushed to "later" — quiet deferral is what inflates scope.
- Verify the path before executing it. Check an approach actually works before building on it, so effort isn't thrown away.
- Verify it saved before building on it. New or duplicated pieces are confirmed as actually persisted before more work is stacked on them — and nothing is recorded as "done" until it's verified. A status should reflect what's real, not what's hoped.
-
🛡️
Phase 7.5 — Content Moderation (Mini Phase)
Upcoming
- Required for Apple App Store approval — Review Guideline 1.2 mandates filtering for UGC apps. Guaranteed rejection without it
- Client-side banned-word filter via FlutterFlow custom function — checks input on save, shows error toast if matched, blocks the write
- Applies to three input points: bio field, display_name field, chat messages (Phase 7)
- Banned-word list covers slurs, swears, threats, sexual content — maintained as a constant in the custom function
- Optional upgrade post-launch: Cloud Function + OpenAI Moderation API for smarter context-aware filtering
- Must ship before TestFlight submission — not after
-
🔍
Phase 8 — Polish, QA & Edge Cases
Upcoming
- Profile photo upload — built in this phase (deferred from CreateProfile). MANDATORY at signup — no photo, no save. Photoless profiles read as bot/catfish accounts and break the GAMEON split-screen layout
- In-app cropping UI with a circular face guide overlay so users centre their face. Required because the GAMEON page split-screen layout breaks visually if the face is off-centre (e.g. one player's face vs. another player's leg)
- Photo upload guidelines — clear pre-upload prompt: "Upload a clear photo with your face centred. Photos that don't show your face clearly may be rejected"
- Existing test users without photos — forced to upload on next login via a one-time gate screen before they can access SwipePage
- Optional: Firebase ML Kit face detection on upload — auto-reject photos with no detectable face, or auto-crop to the detected face bounds
- Empty state: no players left to swipe — friendly screen encouraging users to check back later
- Empty state: no matches yet — encouraging prompt so the screen isn't blank
- Empty state: no messages in a chat — conversation starter prompt
- Loading states — skeleton screens while Firestore data loads (prevents blank flashes)
- Error handling — network errors, Firebase outages, graceful fallbacks with user-facing messages
- Full device testing — iPhone SE (small screen) through iPhone Pro Max (large screen)
- Edge cases — duplicate match prevention, simultaneous swipe handling, deleted account handling
- No placeholder or test content in the submission build — Apple will reject dummy data
- Carried-forward bug list (updated 24 May 2026): SwipePage filter not excluding current user (logged-in user can swipe their own card, creating self-match docs); X (Pass) button still triggers mutual match — lastSwipeDirection App State not updating to "left" before the swipe action fires; Authenticated User Photo URL on Firebase Auth side stays empty — needs to sync from Firestore photo_url on save; Left/right photo binding order on GAMEON page is the reverse of the original intent (left should be current user, currently rendering as matched user); Firestore Security Rules error toast on GAMEON page load — likely a Backend Query on matches collection without proper filters; Action 4 (Navigate to GAMEON) matchedUserId parameter likely still uses the old field-reading pattern and needs the same Reference ID fix applied during Phase 7 prep.
- Location autocomplete (noticed during testing, 5 June 2026): the City and Suburb fields need Google Places Autocomplete so users select a real location from a dropdown — no typos or made-up places. It also returns latitude/longitude, the foundation for the distance filter (deferred to post-launch — see Phase 13).
- Input quality & UX: spell-check on the bio field (red-underline on misspellings); profile photo upload confirmed not working in testing and must be fixed; show the password requirements (8+ characters with an uppercase, lowercase, number and symbol) on the Create Account page so sign-ups don't fail blind.
-
🎨
Phase 9 — Brand & Design System
Upcoming
- Design direction (set 4 June): rebuild the app's UI to match the interactive prototype's look — the approved visual language (clean white cards, brand purple with a lime accent, rounded modern iOS style, sport-first). The prototype is the reference the FlutterFlow screens get restyled toward.
- Look unique, not stock (added 5 June): the "generic low-code app" tell is default fonts, default components, and default spacing — not the colour. Explore distinctive colour schemes and custom fonts (not the platform defaults) and style custom components, so LET reads as a real brand, not a template. Colour (purple + lime) is already ownable; uniqueness is won on type and components.
- Logo suite — wordmark, icon mark, lockup variations (light/dark)
- Colour system — primary, secondary, semantic (success/error/neutral) with hex, RGB, and accessibility contrast ratios
- Typography — typeface selection, scale (H1–body–caption), weights, line heights
- Tone of voice — brand personality, vocabulary, writing principles, do/don't examples
- UI component styles — buttons, cards, chips, inputs, modals applied to FlutterFlow theme
- App icon & splash screen — all required iOS sizes
- Brand guidelines document — single source of truth for all future creative
-
⚖️
Phase 10 — Legal & Compliance
Upcoming
- Privacy Policy — GDPR/Australian Privacy Act compliant, hosted publicly (required for App Store)
- Terms of Service & User Agreement — covers acceptable use, match disputes, content standards
- LET trademark application — register with IP Australia (word mark + logo mark)
- Apple Developer Program — enrol at developer.apple.com ($149 AUD/yr subscription)
- Data retention policy — how long swipe/match/message data is stored
- Age gating — 18+ confirmed. Enforce via date of birth field on sign-up
- Moderation process — documented response plan for user reports (Apple requires this)
-
🌐
Phase 10.5 — Website & Domain (Mini Phase)
Upcoming
- Purchase domain — lettennis.app or lettennis.com.au via Cloudflare Registrar (~$10/yr at-cost pricing)
- Netlify hosting — static hosting with auto-deploy from terminal (Personal plan, $9/mo, subscribed June 2026)
- Point domain DNS to Netlify (one-time, ~10 min)
- Build site as plain HTML/CSS — same workflow as the playbook, deployed via "netlify deploy --prod"
- Home page — hero, feature overview, screenshots, "Join the waitlist" email capture form
- /playbook — replaces tiiny.site as the canonical playbook URL
- /privacy — privacy policy from Phase 10 (App Store mandatory)
- /terms — terms of service from Phase 10 (App Store mandatory)
- /support — contact / FAQ page (App Store mandatory — Support URL required field)
- /press — brand assets and press kit for media outreach
- Knocks out 3 App Store submission requirements at once: privacy URL, terms URL, support URL
- Estimated build time: 6–10 hours across a focused session, post Phase 9 (brand) and Phase 10 (legal copy)
-
📣
Phase 11 — Marketing & Pre-Launch
Upcoming
- Landing page — lettennis.com.au (or .app), email capture for waitlist
- Social channels — Instagram, TikTok; content strategy before and around launch
- Beta user recruitment — 20–50 real Melbourne tennis players via local tennis clubs, Facebook tennis groups, Meetup, and direct outreach to parks and rec centres
- App Store optimisation (ASO) — keyword research, listing copy, category selection
- Press & partnerships — local tennis clubs, Melbourne tennis communities, tennis media
- Launch plan — soft launch strategy, timing, and post-launch feedback loop
-
🚀
Phase 12 — App Store Submission
Upcoming
- Demo accounts — two pre-seeded test accounts with complete profiles, a mutual match, and messages exchanged. Credentials submitted in App Review Notes so Apple's reviewer can test the full flow
- App Review Notes — written explanation of what LET does, step-by-step instructions for the reviewer to test swipe → match → chat
- Support URL — live support page or dedicated email (e.g. help@lettennis.app) linked in App Store listing (required field — cannot submit without it)
- Export compliance — declare encryption usage (Firebase uses HTTPS/TLS); select "Yes, exempt" under US export law in App Store Connect
- Privacy Nutrition Labels — declare all data collected (name, email, usage data, identifiers) in App Store Connect
- App Store listing — category (Social Networking), age rating (18+), subtitle, keywords, description, promotional text
- App Store screenshots — required sizes: 6.9" and 6.5" iPhone; all using finalised brand assets
- TestFlight beta with recruited Melbourne players — gather feedback, fix critical issues before public release
- Apple review submission — respond promptly to any reviewer questions; typical review time 24–48hrs
- Public App Store release
-
📍
Phase 13 — Post-Launch Roadmap
After Launch
- Location-based discovery (distance / km radius) — high priority post-launch. Let players find others within a chosen radius of them — the heart of "find a local game fast." Requires: City/Suburb Places Autocomplete to capture latitude/longitude, coordinates stored per profile, and a geo radius query (Firestore has no native radius query — needs geohashing or a bounding-box approach). For safety, show approximate distance only ("~5 km away"), never precise coordinates. A multi-session feature, deliberately deferred to post-launch so it doesn't block MVP submission — but central to the long-term product.
- LET Premium — unlimited swipes, profile boost, read receipts (from the monetisation roadmap)
- Android release — expand beyond iOS once the iOS app is stable
Development Principles
LET is built with a clear philosophy: ship a focused, working product fast —
then iterate. These principles govern every decision made during development,
from platform choice to feature scope.
One platform, done right
We build iOS first on FlutterFlow with Firebase as the backend.
No platform sprawl, no framework switching mid-build. This constraint
keeps the team fast, the codebase simple, and the product polished
before we expand to Android.
Simplicity is a feature
When two approaches solve the same problem, we pick the simpler one.
We avoid custom code unless FlutterFlow has no built-in solution.
A maintainable app is a shippable app.
One phase at a time
We complete and test each phase before starting the next. There is
no "we'll fix it later" — if something breaks, it gets fixed before
moving forward. This keeps the product stable at every milestone.
Scope discipline
Features not in the PRD do not get built. Deferred features stay
deferred until the MVP is complete and tested. Every addition must
be explicitly approved against the product vision.
Data integrity & security first
All Firestore writes are authenticated and scoped to the user's UID.
Security rules are written and published before any data is exposed.
No sensitive data is stored in plain text. Users can never see or
modify records they don't own.
Functionality before aesthetics
Branding and visual polish happen last. We build flows that work
correctly, then make them look premium. This avoids the common trap
of a beautiful app that doesn't function reliably.
Documented as we go
The PRD, Architecture doc, Build Plan, and AI Rules are living
documents — updated whenever significant progress is made. Any
AI-assisted session begins by referencing these documents to maintain
continuity and context.
Real data only
No placeholder or dummy data appears in production flows. Every
piece of content shown to a user comes from a real Firestore document.
This ensures the product is always representative of the real experience.
Live prototype:
lettennis.tiiny.site
·
Target market: Melbourne, Australia
·
MVP target: App Store submission, two accounts confirmed end-to-end
Built secure, not secured later
Security is treated as a feature, not a final checklist. LET's defence sits on the
backend, where it cannot be bypassed: Google's Firestore Security Rules decide every
read and write and are enforced on Google's servers — never in the app, where a
determined user could otherwise reach around the interface. Below is what protects
the app as it is built, the plan for users' information, and the plan for money once
LET monetises.
1 · Securing the app as we build
Participant-only conversations
Messages and match records are locked so they are readable and writable only by the two matched players — enforced by Firestore Security Rules on Google's servers, not the app interface, so no one can reach another player's conversation through the data layer. (Rules published and live as of 5 June; final end-to-end chat re-test pending a swipe-flow fix.)
Authentication hardening
Enforced password complexity and minimum length are live, with email-enumeration protection being switched on so the login flow cannot be used to discover who holds an account.
Firebase App Check
Enabled at the TestFlight stage to block bots and scripts from hitting the backend directly, so only the genuine LET app can reach the database.
Safety & moderation
In-app reporting of objectionable content is live and reviewed through a moderation queue; block and unmatch are being added alongside it — meeting Apple's user-generated-content requirements.
No secrets in the app
No private keys ship inside the app. Sensitive operations run on managed Google infrastructure, with data encrypted in transit and at rest by default.
Gated pre-launch materials
This playbook and the live demo are password-gated, marked confidential, and have their access rotated, so the build and its IP are not openly accessible.
2 · Protecting users' information
- Data minimisation — LET collects only what the product needs to work. Personal contact details such as email and phone are kept out of the broadly-readable profile data, so a player can be discovered without exposing private information.
- Privacy by design — a clear privacy policy and Apple App Privacy labels ship at launch, aligned with the Australian Privacy Act and the Australian Privacy Principles (APPs).
- User data rights — in-app account deletion permanently removes a user's profile and login (Apple-mandatory), giving users real control over their data.
- Breach readiness — built on managed Google infrastructure to shrink the attack surface, with awareness of Australia's Notifiable Data Breaches obligations.
3 · Protecting money (when LET monetises)
LET launches free. When Premium subscriptions and court-booking commission arrive, payment security is designed in from the start:
- No card data ever touches LET — payments run through PCI-compliant processors (Apple In-App Purchase for Premium; an established provider such as Stripe for bookings), so LET never stores or handles raw card details.
- Server-side validation — every purchase and subscription state is verified on the backend, never trusted from the app, to prevent spoofed or replayed transactions.
- Fraud & abuse monitoring — sensitive money logic runs in secure Cloud Functions with monitoring, keeping financial operations off the device and auditable.
Protecting the idea
The moat is execution, but the plan protects the name and the work too: trademarking LET and the logo (IP Australia), a confidentiality / NDA step before the build is shown to partners, and a written IP-assignment from any collaborator who touches the code.
Phase 3 — Build Report
SwipePage · 10 May 2026
Time to Complete
~5–6 hours (original estimate: 30 minutes)
What the App Can Now Do
🃏
Real player cards
Live profiles pulled from Firestore, displayed in a swipeable card stack
👆
Two ways to swipe
Users can manually swipe cards or tap the X / ✓ buttons — both work identically
🗄️
Swipe recording
Every pass or like is written to Firestore with correct player IDs, direction and timestamp
🎯
Ready for match detection
Data structure in place to detect mutual likes in Phase 4
Key Challenges Encountered
⚠️
AI Generation Failures
FlutterFlow's AI ignored critical widget requirements 3 times despite explicit instructions, requiring full manual rebuild
⚠️
Undocumented Platform Constraint
SwipeableStack widget only accepts a single child — discovered mid-build, required architectural pivot for button placement
⚠️
Data Access Architecture
Buttons placed outside the SwipeableStack couldn't read current player data — solved by wiring buttons to trigger swipe gestures instead of writing to Firestore directly
Key Insight
These challenges are typical of first-time builds on any platform. The architectural patterns discovered in Phase 3 are now understood and documented — Phase 4 onwards will move significantly faster.
Phase 4 — Build Report
Mutual Match Detection · 11 May 2026
Time to Complete
~3 sessions across 2 days (completed & tested 16 May 2026)
What Was Built
🔀
Direction tracking fixed
Swipe direction was hardcoded as "left". Now correctly reads "left" or "right" from App State before writing to Firestore
🔍
Mutual match query
On every right-swipe, Firestore is queried for a reciprocal right-swipe using 3 filters (swiperId, swipedId, direction)
💾
Match document creation
When a mutual match is found, a document is written to the matches collection with user1Id, user2Id, and server timestamp
🎾
Match alert
"It's a match! 🎾" alert dialog fires when mutual match is detected. Placeholder for the full match modal in Phase 5
Key Challenges Encountered
⚠️
Doc Reference vs String type mismatch
matches collection fields were created as Doc Reference type, causing User ID to be greyed out. Fixed by deleting and recreating as String fields
⚠️
Action Flow Editor — no "Insert Before"
FlutterFlow has no way to insert an action before an existing one. Required deleting and rebuilding button action flows in the correct order
⚠️
Routing sent users to old test page
"Navigate Automatically" on login buttons was routing to an old HomePage. Fixed by disabling auto-nav and manually routing Sign In → SwipePage and Create Account → CreateProfile
Phase Complete — Test Results
End-to-end test passed 16 May 2026. Two accounts (Safari + Safari Private) both swiped right on each other. Match document confirmed created in Firestore with correct user1Id, user2Id, and server timestamp. "It's a match! 🎾" alert dialog fired on both sides. Phase 4 is fully complete.
Phase 5 — Build Report
Profile & Settings Page · 17 May 2026
Time to Complete
~45 minutes (original estimate: 30 minutes — dinner break included)
What Was Built & Tested
📋
ProfilePage created
Duplicated from CreateProfile — saved layout time and guaranteed visual consistency across the app
🔍
Firestore backend query
Users collection → Single Document → uid Equal To Authenticated User ID. Wired to the page Column
✏️
All fields pre-populated
display_name, age, suburb, city, club, bio — each TextField Initial Value set from users Document
🎯
Chips pre-populated
Skill Level, Match Type, and Availability ChoiceChips all load saved values via Initial Option from users Document
💾
Save Changes button
Update Document action — maps 9 fields (display_name, age, suburb, city, club, bio, level, matchType, availability) to their widget values
🚪
Log Out button
Firebase Auth Log Out → Navigate to LoginPage (Replace Route). Tested — lands correctly on login screen and clears session
🧭
Navigation wired
Person icon in SwipePage bottom nav bar wired to Navigate To ProfilePage — one tap from anywhere in the app
✅
End-to-end tested
Logged in → navigated to ProfilePage → confirmed data loaded → tapped Log Out → confirmed landing on LoginPage
Approach That Saved Time
Rather than generating a new page from scratch, Phase 5 was built by duplicating CreateProfile. The layout, form fields, chip selectors, and visual style were inherited instantly — the only work was rewiring the data direction (read instead of write) and swapping Create Document for Update Document. Phase 6 (Match Page) is next.
Phase 6 — Build Report
GAMEON Match Page · 17–18 May 2026
Time to Complete
~7 hours across 2 sessions (3h yesterday · 4h tonight)
What Was Built & Tested
🤖
GAMEON page AI-generated
FlutterFlow AI gen produced 95% of the layout from a single text prompt — split-screen photos, dark overlay card, headline, buttons
📝
Copy locked sport-first
"GAME ON", "Plan a Hit", "Find More Players" — deliberately not "MATCHED!" or "Keep Swiping" to avoid dating-app tone
🔗
Three page parameters wired
matchedUserName, matchedUserPhoto, matchedUserId — passed from SwipePage via Swipable Stack Current Element
🖼️
Split-screen photos working
Right = matched player's photo. Left = current user's photo via backend query on users collection
🧬
Dynamic subtitle
"You and [matchedUserName] are ready to hit the court" — Combine Text widget mixes static text with the live name variable
🎾
Plan a Hit button
Stubbed for Phase 7 with "Coming soon" snackbar. Will navigate to chat thread once chat is built
↩️
Find More Players
Plain text link → Navigate Back. Returns user to SwipePage exactly where they left off
🔁
SwipePage rewired
Replaced the Phase 4 "It's a match!" alert dialog with Navigate To GAMEON action — passes the swiped user's display_name, photo_url, and uid
Key Challenges Encountered
⚠️
FlutterFlow web preview blocks external images
Pravatar, Firebase Storage, and Unsplash URLs all hit CORS errors in the web preview. Solved by hosting photos on Imgur. Photos render correctly on real iOS devices regardless
⚠️
Page parameter type mismatch
matchedUserPhoto created as String type was greyed out in the Image Path picker. Fixed by changing parameter type from String to ImagePath
⚠️
Browser session caching for two-account testing
Safari Private window kept logging both windows into the same Firebase account. Solved by using two different browsers (Safari + Chrome) for genuinely isolated sessions
⚠️
Pass parameter dropdown overrides previous bindings
Switching the parameter dropdown loses the previous binding rather than adding a new one. Each parameter needs its own + Pass click — discovered the hard way after losing two bindings
⚠️
Authenticated User Photo URL is Firebase Auth, not Firestore
Initial left-photo binding pulled from Firebase Auth's photoURL field (empty) instead of the Firestore users doc photo_url. Solved by adding a backend query on GAMEON to fetch the current user's Firestore document
Phase Complete — Test Results
End-to-end test passed 18 May 2026 with two real Firebase accounts (Olcaraz + Djohn). Both users swiped right on each other. GAMEON page fired correctly on both sides with real names and real Imgur-hosted photos rendering in the split-screen layout. Mutual match detection, navigation, parameter passing, and dynamic UI all confirmed working.
Phase 7 — Build Report In Progress
Matches & Chat · 22 May–4 June 2026 (and ongoing)
Time Invested So Far
~26 hours, ~2h a night (nearly every night, 22 May–4 June · prep 22–24 May · AI scaffold + live test 25 May · row wiring 28 May · matches list + chat list 29 May · send, timestamps & header 29–31 May · GAMEON link + report user 1 June · reported-name + security planning 3 June · safety menu built & wired 4 June)
What's Shipped So Far
🛠️
9 binding fixes in the swipe chain
Discovered and fixed a deep bug across four action steps that was silently writing empty user IDs. Without it, mutual matches couldn't be detected at all
✅
Mutual match verified live
Two real accounts on two browsers both swiped right. GAMEON fired on both sides simultaneously with correct names, photos, and no errors. Firestore confirmed a clean matches doc
🧭
Navigate-to-GAMEON parameter fixed
The matchedUserId page parameter had the same broken pattern as the swipe chain. Rebound to Document Reference ID — now GAMEON receives a stable, populated match ID
🤖
MatchesListPage AI-scaffolded
FlutterFlow Designer AI generated the full layout — title, subtitle, eight placeholder rows, reusable MatchRow component, empty state — from a single short prompt
🔍
Backend Query on matches
Page-level Firestore query with an OR filter: matches where user1Id == auth.uid OR user2Id == auth.uid. Only the current user's matches load — no client-side filtering
🔁
Dynamic row rendering
Eight static placeholder rows replaced with one MatchRow wrapped in a ListView's "Generate Children From Variable". Each row now corresponds to a real match document from Firestore
🔗
First parameter bound dynamically
MatchRow's name parameter is now bound to a field on the loop variable — confirming the entire data pipeline (query → list → row) is wired and live
🧹
Junk data cleared
Stale swipes and self-match docs from before the binding fix were wiped from Firestore. Clean slate for the rest of Phase 7 testing
👥
Matches list shows real partners
Each row resolves the other player in the match and shows their real name, photo, and level — pulled live from their profile. The matches list is functionally complete
💬
Chat messages render live
The chat screen now pulls each match's message history and renders one bubble per message, live from Firestore. Sending, timestamps, and bubble alignment are next
Key Challenges Encountered
⚠️
A field that should never be empty, sometimes was
Several action steps were pulling a user's uid from a Firestore field that wasn't always populated — silently producing empty values that broke the entire mutual-match flow. The fix was switching every reference to use the document's own reference ID, which is guaranteed to exist
⚠️
Two-account testing leaked sessions
Safari plus Safari Private shared the same auth session despite "Private" mode, making it impossible to test as two distinct users. Switching the second account to Firefox gave properly isolated sessions
⚠️
FlutterFlow's dynamic-children option is hidden
The setting that turns a static list into a dynamic one isn't on the widget by default — it appears only after wrapping the row inside a ListView. Worth knowing for every future list-based page
In Progress — Next Up
The core loop — discover → match → chat — is complete and demoable: the send button, timestamps, sent/received alignment, the chat header, match-row and GAMEON "Plan a Hit" navigation, and the first safety feature (report user) are all built and verified in two-account live tests. Next is the rest of the safety layer, built safest → fragile: a shared Report / Unmatch / Block menu, a report reason-picker, then a security-hardening pass (participant-only Firestore rules, App Check, auth hardening) folded in before Block so the block is rule-enforced, then unmatch, block, an account-deletion screen (Apple-mandatory before any App Store submission), and an 18+ age gate at signup. Realistic remaining time: ~9–15 focused hours.
Your Input Needed
The following items need a decision or action before LET can launch. None of these
are technical — they sit in your lane. Each one is explained plainly below.
Question 1
Which tagline do you prefer?
LET needs a short line that sits under the logo — on the app, the website, and any marketing. We have five options. They range from action-focused to name-first. Which one feels right to you?
A
LET Tennis — Find a game fast
Current favourite
B
LET — Find a game fast
C
LET Tennis — Tennis partners, fast
D
LET — Find your next game
E
LET — Play more tennis
Question 2
Registering the LET trademark
Before LET grows any following, we should register the name and logo as a trademark through IP Australia (the government body that handles this). This protects us from someone else claiming the name later.
~10 months
Processing time
$250–330
AUD per class (gov fee)
Lodge now
Recommended timing
Because processing takes the better part of a year, it's worth lodging the application as soon as the name is confirmed. We'd recommend engaging a trademark attorney to handle the filing — typically $500–1,500 all-in for a straightforward application.
Question 3
Apple Developer account
To put LET on the App Store — even just for beta testing — we need to be enrolled in Apple's Developer Program. This is a straightforward annual subscription through Apple's website. The question is whether it sits under a company ABN or a personal name.
developer.apple.com
Where to enrol
Decision needed: should this be registered under an existing company ABN, or do we need a new entity set up for LET first? This affects how revenue from the App Store is paid out.
Question 4
Which domain name should we lock in?
LET needs a permanent web address before launch — for the marketing site, the App Store listing (Apple requires a privacy policy URL and a support URL), and for sharing the app with players. lettennis.com is already taken, and we've ruled out the longer .com.au option. Five candidates below — short, brandable, all need to be checked for live availability on GoDaddy before purchase. The recommendation is option A, but happy to explore alternatives.
A
lettennis.app
Recommended
B
let.tennis
.tennis TLD
C
hitup.app
matches in-app "Plan a Hit" copy
D
gameon.app
matches GAMEON page
E
letplay.app
action-oriented
✓ Decided
Age rating — 18+
LET will launch as an 18+ app. Because the app involves strangers arranging to meet in person, 18+ is the cleanest path through Apple's review process and limits legal exposure at launch. Junior and teen players can be considered in a future version once moderation infrastructure is in place.