Name: Samantha Lopez
ID: 20266142
Email: samantha@kaist.ac.kr
Gittea Repo: https://git.prototyping.id/20266142/Overheard.git
Live site URL https://overheard-d7396.web.app
App Description
Overheard is a location based message board. Its premise is simple and intentionally constrained, you are only able to read what others have left at a place if you are physically in the same location. You can only leave a message if you are standing there too. Overheard functions on anonymity It strays away from becoming a "social media" by eliminating profiles, feeds, and online identities. The app utilizes firebase's anonymous authenticatioin to store activity on devices, enough to link interactions across sessions but not enough to create anythings that ties your messages back to you.
Overheard works like this; upon opening, the app gets your location and translates coordinates into a geohash, a short string representative of your geogrpahical location. It then takes that string to look for messages whose geohash contain an identical prefix thus returning messages left around you. The messages appear as pins on the map. You can tap on any pin to read what was left there. When reding a message you are presented with two choices: to echo it which restarts its 30 day life span and thus extends its visibility on the map, or let it go, which simply closes the read view of the messages. You can also leave your own messages compromised of text and optionally an image for others to read within the next 30 days, you can even choose a display pin color for your message(optional).
Echoing messages
one of my favorite features of the entire app is the 30 day decay feature that each message lives under. This makes it so messages don't accumulate forever, they face, a message which goes unechoed dissapears within 30 days, not because it gets deleted from the database but because it was either undiscovered or because no one made the decision to extend its lifespan. Echoing a message resets its lifespan it signifies that someone cared enough to keep it alive. This interaction allows messages to become curated over time, messages that survive are those who kept being encountered by people who wanted those messages to survive. This si what makes the app interesting. Individual messages are fleeting, echoing is what makes something as close to permanent as you can get within this app and physical presence is required for all of these interactions.
Features
Geoloacation based message visiibility: Overheard uses the browser Geolocation API to get user coordinates then turns them into a geohash (using ngeohash). The geohash is then used to look for messages in firebase within the desired area (location scope is calculated based on geohash prefix similarity, currently the prefix is 7 characters which is about a city blocks worth of distance). This feature makes physical presence a requirement to interact with the app in any meaningful way.
Message pins on the map: Each nearby message is visible as a pastel colored circle on the map. This is done via Google Maps legacy Marker API with the icon being handled by pins.js. Pin color can be chosen at the time of leaving a message otherwise a randomly generated color will be used. Opted for this view over a feed to reinforce spacial memory, messages live in the locations where they were left not in a social media like feed.
30-day Decay: Messages had a lastEchoAt timestamp. Messages whose last echo was more than 30 days ago get filtered out and not rendered thus making them dissapear entirely from user view.
Echo: Increments the messages echoCount and sets the messages lastEchoAt to the current timestamp, reseting their 30 day decay clock. Plays a small arpeggio like sound as feeback when pressed.
Let go: Pressing this simpley closes the detailed message view. The alternative to echoing somethings should be equally as effortless and framed as letting a memory go.
Composing a message: ComposeSheet.svelte calls addMessage with the text, image, and pin color. Maximum 240 characters are accepted. Images are analyzed to ensure that no selfies are uploaded to preserve anonymity.
Share with link and QR code: SharePopover.svelte creates a URL and a QR code (via a qrcode package). +page.svelte is able to read the the ?message parameter on the url once it opens and shows that message (if it is still alive). I decided to add this feature as it brings some of the sharing and allows for even more physical interactions. For instance the QR code can be posted in places both sharing a message and inviting people to share their own.
location trail: Every time the app loads, addTrailPoint writes the coordinates to localStorage. In MapView.svelte, the full history is drawn as a polyline on the map (toggleable with the small button in the top-left). A visualizer of all the places where you have shared and received memories within Overheard.
Archive page: Shows a list of every message that has been left on the device and shows their decay status.getMyMessages() looks for messages in Firestore by authorId matching the anonymous UID. This allows you to be able to see what you've left behind and whether it's still alive.
Stamps: Every time a message is posted from a new geohash-4 region (4 char prefix location precision), checkAndAwardStamp reverse-geocodes the coordinates and writes a stamp to users/{uid}/stamps/{geohash4}. The stamp page shows all the earned stamps like a sticker-album layout. Each stamp is a circle with a predetermined icon and the place name. This serves as a track of all the places you've been in and in which you choose to partake in the sharing of anonymous memories.
Face detection for photo uploads: When you select a photo hadFace(file) from faceDetection.js block the upload of a picture if any face is detected. The check occurs before the image even gets to be previwed for upload. Since overheard is anonymous its pictures should also be.
App summary; TLDR
Overheard was not built ot be a social app, it purposely erased the ability to create a public identity, you cannot follow anyone, see anyones profile, or even know who left a message in the first place. Its has no guarantee of permanence, participating is accepting that the memories you share and receive might fade. It reinforces physical presence, you cannot sit and scroll through messages but you can only experience the memories of others by crating some yourself in the same physical location. Overheard is designed to feel like you are reading something somene left for you in a particular place, not scrolling for content.
Code Organization
File Organization
Overheard is an application built using SvelteKit and which was deployed to firebase for hosting. The file structure it functions under is as illustrated below.
Data/Application Flows
App opens
Message pins render
Tapping a pin
Leaving a message
New message is added during active session
View the diagrams on Figma with the following link: https://www.figma.com/board/zJCrMvicQFHpyX9eqeCY2W/Final-Project-Diagrams?node-id=2008-3984&t=NF572Ti6bMLwkJB1-1
Svelte stores (shared states)
messagesStore.js
| Variable/Function | Purpose |
|---|---|
messageStore |
array of all the nearby active messages |
livePinIdsStore |
set of message ID's that arrive bia the real time listener |
setMessages(newMessages) |
setter that compares against previous list and plays playNewPinchime() if any new IDs appear |
mapStore.js
| Variable/Function | Purpose |
|---|---|
mapStore |
help figure out what to render |
selectedMessage |
the message tapped on by the user (null if none) |
composing |
true when the compose sheet is open |
userStore.js
| Variable/Function | Purpose |
|---|---|
userStore |
holds the anonymous Firebase identity for the device |
uid |
the anonymous Firebase UID (needed before stamps or archive can load) |
ready |
true once auth has resolved |
initAuth() |
calls signInAnonymously and subscribes to onAuthStateChanged to keep the store in sync |
stampStore.js
| Variable/Function | Purpose |
|---|---|
stampsStore |
array of earned passport stamps (populated on load, appended to live when a new stamp is earned) |
globalCountStore.js
| Variable/Function | Purpose |
|---|---|
globalCount |
total messages ever posted worldwide |
refreshGlobalCount() |
fetches from meta/stats in Firestore and updates the store |
How different data is stored in Firestore
Messages
authorId: "T9R85su4FvUII22QMUUljK0jyy33" //(string)
createdAt: May 17, 2026 at 7:34:03 PM UTC+9 //(timestamp)
echoCount: 0 //(int64)
geohash: "wy6wfhcqd" //(string)
imageUrl: "" //(string)
lastEchoAt: May 17, 2026 at 7:34:03 PM UTC+9 //(timestamp)
lat: 36.370018 //(double)
lng: 127.355324 //(double)
moodColor: "hsl(40, 60%, 72%)" //(string)
text: "cocumentation example" //(string)
Meta Stats
id: "" //(string)
totalMessagesEverPosted: 0 //(int64)
Users
uid: "" //(string)
Stamps
geohash4: "precision-4 geohash" //(string)
city: "" //(string)
country: "" //(string)
iconId: "" //(string)
color: "hsl(...)" //(string)
earnedAt: May 17, 2026 at 7:34:03 PM UTC+9 //(timestamp)
Whats in the .env file
| Variable | Description | Where I found |
|---|---|---|
PUBLIC_FIREBASE_API_KEY |
Firebase Web API key | Firebase Console → Project Settings → Your apps |
PUBLIC_FIREBASE_AUTH_DOMAIN |
Firebase Auth domain | Same — {projectId}.firebaseapp.com |
PUBLIC_FIREBASE_PROJECT_ID |
Firebase project ID | Same |
PUBLIC_FIREBASE_STORAGE_BUCKET |
Firebase Storage bucket | Same — {projectId}.appspot.com |
PUBLIC_FIREBASE_MESSAGING_SENDER_ID |
Firebase messaging sender ID | Same |
PUBLIC_FIREBASE_APP_ID |
Firebase app ID | Same |
PUBLIC_MAPS_KEY |
Google Maps JavaScript API key (also used for the Geocoding REST API) | Google Cloud Console → APIs & Services → Credentials |
Opportunities for improvement
- The current decay logic works by simply not rendering the messages which are past the 30 day threshold after their last echo. However, these messaages never get deleted from the Firestore which means they are never truly gone.
- The map does not update as the user is moving. Currently the app only calculates messages and location on open or on refresh. Making it so the app updates location as the user is moving could add to the interacton significantly.
- The current face detection was utilized using a model trained on close range faces and works best on selfies, thus currently some mid to far range pictures with faces can bypass this check.
Resource auknoledgement
Below is a comprhensive list of everything utilized to being Overheard to life (including links and usage notes)
Google Maps JavaScript API https://developers.google.com/maps/documentation/javascript/overview
renders the interactive map, places custom pins (via google.maps.Marker with SVG icons), handles click interactions, and applies the custom desaturated/parchment visual styling via the styles array.
Google Maps Geocoding API
https://developers.google.com/maps/documentation/geocoding/overview
Used for the stamps feature, converts a message's lat/lng coordinates into a city and country name when a user posts from a new region.
@googlemaps/js-api-loader
https://www.npmjs.com/package/@googlemaps/js-api-loader
A library used to cleanly load the Google Maps JavaScript API script asynchronously inside Svelte's onMount.
Firebase Firestore
https://firebase.google.com/docs/firestore
Stores all message documents (text, coordinates, geohash, timestamps, echo data, mood colors), the global message counter, and per-user stamp collections.
Firebase Storage
https://firebase.google.com/docs/storage
Stores uploaded images attached to messages. Files are uploaded here and the resulting download URLs are saved in Firestore message documents.
Firebase Hosting
https://firebase.google.com/docs/hosting
Hosts the built static SvelteKit app, providing the live .web.app deployment URL.
Firebase Authentication (Anonymous)
https://firebase.google.com/docs/auth/web/anonymous-auth
Gives every device a persistent, anonymous user ID without requiring registration. Used to link messages to a device for the archive page and stamps, preserving the app's anonymous design.
ngeohash
https://www.npmjs.com/package/ngeohash
Converts coordinates to geohash strings and back (encode, decode_bbox). This powers the proximity-based "nearby messages" queries.
@mediapipe/tasks-vision (BlazeFace)
https://ai.google.dev/edge/mediapipe/solutions/vision/face_detector/web_js
Google's face detection model. Used to scan images before upload and block any containing faces.
lucide-react
Outline icon set used throughout the UI( navigation bar icons, stamp icons, and other interface elements).
qrcode
https://www.npmjs.com/package/qrcode
Generates QR codes for the share feature, allowing any message to be turned into a scannable code that links back to that specific message.
SvelteKit / Svelte 5
https://svelte.dev/docs/kit/introduction
The application framework, Svelte 5's runes mode ($state, $derived, $effect, $props) is used throughout for reactive state management, and SvelteKit provides routing (map page, archive page) and the build pipeline.
@sveltejs/adapter-static
https://www.npmjs.com/package/@sveltejs/adapter-static
Configures SvelteKit to build as a fully static site, which is what allows the app to be deployed to Firebase Hosting.
- Claude (coding assistance, debugging, feature implementation)
- Copilot (coding assistance)
- Course notes (ID 30011)
- Referenced code and materials from my own Individual project from earlier in the semester





