← All projects

02 — Mobile

Mongmo

Year
2026
Role
Design & development
Context
Devine (Howest) — Creative Development, React challenge
Stack
Expo, React Native, TypeScript, Supabase, PostgreSQL, TanStack Query, Zustand

A cross-platform application for discovering Mongmo's pop-up events, completing location-based challenges, earning rewards and ordering ahead. Built with React Native and a Supabase backend.

Mongmo's sign-in screen: Pop-ups, points, pickup

The app

01

The project

Mongmo is a Vietnamese café brand that organises temporary pop-ups across Belgium, including brunches, DJ nights and market events.

Read more Read less

I designed and developed a cross-platform application that helps customers discover upcoming pop-ups, check in at venues, participate in challenges and earn rewards. Customers can also order ahead for pickup.

The application includes an owner interface for creating events, managing challenges and tracking incoming orders.

Customers

Owners

02

Why a mobile application?

Mongmo’s events are temporary and frequently change location. The application needed to help customers discover nearby events and interact with them while physically attending.

Read more Read less

Several features depend on native device capabilities:

  • Geofencing: Notifies customers when they enter a 500 m region around an active event.
  • QR and GPS check-in: Combines a scanned QR code with location verification.
  • Interactive challenges: Uses the camera and motion sensors for activities such as photo submissions and dancing.
  • Notifications: Provides updates about events, orders and rewards.

The application also supports web through React Native Web, with alternative implementations for features that require native APIs.

03

Stack

Expo provides access to native functionality while maintaining a shared TypeScript codebase across platforms.

Read more Read less

React Native · Expo · TypeScript · Supabase · PostgreSQL · TanStack Query · Zustand

Supabase handles authentication, PostgreSQL, storage and realtime updates. TanStack Query manages server state and caching, while Zustand handles local state such as the shopping cart, filters and settings.

One TypeScript codebase · iOS, Android, web Expo · React Native
TanStack QueryServer state, caching
ZustandCart, filters, settings
Supabase
AuthPostgreSQLStorageRealtime

Engineering decisions

04

Server-authoritative architecture

One of the main architectural decisions was keeping business rules on the server rather than trusting data submitted by the application.

Read more Read less

For example, when a customer places an order, the client sends product IDs and quantities, but not prices.

const { data: order, error } = await supabase.rpc('create_order', {
  p_event_id: location.type === 'event' ? location.eventId : null,
  p_items: items.map((item) => ({
    menu_item_id: item.menuItemId,
    quantity: item.quantity,
  })),
  p_pickup_at: pickupAt,
  p_user_reward_id: userRewardId,
});

if (error) throw toApiError(error);

PostgreSQL retrieves the current prices, validates the items and stores a snapshot of the order. This prevents client-side price manipulation and preserves historical receipts when menu prices change.

The same approach applies to check-ins. The client submits the QR token and device coordinates, while the server verifies the event, its operating hours and the customer’s distance from the venue.

Client-side checks provide immediate feedback, but the server makes the final decision.

Guest orders

Owner's board

05

Database security

Supabase uses PostgreSQL row-level security to control data access. However, some operations required additional restrictions.

Read more Read less

For example, an event’s QR token is stored alongside public event information. Granting unrestricted read access would expose the token and compromise the check-in process.

I restricted access to sensitive columns and implemented a separate function that allows verified event owners to retrieve their QR codes.

During a security review, I also identified an issue with PostgreSQL SECURITY DEFINER functions.

An internal function responsible for awarding badges could be called directly by authenticated users, potentially allowing them to award themselves badges and points.

I corrected this by explicitly revoking execution permissions from public roles:

revoke execute
on function public.award_badge(uuid, text)
from public, anon, authenticated;

Privileged functions are now accessible only through the intended server-side operations.

One pop-up's row Guests can read
Title ✓ Yes
Location ✓ Yes
Date ✓ Yes
QR token Owners only, through a function that checks ownership ✕ No
06

Configurable challenge system

Mongmo supports eleven challenge types, including photo submissions, ratings, voting, timed activities and motion-based challenges.

Read more Read less

Rather than implementing a separate data structure for every challenge, I created a shared model with type-specific configuration.

TypeScript uses a discriminated union to associate each challenge type with its required fields:

export type ChallengeRules = {
  [Type in ChallengeType]: {
    type: Type;
    config: ChallengeConfigByType[Type];
  };
}[ChallengeType];

This provides type checking during development while allowing new challenge types to be added without restructuring the entire system.

The database also validates configurations and challenge completion. For example, photo challenges verify uploaded files in storage instead of relying on the client’s completion status.

07

Points and rewards

Instead of storing a single points balance on each user’s profile, Mongmo uses an append-only transaction ledger.

Read more Read less

Completing challenges and earning badges creates positive entries, while redeeming rewards creates negative entries.

The balance is calculated from the sum of these transactions.

This provides a history of how points were earned or spent, avoids overwriting balances during simultaneous operations and supports calculating leaderboards over different periods.

The trade-off is that balances require aggregation when retrieved, which is acceptable at the application’s current scale.

Ledger · example

  1. Challenge · Rate the best dish +20
  2. Challenge · Latte art +15
  3. Challenge · Early riser +20
  4. Reward redeemed −30

Balance = sum of entries 25

Building around the phone

08

Native features and cross-platform development

The application uses Expo’s native APIs for location, camera access, notifications, motion sensors and calendar integration.

Read more Read less

Platform-specific functionality is separated into dedicated implementations.

For example, adding an event to the calendar opens the native calendar interface on iOS and Android. On the web, the same function generates an .ics file.

Metro selects the appropriate platform implementation automatically, allowing the rest of the application to use the same interface without repeated platform checks.

Geofencing also uses background location tasks to detect when customers enter the region around an active pop-up. Notifications are checked against event dates and previous alerts to avoid notifying users about expired events.

Feature iOS Android Web
Calendar System event sheet System event sheet Downloads an .ics file
Motion Device motion sensor Device motion sensor Browser devicemotion event
Icons SF Symbols Lucide Lucide
Haptics Haptic feedback Haptic feedback Quietly off
09

Debugging the motion sensor

One of the most challenging issues occurred while implementing the dance challenge, which measures how long a customer is actively moving.

Read more Read less

The original implementation calculated dancing time using the motion sensor’s reported interval:

total += interval || SAMPLE_MS;

During testing on a physical iPhone, the progress meter barely increased.

After tracing the value into the library’s native Swift implementation, I discovered that iOS reported the interval in seconds, while the application expected milliseconds.

An interval of 0.1 represented 100 milliseconds but was effectively being counted as 0.1 milliseconds.

Rather than depending on the library’s interval value, I changed the implementation to calculate elapsed time between sensor readings.

The revised algorithm also caps the time between readings to prevent interruptions from incorrectly increasing progress.

I extracted the calculation into a pure function, making it possible to test the behaviour with simulated sensor data without requiring a physical device.

The investigation also revealed a separate compatibility issue with the web implementation. I addressed this by separating native and browser motion handling, using the browser’s devicemotion event for web.

Wiggle your cursor in here

0.0 / 5.0 s

Same idea as the fix: it measures the time between readings itself, and caps it.

Looking back

10

Takeaway

Mongmo gave me experience developing a cross-platform application that combines native device functionality with a relational backend.

The main technical challenges involved designing reusable systems for different challenge types, securing operations involving points and rewards, and ensuring that business rules were enforced independently of the client.

Debugging the motion sensor also reinforced the importance of testing on physical devices and inspecting third-party implementations when their behaviour differs from the documentation.