- Published on
Building Circles: A Social App for Planning Hangouts with Friends
- Authors

- Name
- Eriitunu Adesioye
- @Eri_itunu
Overview
Circles is a mobile app I built for planning hangouts with friends. The core idea is simple: you create "circles" (small friend groups), propose events within them, and everyone can RSVP, chat, and share photos from the hangout after the fact.
The project is split into two halves — a Flask backend and a React Native (Expo) mobile app. Building both sides end-to-end was the goal from the start, since I wanted experience owning a full production-style architecture rather than relying on a BaaS.
What the app does
There are four main features:
- Circles — create private groups with friends. Think of it like a WhatsApp group but purpose-built for planning.
- Events — propose a hangout inside a circle, pick a location, set a time, and invite members. Everyone can accept or decline.
- Explore — get place recommendations based on your location and preferred event types (dinner, cafe, bar, etc.).
- Memories — after the hangout, upload photos that are tied to the event and visible to everyone who attended.
There's also a built-in group chat for each circle, powered by Stream Chat.
Backend: Flask + PostgreSQL
The backend is a Flask REST API backed by PostgreSQL. I chose Flask because I wanted something lightweight I could structure myself, rather than a framework that makes too many decisions for you.
A few things I'm happy with on the backend:
GeoAlchemy2 for location queries. User locations and recommended places are stored as PostGIS geometry points. This made it straightforward to query recommendations within a geographic bounding box for a given region.
Pre-signed GCS uploads for memories. When users upload event photos, the mobile app requests signed Google Cloud Storage URLs from the backend, then uploads directly from the device to GCS. The backend never handles the raw image bytes — it just verifies the files exist in GCS afterward and creates the database records. This keeps the server lightweight and upload speeds fast.
JWT with automatic refresh. Access tokens expire in 15 minutes, refresh tokens in 180 days. The mobile client handles refresh transparently via an Axios interceptor that queues requests during a refresh and retries them once a new token is issued. Getting this right was one of the trickier parts — managing the queue and preventing multiple simultaneous refresh calls took a few iterations.
Push notifications with email fallback. I used Expo's push notification service for in-app alerts (new event, invite, memories upload, etc.). If a push fails — because the user uninstalled the app or revoked permissions — the backend falls back to sending an email via Resend. Small touch, but it made the notification system feel more reliable.
Mobile: Expo + React Native
The mobile app is built with Expo (SDK 54) and TypeScript. Navigation uses Expo Router's file-based routing, which mirrors Next.js's App Router — an easy mental model to work with.
State management. Auth state lives in a Zustand store with tokens persisted in Expo SecureStore (native encrypted storage). Everything else — events, groups, profiles — is fetched and cached via TanStack React Query, which handled most of the loading/error/stale-while-revalidate complexity out of the box.
Onboarding flow. New users go through a short onboarding after verifying their email: set up their profile, pick a home region, and grant device permissions. The home region (stored as an ISO subdivision code like US-CA) is used to scope the Explore recommendations.
Auth methods. The app supports email/password, Google Sign-In, and Apple Authentication. Each has slightly different flows on the backend, but the mobile client abstracts them into a consistent auth store so the rest of the app doesn't care how the user signed in.
Integrations
Pulling this together required more third-party services than I initially expected:
| Service | Purpose |
|---|---|
| Google Maps Places API | Explore recommendations |
| Stream Chat | Real-time group messaging |
| Google Cloud Storage | Event photo storage |
| Firebase App Check | Mobile app verification |
| Resend | Transactional emails |
| Expo Notifications | Push notifications |
Each integration added complexity — mostly around credential management and handling failure cases gracefully. Firebase App Check in particular was tedious to set up correctly across dev and production environments.
What I learned
The biggest takeaway was how much surface area a "simple" social app actually has. Authentication alone — with multiple OAuth providers, email verification, token refresh, and secure storage — took longer than I expected before I even wrote a single feature screen.
The split between client and server also forced me to be deliberate about API design early. A mobile app is much less forgiving than a web app when it comes to breaking changes, so I had to think carefully about response shapes and versioning from the start.
Building both halves myself also made it clear where the real complexity lives. The UI flows are the visible part, but the work that made the app actually feel reliable was mostly invisible: the token refresh queue, the GCS upload verification, the push notification fallback. Those are the pieces that don't show up in a demo but determine whether users trust the app.
What's next
There are a few things I'd still like to add — event suggestions based on past hangout history, deeper social discovery, and a web client. Whether I get to all of them depends on how much time I can carve out. For now, the core loop works: create a circle, plan a hangout, share the memories.