A location-based social exploration game for UNSW students.
Mobile app where students discover geofenced Points of Interest (POIs) around UNSW Kensington campus, leave billboard notes, and reply with pixel-art stickers. Progression via quests and levelling unlocks cosmetic perks, billboard capacity, and sticker collection capacity upgrades.
Core loop: Explore campus → discover POIs → leave/read notes → reply with stickers → complete quests → level up → unlock perks → explore more.
| Layer | Choice |
|---|---|
| Mobile framework | Expo (React Native) with native geofencing |
| API runtime (non-real-time) | Hono on Cloudflare Workers |
| Real-time runtime | Cloudflare Durable Objects (WebSockets, broadcasting) |
| Database | PostgreSQL + PostGIS on Supabase (no RLS) |
| ORM | Drizzle |
| Auth | Clerk (social login only — Google, Apple) |
| Maps | react-native-leaflet-view + OSM |
| Content moderation | OpenAI Moderation API |
| Real-time transport | WebSockets via Durable Objects |
| Push notifications | Expo Push Notifications |
| Monorepo | Bun workspaces (apps/app, apps/api, packages/db, packages/shared) |
Cloudflare Workers are the entry point for all HTTP requests. Routing depends on the type of request:
| Request type | Handler | Example |
|---|---|---|
| Non-real-time GET | CF Worker (Hono) → Postgres | Fetch user profile, list saved stickers |
| Non-real-time POST | CF Worker (Hono) → Postgres | Update user display name, settings |
| Real-time GET | Durable Object → Postgres (via WebSocket) | Query notes, POIs, and map data |
| Real-time POST | Durable Object → Postgres + broadcast | Post a note, place a sticker |
| WebSocket connect | Durable Object (persistent connection) | Live map updates, push notifications |
Durable Objects manage persistent WebSocket connections for real-time features — querying notes and map data, broadcasting changes, and sending notifications. Mutations required for real-time functionality (e.g. posting notes, placing stickers) also run inside the Durable Object so the result can be broadcast immediately.
Non-real-time requests (e.g. updating user settings, fetching saved stickers) go through standard CF Workers with Hono and do not require a Durable Object.
- 2D top-down map centered on UNSW Kensington campus
- User sees: their own location dot, POI markers, and billboard notes
- No other users are visible on the map — anonymity of presence
- POI geofence radius: 30m (tuned during playtesting if needed)
- Map provider: react-native-leaflet-view with OSM tiles
- User represented on map by a 64×64 pixel art avatar drawn at sign-up
- 5–10 concurrent POIs selected daily from seeded/admin-created POIs
- POIs are visually distinct from notes (e.g. glowing markers vs. note icons)
- Rotation: each campus day deterministically selects active POIs from the POI table
- First visit to a POI — on an individual user basis — advances quest progress, assuming the user has a relevant quest (daily or level-based). Visiting gives no direct XP.
- Visited POIs are recorded to prevent repeat quest progress from the same POI
- Optional picture: compressed 128×128 base64 PNG image (pixel-art icon or photo)
- Placed by users at their current real-world location
- Concurrent limit: starts at 3 active billboards per user and scales with level. If a user posts while already at their concurrent limit, the backend soft-deletes that user's oldest active billboard before publishing the new one.
- Daily posting limit: separate Sydney calendar-day cap that is slightly above the concurrent limit to prevent unlimited churn. MVP seeded values are concurrent + 1, capped at 10/day.
- Always display a username pill above them
- Take up ~60% of viewport height when expanded
- Passed through OpenAI Moderation API before publishing
- Stickers: 64×64 pixel art canvas, fixed 8-colour palette, drawn in-app, stored as base64 PNG
- Sticky notes: text replies (look like sticky notes)
- 1 placement per user per billboard (unlimited placements total across billboards)
- There is no threading — you cannot reply to another user's sticker/sticky note, only place on the billboard itself
- Later placements have higher z-level (appear on top of earlier ones)
- Tapping a sticker reveals a username pill above it
- Stickers can be saved to a collection and reused on other billboards
- Sticky notes can also be saved and reused like stickers
- User submits content
- Backend calls OpenAI Moderation API
- If flagged → content rejected, user notified
- If passed → content published
- Other users can report content AI may have missed
- Daily posting limits use a Sydney calendar day (midnight → 11:59pm Australia/Sydney), not a rolling window
- If a billboard receives no placements within 24 rolling hours from creation, it soft-deletes
- All billboards have a hard maximum lifetime of 5 days, after which they soft-delete regardless of placements
- The entire billboard (all placements) soft-deletes together
- Saved stickers/sticky notes persist in the user's collection regardless
A quest is a specific objective. Completing it awards XP. Quests are the only way to earn XP — visiting POIs, leaving notes, and receiving replies only matter for XP insofar as they complete quest objectives.
Quests are parameterised templates rather than fixed one-time objectives. Each quest type has a randomised numeric value per tier:
| Quest type | Description |
|---|---|
| Visit N new POIs | Discover N previously unvisited POIs |
| Leave N billboards | Post N text notes |
| Place N stickers | Place N sticker replies |
| Receive N sticker replies | Get N stickers on your billboards |
| Save N stickers | Save N stickers to your collection |
Levelling: each level tier generates a new set of quests with randomised values appropriate to the tier. Higher tiers require more quests per level and use larger value ranges. Completing all quests in a tier awards a level-up.
- Daily quests rotate each Sydney calendar day, separate from the main quest tree
- Daily quest templates and the curated daily quest pool are seeded data; the active daily quest is deterministically selected from the Sydney calendar day
- Completing the daily quest maintains the streak
- Streaks grant bonus rewards (cosmetics, XP multipliers)
- Push notification each day to remind users
| Level | Perk |
|---|---|
| 1 (base) | 3 concurrent billboards, 4 billboards/day, 10 sticker slots |
| 2 | +1 concurrent billboard (4 total), 5 billboards/day |
| 3 | +2 sticker slots (12 total) |
| 4 | Cosmetic: signature on notes/stickers |
| 5 | +1 concurrent billboard (5 total), 6 billboards/day |
| 6 | Note border flair |
| 7 | +2 sticker slots (14 total) |
| 8 | +1 concurrent billboard (6 total), 7 billboards/day |
| 9 | Unique sticker colour palette expansion |
| 10 | Maxed: 10 concurrent billboards, 10 billboards/day, 20 sticker slots, all flairs |
- Public usernames displayed on notes/stickers (incentivises good behaviour)
- Auth via Clerk with social login (Google, Apple)
- Product data uses internal UUIDv4 user IDs; Clerk user IDs are stored only as unique auth-provider identifiers for account lookup
- No anonymous posting
- On sign-up, users draw a 64×64 pixel art avatar that represents them on the map
- WebSockets via Durable Objects for live updates: new notes, new sticker placements, POI rotations appear instantly without a refresh
- Push notifications: daily quest reminder (8–9am), reply received, new POI nearby
- The Durable Object handles all WebSocket connections, broadcasts changes to connected clients, and performs mutations needed for real-time features so results are immediately visible
- Users can report any note or sticker
- Reports go to an admin panel
- View reported content with context
- Take action: hide, remove, warn user, ban user
- Soft-delete model:
hidden_attracks moderation visibility actions, whiledeleted_attracks lifecycle/owner/expiry removal from active product surfaces - Create/edit POIs: name, description, lat/lng (or map click), optional picture upload (128×128 compressed base64)
- Active users (DAU/WAU/MAU)
- Popular POIs (visit counts)
- Note/sticker volume over time
- Abuse metrics (report rate, flagged content rate)
- Art direction: pixel art focus, inspired by Pikmin Bloom and Webfishing
- Typography: Jersey 10 font throughout the entire app
- Colour palette:
- Map/UI base: earthy tones — warm greens, browns, tans
- Stickers: vibrant, saturated colours (pop against the earthy UI)
- Notes/billboards: parchment or wood tones with ink-like text
- Cozy, handcrafted feel — rough edges, pixel borders, no sharp modern UI chrome
- Map is the primary navigation surface; user's 64×64 avatar replaces a standard location dot
- Billboard expanded view: ~60vh overlay on mobile
- Username pills float above billboards (always visible) and stickers (visible on tap)
- POI markers are visually distinct from billboard markers
- In-app pixel art editor: 64×64 grid, fixed 8-colour palette, tap-to-fill
- Colour palette and cosmetic flairs unlock with level
| Question | Decision |
|---|---|
| Primary key format | UUIDv4 for internal primary keys; Clerk IDs are unique auth-provider identifiers, not primary keys |
| Billboard limits | Concurrent cap + Sydney-day posting cap; posting at the concurrent cap replaces the user's oldest active billboard |
| POI geofence trigger radius | 30m |
| Map provider | react-native-leaflet-view + OSM |
| Sticker storage format | base64 PNG |
| Quest system | Parameterised templates (visit N POIs, leave N notes, place N stickers, receive N replies, save N stickers) with per-level randomised values |
| Daily quest variety | Seeded pool of ~5 curated daily quests; one is randomly assigned per Sydney calendar day |
| Push notification timing | Morning (8–9am) |
| POI picture format | Compressed 128×128 base64 PNG |
| User avatar format | 64×64 pixel art, drawn on sign-up, stored as base64 PNG |