Skip to main content
  1. projects/

Building my first mobile app 🦎

projects read the room app development social media privacy society
Table of Contents

Imagine a world where everyone could see how all humans honestly felt about any topic, across borders, languages, and cultures — would that drive more empathy?

This article is meant to be a practical high-level guide for indie developers, from architecture decisions to app store launch. I hope it helps some people think through the process before they get started.

Disclaimer: I’m a physicist by training, not a software engineer!


In late 2023, I started building > read(the_room) (RTR) — an anonymous polling platform that maps how people feel across countries, cities, and generations. No accounts, no tracking, no selling of user data. The platform is a living social experiment in some sense, and the whole project is structured to be a nonprofit initiative.

If you haven’t read the backstory, I wrote about why I started RTR and the vision behind it. The short version: I wanted to build my ideal flavour of social media. Earth’s vibe check. A platform that is not addictive, and triggers introspection, highlights nuance, and avoids fueling outrage.

From website to mobile app
#

RTR started as a web app, but that website is now just the app landing page, why?

Retention. The web version worked well enough for one-off visits: someone shares a link, you answer a question, maybe you browse a few more. But people rarely (ehm, never) came back. There was no mechanism to remind users that new questions were live, no way to re-engage someone who answered once and forgot about us. We were entirely dependent on viral sharing for every single session, which was cool when it happened but… Not sustainable. It also was a shame because so many people liked the core idea and philosophy, but didn’t know what a progressive web app even was. I was building for the wrong crowd from the start. The tech stack doesn’t matter to users, but familiarity and trust does. PWAs were not familiar.

A native app also allowed for push notifications which let us surface new responses to the question author, and let them know how their question was doing. I could even have widgets instead of notifications for those who want to stay engaged passively. With a mobile app, I could also now softly enforce 1 device = 1 vote, the website had double-counting issues. Biometric and passkey authentication means you can pick up where you left off without friction. It was the more natural medium for the project, but it was harder to build.

The move to mobile was less about adding features and more about fixing the fundamental problem: we were building a platform that required people coming back to see the responses on their questions grow but, originally, I had no way to bring them back without collecting an email or piece of contact info which I chose to avoid doing. It was one of my design constraints. In the end, the switch from web to native was worth it.

Start with architecture, not code
#

This is the single most important thing I’d tell any first-time developer: understand your planned architecture before you write a line of code.

I didn’t learn this from a course or a textbook. I learned it from YouTube. Channels covering system design, database modelling, and client-server architecture gave me the mental models I needed before touching a framework. I learned what to ask. Understanding how data flows between a client and a server, what a relational database actually does, and why you’d pick one auth strategy over another — that foundation saved me months of refactoring later.

You don’t need a computer science degree to build an app. But you do need to understand the shape of the system you’re building. Spend a week watching architecture videos before you commit to a tech stack. It’s the highest-ROI time investment you’ll make.

Build once, ship twice: Flutter
#

The framework decision can paralyze you. React Native, Flutter, Kotlin Multiplatform, native for each platform. The discourse is endless and mostly unhelpful. Some more fancy frameworks have a seemingly infinite number of plugins, and I wanted to avoid decision fatigue…

I chose Flutter for three reasons:

  1. Single codebase for iOS and Android. I’m one person. Maintaining two native apps was never realistic.
  2. Dart is straightforward. Coming from Python, picking up Dart was fast. The type system caught bugs early without being oppressive.
  3. Hot reload is awesome. When testing UI tweaks on your computer, hitting r to reload or shift+R to restart without a full recompiling of the app is genuinely fast, and made it so much easier to learn what I was doing.

Was Flutter the objectively best choice? I don’t know. But it was a good-enough choice that let me build without overthinking. If you’re a solo developer, the best framework is the one that stops you from deliberating and allows you to prototype fast. RTR runs on both platforms from one codebase, and that’s what mattered to me. I wasn’t familiar with React for web development (the RTR web app used Flask and Jinja2 templating), so ReactNative (Expo) wasn’t so obvious to me.

One big motivating decision was realizing that Talabat (the largest food delivery app in the Middle East) was built with Flutter. Right away, all my concerns of deciding between ReactNative (Meta, big company backing the framework) or Flutter (open source, Google) just evaporated. I’ve used Talabat and it worked, I just wanted something that worked.

Backend: Supabase
#

I didn’t want to host my own backend. I didn’t want to manage a server, configure nginx, set up SSL, handle database backups, or deal with scaling, and CORS bugs. I wanted to write application logic.

Supabase is an open-source Firebase alternative built on PostgreSQL, and for an indie project it hits a sweet spot:

  • Postgres under the hood. Real relational database with proper constraints, foreign keys, and materialized views. Not a document store pretending to be a database.
  • Row Level Security. This was critical for RTR’s privacy model. RLS policies enforce access control at the database level, so even if my application code has a bug, the database won’t leak data it shouldn’t.
  • Edge Functions for server-side logic without managing infrastructure. Also, Edge Functions are deployed on a Global CDN, making some queries snappy from abroad too (more on this later).
  • Auth built in, though I ended up building custom passkey authentication on top of it… I did want to use their built in auth system. I tried Sign in With Google, but it forced my to store user emails, I didn’t like that, and so for simplicity and parity across platforms I chose to go Passkeys-only and avoid both Sign in with Google/Apple and other third-parties.
  • It’s open source. If Supabase the company disappears tomorrow, I can self-host. That alignment mattered to me.

The trade-offs between Supabase and self-hosting are real though, Supabase’s free tier has egress limits that I’ve had to optimize around with materialized views and CDN caching via Edge Functions. But for a solo developer who wants PostgreSQL without the ops burden, it’s hard to beat. The free tier works fine for less than 1000 users (I’m at 580 now). It should still only cost $25/month up to 100,000 monthly active users.

About network speeds: I did notice that without read-replicas (which I still haven’t set up) the querying is in fact slow from MENA/Asia. This is because I chose to host my backend in Supabases’ US East location, which I thought was a compromise between my social hubs in SF / Montreal, but I didn’t realize how slow the app would feel for users from Oman without further optimizations and, in future, having a proper CDN layer. Edge Functions also make reading a bit faster, but writing is still slow. I haven’t fully figured this part out.

Dev Environment: VSCode → Cursor → Claude Code
#

My development environment evolved in three stages, and I think this progression is worth documenting honestly.

Stage 1: VSCode. I wrote everything manually, learned Dart and Flutter fundamentals the hard way. This was valuable to me — I needed to understand what the code is doing before I let AI help me write it. I picked a Flutter template and went from there. Spent a couple of weeks messing around.

Stage 2: Cursor. AI-assisted coding with context awareness. This sped up boilerplate significantly and helped me learn Flutter patterns faster by seeing well-structured suggestions. Good for the “I know roughly what I want but don’t know the API” phase. It was a really good autocomplete. Cursor was decent, but for some reason slowed down my computer a lot so I ditched it before they got into their agentic coding phase.

Stage 3: Claude Code. This is where my workflow fundamentally changed. Claude Code operates at a higher level of abstraction — I describe features and architectural decisions, and it generates implementations that are aware of my existing codebase. For a solo developer, this is transformative. It’s the difference between having no team and having a very fast junior developer who never gets tired. I remember telling a friend I was exhausted writing code until 4AM after work one day. His response: “Imagine how Claude feels!”.

Honestly, I think the exact tools you use is a matter of taste and expertise. I hear Loveable and other no-code solutions exist now, but for me… I am comfortable in the terminal so I picked Claude Code and don’t regret it. It feels faster and I feel like I have more control and transparency over what is going on under the hood.

Some tips for AI-assisted development
#

Don’t let it make architectural decisions. AI is great at implementing within constraints. It’s less reliable at choosing the right constraints. Architecture should come from your understanding of the problem, not from an autocomplete suggestion. Read up first.

Plan and document first, clear context, then build. This is my core workflow. For every feature or bug fix, I start by writing a markdown file: the problem, the approach, the relevant files, and the expected behaviour. I iterate on this plan with the AI until it’s solid. Then I clear the context, start a fresh session, and tag the markdown file so the AI builds from a clean, well-defined spec. The markdown then doubles as human-readable documentation for that feature — no extra work needed later. When the feature is finished, update the markdown. Clear context, ask the AI to validate the code against the Markdown documentation or note any discrepancies and fix them.

Keep your database schema in markdown. I maintain a markdown file with my full Supabase schema — tables, relationships, RLS policies, edge functions. Whenever I’m working on a feature that touches the database, I tag that file so the AI has the full picture. When the schema changes, I update the doc. Saved me tonnes of time…

Give it context. The better the AI understands your architecture, the better its output. Is the feature you’re building part of a string of features that have to work together? You should note that in context! Between the feature plans and the schema doc, the AI produces code that fits the system rather than generic solutions that only work independently.

Review diffs, not just outputs. When AI modifies existing code, read the diff carefully. It can introduce subtle regressions, especially around state management and edge cases.

Commit to git only when a feature is stable. I started only committing once features are stable, but not too much (as I usually would do in the pre-AI era). I think large diffs and overhauls of how the code achieved the goal happens much more with agentic coding, so I only commit after the feature is built and end-to-end testing is done on a real device and not the simulator.

Understand the code it writes. This is non-negotiable. If you can’t read and reason about the output, you’re building on sand. AI accelerates development; it doesn’t replace understanding.

App store policies — preparing for beta
#

Getting an app into the App Store and Google Play are their own projects honestly. A few things that caught me off guard:

Apple’s review process is opaque. You submit, you wait, you sometimes get rejected for reasons that feel arbitrary. Build in buffer time of about a week. My first submission was rejected because of how I described the app’s onboarding with Passkeys and they thought I needed to give them a “trial account” for testing.

Google Play requires a closed testing track. Before you can go to production, you need 12 individual Android testers (who aren’t on your “internal testing” team) who’ve opted to test your app with real usage over for 14 consecutive days! Plan for this.

Privacy labels are mandatory and detailed. Both platforms now require you to declare exactly what data you collect, how you use it, and whether you share it. If you’ve built with privacy in mind, this is actually easy — you just check “no” on most things. But fill these out carefully; inaccuracies can trigger review delays. You’ll need a website with a privacy policy listed somewhere and it must be kept up to date, so buy your web domain early. You’ll also need this for notifications, deeplinks, etc…

Version management is tedious but important. iOS and Android have different versioning schemes. Track your build numbers carefully. I waste time debugging submission failures that turn out to be a duplicate build number more times than I’d like to admit.

Have a few Android and iPhone internal testers. Make sure you either have access to a relatively recent Android device or iPhone throughout the beta testing phase. It’ll save you lots of time. Also, notifications can’t be tested on the phone simulator you use for developing on your computer!

Lessons from beta testing
#

Users don’t read instructions. I built passkey authentication thinking “no password to remember” was obviously better. Turns out most users don’t know what a passkey is. I had to redesign and implement an onboarding flow to explain the why before the how.

Geographic dispersion undermines my value proposition. RTR’s core feature is comparing how different regions respond to the same question. With 500 installs across 50 countries, most locations have too few users for meaningful comparison. I learned that concentrated growth in a few cities is worth more than thin global coverage. This seems obvious in retrospect, but it reshaped my entire acquisition strategy. There’s no point making the app work in 7 languages if it’s not being used heavily in one. I focus on English-only privacy-aware groups for now.

Performance is a feature. Early versions had 3-5 second feed loads because I was making too many database queries. Optimizing to materialized views and edge functions brought that under a second.

Don’t fly blind! I started using analytics from PostHog (which I really did not want to implement, but they do respect privacy, GDPR, and are open source too) and it made such a huge difference. I cannot stress this enough. Before PostHog I was flying blind and hoping users reported every experience they had on the app. I did not know what was working and what didn’t, or what features were being used so I can polish and develop those ones further.

Beta testers who give negative feedback are your most valuable users. The people who tell you what’s broken are doing you a favour. Make it easy to report issues and respond to every one as quickly as possible. Honestly, beta testing kind of took over my life for a couple of months, at some points I was releasing updates every couple of days. Plan for this.

What I’d do differently: don’t build anything new during beta testing unless users ask you to! I spent too much time building “rooms” and the “My Network vs World” feature to see where your friend groups stand relative to everyone else but no one cared, or used, this feature. No one asked for it, but I built it anyway… Instead, it was beta testers who twisted my arm to build: comments, lizzies, new question categories, all of which are actively used features now.

App store publishing
#

Getting from beta to production is yet another challenge:

The app store listing is a landing page. Treat it like one. Your description, screenshots, and keywords determine whether you get some random installs. I iterated on my App Store description multiple times — shorter, more concrete, focused on what users do rather than the technical architecture. What is your key value prop to the user? Highlight that.

Ratings and reviews matter immediately. Even a handful of early reviews affect your visibility. Don’t be afraid to ask beta testers who like the app to leave a review and a rating. It made such a big difference when advertising to other communities.

Expect post-launch bugs. No matter how much you test, real users on real devices will find issues. Have a fast path to push updates. Document the build and submission flow for updating your app!

Oh, and Apple has strict sizes for screenshots. It takes a while to get them right!

Marketing lessons (still working on it…)
#

I really, really, really underestimated how much effort and time this would take. I think the biggest lesson so far is: market before launch! Next time, I would have a signup and only start coding once there’s ~3x more sign ups than the number of beta testers I’m targeting.

Other small things: privacy-focused communities respond to authenticity. Reddit alternatives, fediverse users, and other privacy-minded folks got the anonymity idea, but for most normal folk I think I might be emphasizing the privacy aspect too much over the “cool” aspect of the sentiment mapping. Still, being honest about limitations and trade-offs resonates more than polished pitches for me, and I hope I find my crowd by being genuine — we’ll see how that goes.

Open Sourcing
#

RTR is open source. You can find the code here, audit the privacy claims, and verify that the architecture does what I say it does.

I chose to go with the AGPLv3 open source licensing as opposed to MIT/BSD because I truly think the human bioindicator idea is so cool, and it would be such a shame if a version of RTR got popularized but was not open source; hence AGPLv3: it’s a strong copy-left license, meaning any derivatives also have to be open source projects. Also, the logos, mascot names etc… are not included as open source files, and you can specify these items explicitly in the repo. I did not know you could do that at first!


Building RTR has been an exercise in making decisions with incomplete information, to shipping before they feel ready, and learning that there’s a lot more to app development than siloed coding. DevOps and marketing is a whole other ballgame (and thankfully I have Janay helping me with the marketing now). Still, if you’re a non-engineer thinking about building something the tools have never been more accessible. Start with learning the software architectures needed, YouTube is a great resource. Pick a tech stack, ship fast, and iterate.

I’m still learning, but maybe this can help others out there who have an idea of their own. Please let me know if you have any suggestions, or appreciated the rundown!

Check it out: https://readtheroom.site

If you’d like to follow along, I recently started a Substack.


Download links #

App Store | Play Store
#