A full-stack mobile journaling app for tracking mood over time β built solo from scratch with React Native, Expo, and Supabase.
Users can log daily entries, browse their history in a calendar or list view, visualize mood trends with live stats, and personalize their profile. Authentication is handled via OAuth (GitHub / Google) with secure, per-user data enforced at the database level through Row Level Security.
- π Calendar view β browse past entries by date
- π List view β scrollable entry history
- π Mood stats β percentage breakdown of moods over time
- π€ Customizable profile β username, full name, avatar upload
- π OAuth authentication β GitHub & Google via Supabase Auth
- π Row Level Security β users only ever see their own data
| Layer | Technology |
|---|---|
| Framework | React Native 0.81 + Expo SDK 54 |
| Routing | Expo Router (file-based) |
| Language | TypeScript |
| Backend | Supabase (Auth, PostgreSQL, Storage) |
| Auth | OAuth 2.0 β GitHub & Google |
| Deployment | EAS Build (Android APK) |
- Node.js
- EAS CLI (
npm install -g eas-cli) - ADB installed and available in your PATH
- Android Studio with a Pixel 6 AVD configured (optional β only needed if using the emulator)
# First time β full native compile (~2β5 min)
make build
# Daily β Metro bundler only (APK already installed)
make dev
# Physical device or tunnel needed
make tunnel| Command | Description |
|---|---|
make build |
Compile native Android code, install APK, start Metro |
make dev |
Start Metro only β no recompile (use daily) |
| Command | Description |
|---|---|
make studio |
Boot Android emulator manually |
make tunnel |
Metro with tunnel (physical devices) |
make web |
Expo web |
make install |
Install JS dependencies |
make lint |
Run ESLint |
make depcheck |
Check for unused dependencies |
make pics |
Push assets to emulator gallery |
| Command | What it removes |
|---|---|
make clean |
node_modules |
make clean-build |
Android build output (~700MB) |
make clean-android |
Entire android/ folder |
make clean-cache |
Metro/Expo cache |
make clean-emulator |
Wipes emulator data |
make fclean |
All of the above + package-lock.json |
make re # fclean + reinstall + prebuild + buildapp/
βββ _layout.tsx β root layout with auth guard
βββ index.tsx β returns null, lets layout handle routing
βββ (auth)/
β βββ _layout.tsx β minimal <Slot />
β βββ landing.tsx
βββ (app)/
βββ _layout.tsx β minimal <Slot />
βββ diary.tsx
Uses Supabase OAuth (GitHub / Google) with a custom dev build β Expo Go cannot handle custom URI schemes.
const redirectTo = makeRedirectUri({ native: 'diaryapp://' })Supabase redirect URLs to configure in the dashboard (Authentication β URL Configuration):
diaryapp://
exp://*/*
http://localhost:8081
create table public.profiles (
id uuid references auth.users(id) on delete cascade primary key,
username text,
avatar_url text,
full_name text,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
create table public.entries (
id uuid default gen_random_uuid() primary key,
user_id uuid references public.profiles(id) on delete cascade not null,
title text not null,
content text,
mood text,
created_at timestamptz default now(),
updated_at timestamptz default now()
);Row Level Security means you never need to manually pass user_id in queries β Supabase reads auth.uid() from the JWT on every request.
CREATE POLICY "users see own entries"
ON entries FOR SELECT
USING (auth.uid() = user_id);-- Mood breakdown as percentages
CREATE OR REPLACE VIEW mood_percentages AS
WITH mood_counts AS (
SELECT mood, COUNT(*) AS count
FROM entries
WHERE mood IS NOT NULL AND mood != ''
GROUP BY mood
),
total AS (
SELECT COUNT(*) AS total_count
FROM entries
WHERE mood IS NOT NULL AND mood != ''
)
SELECT mood, count,
ROUND((count::numeric / total_count) * 100, 2) AS percentage
FROM mood_counts, total
ORDER BY percentage DESC;
-- Total entries per user
CREATE OR REPLACE VIEW user_entry_stats AS
SELECT user_id, COUNT(*) AS total_entries
FROM entries
GROUP BY user_id;
-- Make views respect RLS
ALTER VIEW mood_percentages SET (security_invoker = true);
ALTER VIEW user_entry_stats SET (security_invoker = true);Build the APK remotely via EAS:
eas build --profile development --platform androidDownload the .apk from expo.dev, then install it directly on your phone or emulator:
adb install /path/to/your-build.apkThen start Metro:
make dev # or: make tunnel for physical devicemake build # first time (~2β5 min)
make dev # daily- Settings β About phone β tap Build number 7 times
- Settings β Developer options β enable USB debugging
- Plug in via USB
adb devicesβ confirm it's listed- Either install the APK via
adb installor runmake build(Expo will ask which target)
iOS requires macOS + Xcode. On Linux, use EAS cloud build:
npx eas build --platform ios --profile developmentRequires an Apple Developer account ($99/year). Install via TestFlight.
EXPO_PUBLIC_SUPABASE_URL=...
EXPO_PUBLIC_SUPABASE_ANON_KEY=...
Expo requires the EXPO_PUBLIC_ prefix for client-side env vars.
import { createClient } from '@supabase/supabase-js'
import AsyncStorage from '@react-native-async-storage/async-storage'
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL!,
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
{ auth: { storage: AsyncStorage } }
)AsyncStorage is required β React Native has no localStorage, so without it the session dies on unmount.
