# Project export: Proof: Because Proof Beats Promises

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: Proof is a visual habit tracker that helps users build better routines with AI-verified photo check-ins, AI goal coaching, and smart progress verification using Claude and Gemini via Letta.
- Devpost: https://devpost.com/software/proof-because-proof-beats-promises
- GitHub: https://github.com/MaazDhalech/Proof
- Video: https://www.youtube.com/embed/pkn4MXjBNwc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — MaazDhalech (20 commits), Babiker Babiker (19 commits), sameer-rahman (9 commits), rahilpasha (8 commits)

## Devpost submission (written by the team)

### Inspiration

We built Proof because we were tired of making promises we couldn’t keep. Everyone has goals—whether it’s reading more, eating better, or running regularly—but it’s easy to fall off track without accountability. We wanted a tool that not only helps you stay consistent but also makes your progress visible and real. Proof is our way of turning intention into action—because proof beats promises.

### What it does

Proof is a visual habit tracker that helps users build better routines through photo check-ins and intelligent coaching. Users set personal goals and log their progress with images—whether it's a healthy meal, a completed workout, or a clean desk. An in-app chatbot, Letta, helps refine goals and suggests personalized, actionable check-in prompts. Gemini Vision verifies check-ins for goal relevance, while Claude (via Letta’s stateful backend) helps users brainstorm and stay motivated. All check-ins and chats are logged to Supabase, creating a secure, timeline-based system of visual accountability.

### How we built it

We built Proof using React Native with Expo, using Supabase for authentication, database storage, and real-time data syncing. We implemented an in-app chatbot (Letta) using Claude via Letta’s hosted API and used Gemini Vision for image-based goal verification. The backend stores chats and check-ins using normalized Supabase tables (profiles, goals, chats, chat_messages, and proofs). Letta was designed as a goal brainstorming assistant, giving dynamic, context-aware suggestions to encourage meaningful, habit-forming behaviors.

### Challenges we ran into

Integrating Letta’s stateful AI was a huge lift. Mapping structured goal data to unstructured chat input and interpreting user photos introduced edge cases that often broke expected behavior or resulted in irrelevant responses. React Native camera integration was buggy across platforms. From handling permission inconsistencies to properly formatting media for Supabase uploads, getting reliable, cross-platform behavior required a ton of trial and error. Managing chat memory and syncing with Supabase was fragile. We struggled with state persistence, real-time updates, and avoiding race conditions between user input, API inference, and Supabase logging.

### Accomplishments we're proud of

Built a fully functional mobile app in just 24 hours. Seamlessly integrated Claude and Gemini into the user flow to power real-time conversations and image analysis. Launched a complete goal-check-in-verification pipeline: from photo input to AI interpretation to structured Supabase logging. Created a dynamic, personalized chatbot that helps users reflect and stay engaged with their goals.

### What we learned

Learned how to implement stateful AI agents using Letta and Claude to guide users through adaptive, evolving goals. Gained experience using Supabase as a scalable mobile backend, including authentication, chat logs, and structured data. Built social graph structures and stored early-stage relationship data for features like friends, group goals, and check-in feeds. Developed reliable media upload pipelines from React Native to cloud storage, ensuring smooth user-generated content flows.

### What's next

Support for video check-ins Group goals and collaborative progress Notifications prompting users for timed goals Private messaging and close-friend tracking Community features to discover and join shared goals More dynamic goals to fit the user's needs AI-powered smart nudges and reflections

## README (from the GitHub repository)

# Proof

Social accountability mobile app built with Expo Router and Supabase. Create challenges, check in to keep streaks alive, and share photo “proof” with friends who can like and cheer you on.

## Repo layout
- `Proof/` – the Expo app source.
- `.env` – local environment secrets (kept out of git).

## Quickstart
```bash
cd Proof          # app source directory
npm install       # install dependencies
npm run start     # choose iOS / Android / web
```
If Metro cache acts up: `npx expo start --clear`.

## Environment
Create a `.env` in the repo root (already ignored):
```
PUBLIC_SUPABASE_URL=...
ANON_PUBLIC_KEY=...
SERVICE_ROLE_KEY=...
LETTA_API_KEY=...
LETTA_CHATBOT_MODEL=...
LETTA_IMAGE_VERIFIER_MODEL=...
```

## What’s inside
- Goal tracking: challenges with start/end dates, frequencies, streaks, and check‑ins.
- Social feed: photo proofs with captions, likes/unlikes.
- Friends: send/accept requests; view friends’ active goals.
- AI assistant: floating Letta chatbot for quick coaching.

## Scripts (from `Proof/`)
- `npm run start` – run Metro and select target.
- `npm run ios` / `npm run android` / `npm run web` – platform shortcuts.
- `npm run lint` – lint with Expo config.

## Contributing
Keep secrets in `.env`. Document Supabase schema/policy changes. Prefer typed `expo-router` routes for new screens.


## Detected evidence (automated analysis)

Indexed codebase: 40 recognized source files, 225 KB.
- JavaScript (language) — detected in the code
- React (technology) — detected in the code
- Supabase (technology) — detected in the code
- TypeScript (language) — detected in the code
- Anthropic (technology) — claimed on Devpost, not found in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (54 of 54)

```
.DS_Store
.expo/README.md
.expo/settings.json
.gitignore
package.json
Proof/.gitignore
Proof/.vscode/extensions.json
Proof/.vscode/settings.json
Proof/app.config.js
Proof/app/_layout.tsx
Proof/app/(auth)/forgot.tsx
Proof/app/(auth)/signin.tsx
Proof/app/(auth)/signup.tsx
Proof/app/(stack)/goals/[goalID]/check-in.tsx
Proof/app/(stack)/goals/[goalID]/index.tsx
Proof/app/(stack)/goals/create.tsx
Proof/app/(stack)/goals/friends/explore.tsx
Proof/app/(stack)/goals/friends/requests.tsx
Proof/app/(tabs)/_layout.tsx
Proof/app/(tabs)/friends.tsx
Proof/app/(tabs)/goals.tsx
Proof/app/(tabs)/index.tsx
Proof/app/(tabs)/profile.tsx
Proof/app/(tabs)/proofs.tsx
Proof/app/+not-found.tsx
Proof/components/Collapsible.tsx
Proof/components/ExternalLink.tsx
Proof/components/HapticTab.tsx
Proof/components/HelloWave.tsx
Proof/components/ParallaxScrollView.tsx
Proof/components/ThemedText.tsx
Proof/components/ThemedView.tsx
Proof/components/ui/IconSymbol.ios.tsx
Proof/components/ui/IconSymbol.tsx
Proof/components/ui/TabBarBackground.ios.tsx
Proof/components/ui/TabBarBackground.tsx
Proof/constants/Colors.ts
Proof/eas.json
Proof/eslint.config.js
Proof/hooks/useColorScheme.ts
Proof/hooks/useColorScheme.web.ts
Proof/hooks/usePushNotifications.ts
Proof/hooks/useThemeColor.ts
Proof/package.json
Proof/README.md
Proof/scripts/reset-project.js
Proof/services/supabase.ts
Proof/supabase/.gitignore
Proof/supabase/config.toml
Proof/supabase/functions/send-checkin-reminder/.npmrc
Proof/supabase/functions/send-checkin-reminder/deno.json
Proof/supabase/functions/send-checkin-reminder/index.ts
Proof/tsconfig.json
README.md
```

### Dependencies

- Proof/package.json: @babel/core@^7.25.2, @expo/vector-icons@^14.1.0, @react-native-community/datetimepicker@8.3.0, @react-native-community/slider@^5.0.1, @react-native-picker/picker@^2.11.1, @react-navigation/bottom-tabs@^7.4.0, @react-navigation/elements@^2.3.8, @react-navigation/native@^7.1.14, @supabase/supabase-js@^2.50.0, @types/react@~19.0.10, axios@^1.10.0, bad-words@^4.0.0, dayjs@^1.11.13, dotenv@^16.5.0, eslint@^9.29.0, eslint-config-expo@~9.2.0, expo@~53.0.12, expo-blur@~14.1.5, expo-camera@~16.1.8, expo-constants@~17.1.6, expo-dev-client@~5.2.1, expo-device@~7.1.4, expo-font@~13.3.1, expo-haptics@~14.1.4, expo-image@~2.3.0, expo-image-picker@~16.1.4, expo-linking@~7.1.5, expo-media-library@~17.1.7, expo-modules-core@^2.4.0, expo-notifications@~0.31.3, expo-router@~5.1.0, expo-secure-store@~14.2.3, expo-splash-screen@~0.30.9, expo-status-bar@~2.2.3, expo-symbols@~0.4.5, expo-system-ui@~5.0.9, expo-web-browser@~14.2.0, nativewind@^4.1.23, prettier@^3.5.3, react@19.0.0, react-dom@19.0.0, react-native@0.79.4, react-native-calendars@^1.1313.0, react-native-element-dropdown@^2.12.4, react-native-gesture-handler@~2.24.0, react-native-reanimated@~3.17.4, react-native-safe-area-context@5.4.0, react-native-screens@~4.11.1, react-native-vector-icons@^10.2.0, react-native-web@~0.20.0, react-native-webview@13.13.5, typescript@~5.8.3

### Recent commits (newest first)

- created README
- Merge pull request #8 from MaazDhalech/feature/Goals
- Fixed the create new goal feature
- Merge pull request #6 from MaazDhalech/feature/friends-revamp
- updated requests to properly display incoming vs outgoing requests
- update to navbar look
- updated relavent pages to query the new friends table and use the new system to display info
- updated friends system, needs testing
- Merge pull request #5 from MaazDhalech/goals
- Merge branch 'main' into goals
- Changed the goals page UI to have archives button
- Added indefinite goals
- Only shows friend once on the friends page
- Added an archived goals tab
- Merge pull request #3 from MaazDhalech/feature/contact-support
- Merge pull request #2 from MaazDhalech/feature/tos_and_privacy_policy
- Merge pull request #4 from MaazDhalech/feature/proofs-page
- Removed Friends from goals page
- Removed Letta and cleaned up package-lock
- Can now delete goals

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{}

```

### Proof/package.json

```
{
  "name": "proof",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "start": "expo start",
    "reset-project": "node ./scripts/reset-project.js",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "lint": "expo lint"
  },
  "dependencies": {
    "@expo/vector-icons": "^14.1.0",
    "@react-native-community/datetimepicker": "8.3.0",
    "@react-native-community/slider": "^5.0.1",
    "@react-native-picker/picker": "^2.11.1",
    "@react-navigation/bottom-tabs": "^7.4.0",
    "@react-navigation/elements": "^2.3.8",
    "@react-navigation/native": "^7.1.14",
    "@supabase/supabase-js": "^2.50.0",
    "axios": "^1.10.0",
    "bad-words": "^4.0.0",
    "dayjs": "^1.11.13",
    "dotenv": "^16.5.0",
    "expo": "~53.0.12",
    "expo-blur": "~14.1.5",
    "expo-camera": "~16.1.8",
    "expo-constants": "~17.1.6",
    "expo-dev-client": "~5.2.1",
    "expo-device": "~7.1.4",
    "expo-font": "~13.3.1",
    "expo-haptics": "~14.1.4",
    "expo-image": "~2.3.0",
    "expo-image-picker": "~16.1.4",
    "expo-linking": "~7.1.5",
    "expo-media-library": "~17.1.7",
    "expo-modules-core": "^2.4.0",
    "expo-notifications": "~0.31.3",
    "expo-router": "~5.1.0",
    "expo-secure-store": "~14.2.3",
    "expo-splash-screen": "~0.30.9",
    "expo-status-bar": "~2.2.3",
    "expo-symbols": "~0.4.5",
    "expo-system-ui": "~5.0.9",
    "expo-web-browser": "~14.2.0",
    "nativewind": "^4.1.23",
    "prettier": "^3.5.3",
    "react": "19.0.0",
    "react-dom": "19.0.0",
    "react-native": "0.79.4",
    "react-native-calendars": "^1.1313.0",
    "react-native-element-dropdown": "^2.12.4",
    "react-native-gesture-handler": "~2.24.0",
    "react-native-reanimated": "~3.17.4",
    "react-native-safe-area-context": "5.4.0",
    "react-native-screens": "~4.11.1",
    "react-native-vector-icons": "^10.2.0",
    "react-native-web": "~0.20.0",
    "react-native-webview": "13.13.5"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@types/react": "~19.0.10",
    "eslint": "^9.29.0",
    "eslint-config-expo": "~9.2.0",
    "typescript": "~5.8.3"
  }
}

```

### Proof/app/_layout.tsx

```typescript
import { DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Slot, useRouter, useSegments } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';
import { useEffect, useState } from 'react';
import { ActivityIndicator, View } from 'react-native';
import 'react-native-reanimated';

import { useColorScheme } from '@/hooks/useColorScheme';
import { supabase } from '@/services/supabase';

// Keep the splash screen visible while we fetch resources
SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const colorScheme = useColorScheme();
  const router = useRouter();
  const segments = useSegments();

  const [appReady, setAppReady] = useState(false);
  const [fontsLoaded] = useFonts({
    SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
  });

  useEffect(() => {
    let subscription: ReturnType<typeof supabase.auth.onAuthStateChange>['data']['subscription'];

    const initializeApp = async () => {
      try {
        if (!fontsLoaded) return;

        // Check for existing session
        const { data: { session } } = await supabase.auth.getSession();
        const inAuthGroup = segments[0] === '(auth)';

        // Handle initial routing
        if (session && inAuthGroup) {
          router.replace('/');
        } else if (!session && !inAuthGroup) {
          router.replace('/signin');
        }

        // Listen for future auth changes
        const authListener = supabase.auth.onAuthStateChange((_event, session) => {
          const currentInAuthGroup = segments[0] === '(auth)';

          if (session && currentInAuthGroup) {
            router.replace('/');
          } else if (!session && !currentInAuthGroup) {
            router.replace('/signin');
          }
        });

        subscription = authListener.data.subscription;

        setAppReady(true);
        await SplashScreen.hideAsync();
      } catch (error) {
        console.error('Initialization error:', error);
      }
    };

    initializeApp();

    return () => {
      subscription?.unsubscribe();
    };
  }, [fontsLoaded, segments]);

  if (!appReady) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  return (
    <ThemeProvider value={DefaultTheme}>
      <Slot />
      <StatusBar style="dark" />
    </ThemeProvider>
  );
}

```

### Proof/app/(tabs)/_layout.tsx

```typescript
import { Tabs, router } from "expo-router";
import React, { useEffect, useState } from "react";

import { HapticTab } from "@/components/HapticTab";
import { IconSymbol } from "@/components/ui/IconSymbol";
import TabBarBackground from "@/components/ui/TabBarBackground";
import { useColorScheme } from "@/hooks/useColorScheme";
import { supabase } from "@/services/supabase";

export default function TabLayout() {
  const colorScheme = useColorScheme();
  const [authChecked, setAuthChecked] = useState(false);

  useEffect(() => {
    const checkAuth = async () => {
      const { data, error } = await supabase.auth.getSession();

      if (error || !data.session) {
        if (__DEV__)
          console.warn("User not authenticated, redirecting to /signin");
        router.replace("/(auth)/signin");
      } else {
        setAuthChecked(true);
      }
    };

    checkAuth();

    const { data: listener } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        if (!session) {
          router.replace("/(auth)/signin");
        }
      }
    );

    return () => {
      listener.subscription.unsubscribe();
    };
  }, []);

  if (!authChecked) return null;

  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: "#ffffff", // Pure white for active tabs
        tabBarInactiveTintColor: "#666666", // Subtle gray for inactive tabs
        headerShown: false,
        tabBarButton: HapticTab,
        tabBarBackground: TabBarBackground,
        tabBarStyle: {
          backgroundColor: "#000000", // Pure black background
          borderTopWidth: 0, // Remove top border
          paddingTop: 8, // Add some top padding
          paddingBottom: 8, // Add bottom padding for better spacing
          height: 88, // Slightly taller for better proportions
          shadowColor: "#000",
          shadowOffset: {
            width: 0,
            height: -2,
          },
          shadowOpacity: 0.1,
          shadowRadius: 3,
          elevation: 8, // Android shadow
        },
        tabBarLabelStyle: {
          fontSize: 11,
          fontWeight: "600", // Semi-bold for better readability
          marginTop: 4,
        },
        tabBarIconStyle: {
          marginTop: 2,
        },
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: "Home",
          tabBarIcon: ({ color, focused }) => (
            <IconSymbol 
              size={focused ? 30 : 28} 
              name="house.fill" 
              color={color} 
            />
          ),
        }}
      />
      <Tabs.Screen
        name="goals"
        options={{
          title: "Goals",
          tabBarIcon: ({ color, focused }) => (
            <IconSymbol 
              size={focused ? 30 : 28} 
              name="target" 
              color={color} 
            />
          ),
        }}
      />
      <Tabs.Screen
        name="proofs"
        options={{
          title: "Proofs",
          tabBarIcon: ({ color, focused }) => (
            <IconSymbol 
              size={focused ? 30 : 28} 
              name="checkmark.seal.fill" 
              color={color} 
            />
          ),
        }}
      />
      <Tabs.Screen
        name="friends"
        options={{
          title: "Friends",
          tabBarIcon: ({ color, focused }) => (
            <IconSymbol 
              size={focused ? 30 : 28} 
              name="person.2.fill" 
              color={color} 
            />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: "Profile",
          tabBarIcon: ({ color, focused }) => (
            <IconSymbol 
              size={focused ? 30 : 28} 
              name="person.fill" 
              color={color} 
            />
          ),
        }}
      />
    </Tabs>
  );
}
```

### Proof/app/(tabs)/index.tsx

```typescript
import { supabase } from "@/services/supabase";
import { useRouter } from "expo-router";
import { useEffect, useState } from "react";
import {
  ActivityIndicator,
  Alert,
  FlatList,
  Image,
  RefreshControl,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from "react-native";

interface Post {
  id: string;
  user_id: string;
  caption: string;
  created_at: string;
  picture_url: string;
  likes: number;
  profile: {
    username: string;
    first_name: string | null;
    last_name: string | null;
    profile_picture: string;
  };
  challenges: {
    name: string;
  };
  userHasLiked?: boolean;
}

export default function HomeFeedScreen() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [loading, setLoading] = useState(true);
  const [refreshing, setRefreshing] = useState(false);
  const [userId, setUserId] = useState<string | null>(null);
  const [likingPosts, setLikingPosts] = useState<Set<string>>(new Set());
  const router = useRouter();

  useEffect(() => {
    loadUserAndData();
  }, []);

  const loadUserAndData = async () => {
    setLoading(true);
    const {
      data: { session },
      error,
    } = await supabase.auth.getSession();

    if (error) {
      console.error("Error fetching session:", error.message);
      setLoading(false);
      return;
    }

    const currentUserId = session?.user?.id ?? null;
    setUserId(currentUserId);

    if (currentUserId) {
      await fetchFeed(currentUserId);
    } else {
      console.warn("No authenticated user found.");
    }
    setLoading(false);
  };

  const fetchFeed = async (currentUserId: string) => {
    try {
      // Select both user1_id and user2_id to handle bidirectional friendships
      const { data: friendData, error: friendError } = await supabase
        .from("friends")
        .select("user1_id, user2_id")
        .eq("status", "accepted")
        .or(`user1_id.eq.${currentUserId},user2_id.eq.${currentUserId}`);

      if (friendError) throw friendError;

      // Extract friend IDs from both directions of the relationship
      const friendIds = friendData?.reduce((acc: string[], entry) => {
        if (entry.user1_id === currentUserId) {
          acc.push(entry.user2_id);
        } else if (entry.user2_id === currentUserId) {
          acc.push(entry.user1_id);
        }
        return acc;
      }, []) || [];

      if (!friendIds.length) {
        setPosts([]);
        return;
      }

      const { data: postData, error: postError } = await supabase
        .from("proof")
        .select(
          `
          id,
          user_id,
          caption,
          created_at,
          picture_url,
          likes,
          profile:user_id (
            username,
            first_name,
            last_name,
            profile_picture
          ),
          challenges:challenge_id (
            name
          )
        `
        )
        .in("user_id", friendIds)
        .order("created_at", { ascending: false });

      if (postError) throw postError;

      // Check which posts the current user has liked
      const postIds = postData?.map((post) => post.id) || [];
      const { data: userLikes, error: likesError } = await supabase
        .from("user_likes")
        .select("proof_id")
        .eq("user_id", currentUserId)
        .in("proof_id", postIds);

      if (likesError) {
        console.warn("Could not fetch user likes:", likesError);
      }

      const likedPostIds = new Set(
        userLikes?.map((like) => like.proof_id) || []
      );

      const postsWithLikeStatus =
        postData?.map((post) => ({
          ...post,
          profile: Array.isArray(post.profile) ? post.profile[0] : post.profile,
          challenges: Array.isArray(post.challenges)
            ? post.challenges[0]
            : post.challenges,
          userHasLiked: likedPostIds.has(post.id),
        })) || [];

      setPosts(postsWithLikeStatus);
    } catch (err) {
      console.error("Feed load error:", err);
      Alert.alert("Error", "Failed to load feed. Please try again.");
    }
  };

  const onRefresh = async () => {
    if (!userId) return;
    setRefreshing(true);
    await fetchFeed(userId);
    setRefreshing(false);
  };

  const handleLike = async (
    postId: string,
    currentLikes: number,
    userHasLiked: boolean
  ) => {
    if (!userId || likingPosts.has(postId)) return;

    setLikingPosts((prev) => new Set(prev).add(postId));

    try {
      if (userHasLiked) {
        // Unlike the post
        const { error: unlikeError } = await supabase
          .from("user_likes")
          .delete()
          .eq("user_id", userId)
          .eq("proof_id", postId);

        if (unlikeError) throw unlikeError;

        const { error: decrementError } = await supabase
          .from("proof")
          .update({ likes: Math.max(0, currentLikes - 1) })
          .eq("id", postId);

        if (decrementError) throw decrementError;

        // Update local state
        setPosts((prevPosts) =>
          prevPosts.map((post) =>
            post.id === postId
              ? {
                  ...post,
                  likes: Math.max(0, post.likes - 1),
                  userHasLiked: false,
                }
              : post
          )
        );
      } else {
        // Like the post
        const { error: likeError } = await supabase
          .from("user_likes")
          .insert({ user_id: userId, proof_id: postId });

        if (likeError) throw likeError;

        const { error: incrementError } = await supabase
          .from("proof")
          .update({ likes: currentLikes + 1 })
          .eq("id", postId);

        if (incrementError) throw incrementError;

        // Update local state
        setPosts((prevPosts) =>
          prevPosts.map((post) =>
            post.id === postId
              ? { ...post, likes: post.likes + 1, userHasLiked: true }
              : post
          )
        );
      }
    } catch (error) {
      console.error("Error toggli
[truncated — 7353 more characters]
```

### Proof/supabase/functions/send-checkin-reminder/index.ts

```typescript
import "jsr:@supabase/functions-js/edge-runtime.d.ts";

Deno.serve(async (req: Request) => {
  try {
    const { user_id, title, body } = await req.json();

    if (!user_id || !title || !body) {
      return new Response(JSON.stringify({ error: "Missing required fields" }), {
        status: 400,
        headers: { "Content-Type": "application/json" },
      });
    }

    // Fetch user's Expo push token from Supabase
    const supabaseUrl = Deno.env.get("PUBLIC_SUPABASE_URL")!;
    const supabaseKey = Deno.env.get("SERVICE_SUPABASE_SERVICE_ROLE_KEY")!;

    const userRes = await fetch(`${supabaseUrl}/rest/v1/users?id=eq.${user_id}`, {
      headers: {
        apikey: supabaseKey,
        Authorization: `Bearer ${supabaseKey}`,
      },
    });

    const userData = await userRes.json();

    if (!userData.length || !userData[0].expo_push_token) {
      return new Response(JSON.stringify({ error: "User or push token not found" }), {
        status: 404,
        headers: { "Content-Type": "application/json" },
      });
    }

    const expoPushToken = userData[0].expo_push_token;

    // Send push notification via Expo API
    const expoRes = await fetch("https://exp.host/--/api/v2/push/send", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify([
        {
          to: expoPushToken,
          sound: "default",
          title,
          body,
        },
      ]),
    });

    const result = await expoRes.json();

    return new Response(JSON.stringify({ status: "sent", result }), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });
  } catch (err) {
    const message = err instanceof Error ? err.message : "Unknown error";

    return new Response(JSON.stringify({ error: message }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });
  }
});

```

### Proof/app/(stack)/goals/[goalID]/index.tsx

```typescript
// app/goals/[goalId]/index.tsx
import { useLocalSearchParams } from 'expo-router';
import { StyleSheet, Text, View } from 'react-native';

export default function GoalDetailsScreen() {
  const { goalId } = useLocalSearchParams();

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Goal ID: {goalId}</Text>
      {/* Replace with actual goal detail logic */}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  title: { fontSize: 20, fontWeight: 'bold' },
});

```

### Proof/eslint.config.js

```javascript
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');

module.exports = defineConfig([
  expoConfig,
  {
    ignores: ['dist/*'],
  },
]);

```

### Proof/app.config.js

```javascript
export default {
  expo: {
    name: "Proof",
    slug: "proof",
    version: "1.0.0",
    orientation: "portrait",
    icon: "./assets/images/icon.png",
    scheme: "proof",
    userInterfaceStyle: "automatic",
    newArchEnabled: true,
    ios: {
      supportsTablet: true,
      bundleIdentifier: "com.proof.proof",
      entitlements: {
        "aps-environment": "development", // ✅ Enables push notifications on dev builds
      },
    },
    android: {
      adaptiveIcon: {
        foregroundImage: "./assets/images/adaptive-icon.png",
        backgroundColor: "#ffffff",
      },
      edgeToEdgeEnabled: true,
    },
    web: {
      bundler: "metro",
      output: "static",
      favicon: "./assets/images/favicon.png",
    },
    plugins: [
      "expo-router",
      [
        "expo-splash-screen",
        {
          image: "./assets/images/splash-icon.png",
          imageWidth: 200,
          resizeMode: "contain",
          backgroundColor: "#ffffff",
        },
      ],
    ],
    experiments: {
      typedRoutes: true,
    },
    extra: {
      ANON_PUBLIC_KEY: process.env.ANON_PUBLIC_KEY,
      PUBLIC_SUPABASE_URL: process.env.PUBLIC_SUPABASE_URL,
      SERVICE_ROLE_KEY: process.env.SERVICE_ROLE_KEY,
      eas: {
        projectId: "b435d21c-5ce2-4d2a-911a-5104207a163a", // ✅ REQUIRED for push notifications
      },
    },
  },
};

```

### Proof/hooks/useColorScheme.ts

```typescript
export { useColorScheme } from 'react-native';

```

[29 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]