Places worth remembering.
GEM is a social mobile application for discovering and sharing meaningful places through people you trust. Rather than surfacing recommendations from anonymous reviewers or engagement-optimized algorithms, GEM builds a personal map from the opinions of friends, classmates, and others whose taste you have chosen to follow.
Built for CS278: Social Computing, Stanford University, Spring 2026.
Requirements: Node.js 20+, the Expo Go app on your iOS or Android device.
git clone <repo-url>
cd gem
npm install
npx expo startScan the QR code with Expo Go. The app connects to the live Supabase project by default and requires no additional configuration.
Environment variables: Supabase credentials are stored in gem/.env. Copy gem/.env.example and fill in your own values if you want to point the app at a different Supabase project.
Distributing a build via EAS:
npm install -g eas-cli
eas login
eas update --autoShare the resulting EAS update URL. Testers open it directly in Expo Go.
Mainstream place-discovery platforms optimize for volume and engagement. The result is thousands of anonymous reviews that are difficult to contextualize and easy to game. In practice, people rely on a much smaller and more trusted signal: a recommendation from a friend, a roommate, a professor, or someone whose judgment they have tested over time.
GEM is built around that observation. Every pin in GEM is a personal recommendation attached to a real person whose profile, taste tags, and history are visible. The social graph is not decorative. It determines what appears in your feed and shapes every discovery experience in the app.
The Discover feed surfaces public gems from across the network, with a horizontally scrollable trending section at the top showing the most-saved places. Posts from accounts you follow display a "circle" badge, giving social context even in the global feed.
Circle mode collapses the feed to gems authored exclusively by accounts you follow, reducing noise and foregrounding trusted recommendations. Both modes support category filters: Study Gems, Food Spots, Coffee Runs, Moments, Hidden Gems, and Late Night.
An interactive Google Maps view renders every gem as a color-coded, icon-labeled pin organized by category. Tapping a pin surfaces a slide-up preview sheet; a second tap navigates to the full gem detail. The map supports the same Circle mode filter as the feed and defaults to Stanford's campus. Custom map styles are applied for both light and dark themes.
The compose screen prompts for a place name, category, optional photo, and a personal note. The location picker queries the app's Supabase database first, then falls back to the OpenStreetMap Nominatim API for unrecognized locations, with a manual entry option as a final fallback. The onboarding flow explicitly models what a high-quality, specific gem looks like before a user posts for the first time.
Users can organize saved gems into named collections such as "Study Spots" or "Late Night Eats." Each collection can be set to public, making it visible on the user's profile, or private. Collections are browsable in full through a dedicated detail screen and display live item counts.
Each profile includes a display name, a unique @handle, a bio, a taste tagline (a single sentence describing what the user values in a place), and up to eight free-form taste tags. These fields are designed to help other users evaluate whether a profile is worth following before committing to a follow relationship. Profiles also display follower and following counts, the user's gem count, public collections, and a preview of the user's latest gem.
Following another user is a deliberate, consequential action. The set of accounts you follow directly controls what appears in Circle mode. The follow decision is supported by taste tags and taglines, which provide enough signal to assess taste alignment before following.
Any gem can be saved with a single tap. The save count on each gem is visible to all users, functioning as lightweight social proof. Gems can also be bookmarked into specific collections for later reference.
Users can leave comments on any gem. Comments support one level of threaded replies. Comment likes are persisted to the database and displayed as counts. Users can delete their own comments and report others' comments through a structured report flow.
Both gems and comments can be reported through a categorized report sheet (Spam, Inappropriate Content, Harassment, Misinformation, or Other). Reports are submitted to the Supabase database for team review. The reporting flow is introduced during onboarding, framing community moderation as a shared responsibility rather than a hidden safety valve.
First-time users complete a seven-step interactive onboarding flow before reaching the app. The flow includes an interactive choice game that asks users to select which of two example gems is more useful, explicitly establishing quality norms before a user posts. Each user's onboarding completion is tracked per-account in AsyncStorage.
The app supports English and Spanish, switchable from Settings, with language preference persisted across sessions via AsyncStorage. Full light and dark theme support is implemented throughout, with custom map styles for each theme.
Feed, Map, and Settings
Compose, Collections, and Profile
Onboarding Experience
Reporting and Moderation
The client is a React Native application packaged with Expo SDK 54, targeting iOS and Android. Navigation is implemented with React Navigation using a bottom tab navigator for the two primary views (Discover and Map) nested inside a native stack for all modal-style screens. Theme state and the authenticated user object are managed at the root level in App.js and passed as props throughout the tree.
The feed is rendered with a virtualized FlatList. Map markers use React.memo and carefully manage tracksViewChanges to avoid the rendering performance pitfall common in react-native-maps when custom marker content changes. Localization uses i18next and react-i18next with a custom AsyncStorage detector that persists the user's language choice across sessions.
The backend is a Supabase project providing PostgreSQL, authentication, file storage, and a REST API. There is no intermediate server layer. The client communicates directly with Supabase through its JavaScript SDK.
The core schema includes the following tables:
| Table | Purpose |
|---|---|
profiles |
Extended user data: handle, bio, taste tagline, taste tags, avatar |
gems |
User-created place recommendations |
places |
Geocoded location data (name, city, latitude, longitude) |
gem_images |
Ordered photo attachments stored in Supabase Storage |
comments |
Comments with optional parent_comment_id for one-level threading |
comment_likes |
Per-user comment likes with composite primary key |
saves |
Many-to-many relationship between users and gems |
collections |
Named, visibility-controlled sets of saved gems |
collection_gems |
Junction table linking collections to gems |
follows |
Directed follow graph between users |
reports |
Structured reports on gems and comments |
Row Level Security is enabled on all tables. Policies enforce ownership on all write operations: for example, gems can only be deleted where auth.uid() = author_id, and follows can only be created or deleted where auth.uid() = follower_id.
The feed is generated by a PostgreSQL stored procedure, get_feed(p_limit, p_offset), called via Supabase RPC. The function joins gems, places, gem_images, profiles, and saves in a single query and injects is_saved and save_count fields computed from the calling user's identity via auth.uid(). This avoids multiple round-trip queries from the client to assemble the feed.
Authentication uses Supabase's Google OAuth implicit flow, opened in an in-app browser via expo-web-browser. A deep-link handler in App.js parses the token hash fragment returned to the custom URI scheme on Android, where Chrome Custom Tabs cannot reliably detect scheme redirects. Sessions are persisted to AsyncStorage and automatically refreshed by the Supabase client.
User-uploaded gem photos are stored in a Supabase Storage bucket (gem-images). The client uploads images using expo-image-picker and stores the resulting storage path in the gem_images table. Public URLs are resolved on the client at render time.
Map rendering uses react-native-maps with the Google Maps provider and custom-styled tile configurations for light and dark modes. Place search during gem creation queries the Supabase database first for previously entered locations, then falls back to the OpenStreetMap Nominatim API, which requires no paid API key.
Collection item counts are maintained as a denormalized item_count column on the collections table, recalculated from the true collection_gems row count after each save or remove operation to prevent drift. The social feed is filtered client-side between Discover and Circle modes using a Set of following IDs fetched alongside the feed data, avoiding an additional query on mode switch.
GEM draws on three core theories from CS278: Social Computing. These concepts shaped both how the system was designed and what behaviors we expected to see.
One of the central challenges for any new social platform is establishing a sense of activity before a large user base exists. Drawing on social proof theory, we recognized that users are significantly more likely to participate when they perceive an existing, active community. An empty feed provides no signal that the platform is worth engaging with.
To address this, we prepopulated the platform with posts from the development team before opening it to participants. Our hypothesis was that users would be more willing to contribute and return if they could see that others had already done so, particularly people within their network. The global feed was designed for this early stage specifically: rather than showing only posts from followed accounts, it surfaces all public gems, making the platform feel active even with a small user base. A circle-based feed, while preferable at scale, would have appeared empty to new users and discouraged initial engagement.
GEM is designed to operate at the boundary between weak and strong ties. The platform surfaces recommendations from a broad social network rather than requiring close friendships as a prerequisite for meaningful discovery. A user does not need a deeply formed relationship with someone to benefit from their gem posts.
The intended use pattern is to leverage weak ties for place discovery and use that discovery as a bridge toward stronger social interaction. Finding a place through a loose connection can prompt a conversation, lead to making plans with a closer friend, or open a new interaction with the person who posted. GEM does not aim to replace strong-tie relationships but to use the wider network as a source of inspiration that flows into those closer connections. The follow graph and taste tags support this by giving users enough signal to determine whether a connection is worth engaging with, even without a prior relationship.
To maintain a useful and respectful community, GEM introduces norms early and makes them persistent. Rather than relying on descriptive norms (what most people do) alone, we focused on injunctive norms (what the community considers appropriate behavior) established at the point of first use.
The seven-step onboarding flow explicitly models what a good gem looks like. It includes examples of low-quality posts alongside higher-quality ones and uses an interactive choice exercise to make the distinction concrete before a user posts for the first time. The community guidelines remain accessible from the Settings screen, so the reference point is never buried. For enforcement, the platform uses user-based moderation: any gem or comment can be reported through a structured flow with five categories. Reports are reviewed directly by the development team. This combination of early norm-setting, accessible guidelines, and crowdsourced reporting was our approach to building a culture of contribution quality without requiring automated moderation infrastructure.
gem/
βββ App.js # Auth listener, onboarding gate, navigation root
βββ supabase.js # Supabase client configuration
βββ constants.js # Categories, theme tokens, map styles
β
βββ screens/
β βββ Login.js # Google sign-in and guest demo mode
β βββ OnboardingScreen.js # Seven-step interactive onboarding
β βββ FeedView.js # Discover and Circle feed with trending strip
β βββ MapView.js # Interactive map with category pins
β βββ AddPin.js # Gem compose screen
β βββ PinDetail.js # Full gem detail view
β βββ PostComments.js # Threaded comment screen
β βββ Profile.js # User profile, collections, follow graph
β βββ Search.js # Search across gems and users
β βββ CollectionDetail.js # Collection contents view
β βββ Settings.js # Language, theme, and account settings
β βββ CommunityGuide.js # Community guidelines
β
βββ components/
β βββ SaveToCollectionModal.js # Collection save bottom sheet
β βββ ReportModal.js # Report bottom sheet for gems and comments
β
βββ services/
β βββ places.js # Nominatim and Supabase place search
β βββ share.js # Native share sheet integration
β
βββ localization/
β βββ i18n.js # i18next initialization
β βββ translations/
β βββ en.json
β βββ es.json
β
βββ assets/
βββ logo.png
βββ icon.png
βββ adaptive-icon.png
βββ splash-icon.png
Electrical Engineering / Computer Science, Class of 2027, Stanford University
evawanek@stanford.edu Β· LinkedIn Β· GitHub
Symbolic Systems (HCI), Class of 2027, Stanford University
gilsilva@stanford.edu Β· LinkedIn Β· GitHub
Symbolic Systems, Class of 2027, Stanford University
ylin13@stanford.edu Β· LinkedIn Β· GitHub
Several directions emerged during the project that were not implemented within the course timeline.
Second-degree recommendations. When a user you follow saves a gem, that action could surface the gem to their followers as a soft signal. This would extend trust propagation one step beyond the direct follow relationship.
Collaborative collections. Collections could be opened to multiple contributors, enabling shared curation for trips, neighborhoods, or recurring group contexts such as weekly study sessions.
Place memories. After visiting a gem, a user could attach a follow-up note recording whether the experience matched the recommendation. This would transform GEM from a recommendation layer into a personal place journal.
Travel mode. A temporary radius expansion when a user is in an unfamiliar location would surface gems from their network in that area, extending the trust graph to places they do not frequently visit.
Taste-based discovery. Overlapping taste tags between profiles could power a recommendation system that suggests accounts to follow, connecting the interest graph to the place graph.
| Layer | Technology |
|---|---|
| Framework | React Native, Expo SDK 54 |
| Navigation | React Navigation (Native Stack, Bottom Tabs) |
| Backend | Supabase (PostgreSQL, Row-Level Security) |
| Authentication | Supabase Auth, Google OAuth (implicit flow) |
| Database queries | Supabase JS client, custom get_feed RPC |
| Storage | Supabase Storage |
| Maps | Google Maps via react-native-maps |
| Place search | OpenStreetMap / Nominatim |
| Icons | Ionicons via @expo/vector-icons |
| Localization | i18next, react-i18next |
| Session persistence | AsyncStorage |
| Image selection | expo-image-picker |
| OAuth browser | expo-web-browser, expo-linking |
| Build and distribution | EAS (Expo Application Services) |
This project was developed for CS278: Social Computing at Stanford University. We are grateful to the CS278 course staff for their guidance throughout the quarter.
The following open-source projects and platforms made GEM possible:
- Supabase for the backend infrastructure
- Expo for the mobile development framework and distribution tooling
- OpenStreetMap and the Nominatim geocoding service for location search
CS278: Social Computing, Stanford University, Spring 2026 - cs278.stanford.edu



