# Project export: Trackstar

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: CruzHacks 2026
- Tagline: A discrete bike theft detection device that sends real-time alerts to your phone with GPS tracking and theft deterring capabilities.
- Devpost: https://devpost.com/software/trackstar-muzsf1
- GitHub: https://github.com/mjao1/trackstar
- Video: https://www.youtube.com/embed/KYpSYDuxKoU?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([Sponsor - Startup Club] Most Start-up Potential)
- Team: 2 GitHub contributor(s) — Michael Jao (7 commits), Emma Nguyen (1 commits)

## Devpost submission (written by the team)

### Inspiration

More than 50% of property crimes reported to the University of California involve bicycle theft. This project aims to address this critical issue affecting UC students by providing a discrete theft detection, prevention, and tracking system. By helping students protect their bikes, we're contributing to campus safety and justice, ensuring students can focus on their education without the financial and emotional burden of bike theft.

### What it does

Trackstar consists of a hardware device and mobile app: Hardware Device: A small, discrete device that attaches to your bike and monitors for unauthorized movement Mobile App: Receive instant alerts when unexpected movement is detected and remotely trigger an alarm to deter thieves Real-time Tracking: When theft is detected, the device automatically sends GPS coordinates so you can track your bike's location How It Works Pair Your Device: Scan a QR code to pair the hardware device with your phone Activate Watch Mode: After parking your bike, activate "Watch" mode through the app Get Alerts: If your bike detects unexpected movement, you'll receive a push notification from the app Track & Deter: View your bike's real-time location on an integrated map and trigger an alarm to deter thieves

### How we built it

We built Trackstar as a fullstack IoT system with three main components working together: Hardware Development: We started with an ESP32 microcontroller and integrated sensors for motion detection (MPU-6050 accelerometer) and GPS tracking (GT U7 module). The firmware uses Arduino and implements a state machine to manage Idle, Watch, and Theft Detected modes. We optimized power consumption by keeping GPS inactive until theft is detected, and implemented fast polling intervals for responsive command handling. Backend Architecture: We built a Node.js/Express REST API with PostgreSQL to serve as the central communication hub. The backend authenticates both users (via JWT tokens) and devices (via device ID/secret pairs), stores device state and GPS coordinates, and manages the command queue that devices poll. We integrated Expo's push notification service to send real-time alerts to users when motion is detected. Mobile App: We developed a React Native app using Expo for cross-platform compatibility. The app features a clean, animated UI with dark/light mode support, real-time status updates, and integrated Google Maps for GPS tracking. We implemented custom pulsing animations for status indicators and created a native map modal with a custom bike marker that pulses to match the theft detected state. The entire system communicates via HTTP over WiFi, with the device polling for commands and the app using REST APIs, creating a responsive real-time monitoring experience.

### Challenges we ran into

One of the biggest challenges we faced was integrating the hardware and software components by using HTTP requests to communicate between the device and our app. Some of the hardware we used had limited documentation, which required us to do lots of board level debugging. Building a cohesive testing structure on both hardware and software ends was also a necessary challenge we faced to verify the system's behavior.

## README (from the GitHub repository)

# Trackstar

A discrete bike theft detection and prevention device that sends real-time alerts to your phone with GPS tracking capabilities.

<p align = "center">
<img width="45%" alt="IMG_0324" src="https://github.com/user-attachments/assets/287eb2f3-24eb-4d36-9ae6-3f37b7b9dfb2" /> <img width="45%" alt="IMG_0331" src="https://github.com/user-attachments/assets/d0dd9d77-b892-4cee-ac7a-4a079829dd20" />
<img width="60%" alt="IMG_4145" src="https://github.com/user-attachments/assets/d4bae123-b9a7-4d2e-84b9-8bb223c9c9fa" />


## Problem Statement

[More than 50%](https://www.macalester.edu/public-safety/crime-prevention/bike_theft_prevention/) of property crimes reported to the University of California involve bicycle theft. This project aims to address this critical issue affecting UC students by providing a discrete theft detection, prevention, and tracking system. By helping students protect their bikes, we're contributing to campus safety and justice, ensuring students can focus on their education without the financial and emotional burden of bike theft.

## Overview

Trackstar consists of a hardware device and mobile app:

- **Hardware Device**: A small, discrete device that attaches to your bike and monitors for unauthorized movement
- **Mobile App**: Receive instant alerts when motion is detected, view your bike's location on a map, and remotely trigger an alarm
- **Real-time Tracking**: When theft is detected, the device automatically sends GPS coordinates so you can track your bike's location

## How It Works

1. **Pair Your Device**: Scan a QR code to pair the hardware device with your phone
2. **Activate Watch Mode**: After parking your bike, activate "Watch" mode through the app
3. **Get Alerts**: If your bike detects unexpected movement, you'll receive a push notification from the app
4. **Track & Deter**: View your bike's location on an integrated map and trigger an alarm to deter thieves

## Extra Features

- **Motion History**: View a log of all motion detection events
- **Dark/Light Mode**: Choose your preferred app theme
- **Low Power**: Efficient embedded design for extended battery life

## App States

### Idle
- Device is in low power mode
- Tap the button to activate "Watch" mode when you park your bike

### Watch
- Device actively monitors for movement
- If motion is detected, you'll be notified immediately
- View motion detection history anytime

### Theft Detected
- You receive an alert when motion is detected
- Confirm if the movement was you or not
- If not you: View GPS location on map and trigger alarm
- Device automatically returns to Watch mode after 10 seconds of no movement

## Components

### Hardware
- ESP32 microcontroller with WiFi connectivity
- Accelerometer for motion sensing
- GPS module for location tracking
- Buzzer for alarm functionality
- LiPo Battery for portability

### Software
- **Mobile App**: React Native app using Expo for cross-platform compatibility. Handles user authentication, device pairing via QR code scanning, and real-time status monitoring. Features a clean UI with dark/light mode support, push notifications for motion alerts, and integrated Google Maps for GPS tracking with a native map modal that matches the main app UI design.
- **Backend**: Node.js/Express REST API with PostgreSQL serving as the central communication hub. Authenticates both users and devices (via device ID/secret pairs), stores device state and GPS coordinates, and manages the command queue that devices poll. Handles motion events and GPS coordinates from devices, and integrates Expo's push notification service to send real-time alerts to users when motion is detected.
- **Device Firmware**: Embedded logic running on the hardware device. Implements motion detection using accelerometer data, manages WiFi connectivity for HTTP communication with the backend, and handles GPS coordinate reading and transmission. Controls device states (Idle/Watch/Theft Detected) and manages alarm activation.



## Detected evidence (automated analysis)

Indexed codebase: 25 recognized source files, 105 KB.
- Express (technology) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- C (language) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (37 of 37)

```
.gitignore
backend/.env
backend/package.json
backend/prisma/schema.prisma
backend/Procfile
backend/railway.json
backend/scripts/seed-device.ts
backend/scripts/simulate-motion.ts
backend/src/index.ts
backend/src/lib/prisma.ts
backend/src/middleware/auth.ts
backend/src/middleware/deviceAuth.ts
backend/src/routes/auth.ts
backend/src/routes/device.ts
backend/src/routes/esp32.ts
backend/src/services/push.ts
backend/tsconfig.json
device/trackstar/trackstar.ino
mobile/.expo/devices.json
mobile/.expo/README.md
mobile/app.json
mobile/app/_layout.tsx
mobile/app/(auth)/_layout.tsx
mobile/app/(auth)/login.tsx
mobile/app/(auth)/signup.tsx
mobile/app/(main)/_layout.tsx
mobile/app/(main)/profile.tsx
mobile/app/(main)/watch.tsx
mobile/app/index.tsx
mobile/app/scan.tsx
mobile/lib/api.ts
mobile/lib/auth.tsx
mobile/lib/notifications.ts
mobile/lib/theme.tsx
mobile/package.json
mobile/tsconfig.json
README.md
```

### Dependencies

- backend/package.json: @prisma/client@^5.22.0, @types/bcryptjs@^2.4.6, @types/cors@^2.8.17, @types/express@^5.0.0, @types/jsonwebtoken@^9.0.7, @types/node@^22.9.0, bcryptjs@^2.4.3, cors@^2.8.5, dotenv@^16.4.5, expo-server-sdk@^3.10.0, express@^4.21.1, jsonwebtoken@^9.0.2, prisma@^5.22.0, tsx@^4.19.2, typescript@^5.6.3, zod@^3.23.8
- mobile/package.json: @babel/core@^7.25.2, @expo-google-fonts/oxygen@^0.4.1, @expo/vector-icons@^15.0.3, @types/react@~19.1.10, expo@~54.0.0, expo-asset@~12.0.12, expo-auth-session@~7.0.0, expo-camera@~17.0.10, expo-constants@~18.0.13, expo-crypto@~15.0.8, expo-device@~8.0.10, expo-font@~14.0.10, expo-linking@~8.0.11, expo-notifications@~0.32.16, expo-router@~6.0.0, expo-secure-store@~15.0.8, expo-splash-screen@~31.0.13, expo-status-bar@~3.0.9, expo-web-browser@~15.0.10, react@19.1.0, react-native@0.81.5, react-native-maps@^1.20.1, react-native-safe-area-context@~5.6.0, react-native-screens@~4.16.0, typescript@~5.9.2

### Recent commits (newest first)

- add images to README
- update README
- update README
- update README
- update README
- update README
- add README
- add gps tracking
- changed icon
- add more local networks for testing
- first commit

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

### backend/package.json

```
{
  "name": "trackstar-backend",
  "version": "1.0.0",
  "description": "Backend API for Trackstar bike theft detection",
  "main": "dist/index.js",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "db:generate": "prisma generate",
    "db:push": "prisma db push",
    "db:migrate": "prisma migrate dev",
    "db:studio": "prisma studio"
  },
  "dependencies": {
    "@prisma/client": "^5.22.0",
    "bcryptjs": "^2.4.3",
    "cors": "^2.8.5",
    "dotenv": "^16.4.5",
    "expo-server-sdk": "^3.10.0",
    "express": "^4.21.1",
    "jsonwebtoken": "^9.0.2",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/bcryptjs": "^2.4.6",
    "@types/cors": "^2.8.17",
    "@types/express": "^5.0.0",
    "@types/jsonwebtoken": "^9.0.7",
    "@types/node": "^22.9.0",
    "prisma": "^5.22.0",
    "tsx": "^4.19.2",
    "typescript": "^5.6.3"
  }
}


```

### mobile/package.json

```
{
  "name": "trackstar-mobile",
  "version": "1.0.0",
  "main": "expo-router/entry",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web"
  },
  "dependencies": {
    "@expo-google-fonts/oxygen": "^0.4.1",
    "@expo/vector-icons": "^15.0.3",
    "expo": "~54.0.0",
    "expo-asset": "~12.0.12",
    "expo-auth-session": "~7.0.0",
    "expo-camera": "~17.0.10",
    "expo-constants": "~18.0.13",
    "expo-crypto": "~15.0.8",
    "expo-device": "~8.0.10",
    "expo-font": "~14.0.10",
    "expo-linking": "~8.0.11",
    "expo-notifications": "~0.32.16",
    "expo-router": "~6.0.0",
    "expo-secure-store": "~15.0.8",
    "expo-splash-screen": "~31.0.13",
    "expo-status-bar": "~3.0.9",
    "expo-web-browser": "~15.0.10",
    "react": "19.1.0",
    "react-native": "0.81.5",
    "react-native-maps": "^1.20.1",
    "react-native-safe-area-context": "~5.6.0",
    "react-native-screens": "~4.16.0"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@types/react": "~19.1.10",
    "typescript": "~5.9.2"
  },
  "private": true
}

```

### mobile/app/index.tsx

```typescript
import { Redirect } from 'expo-router';

export default function Index() {
  return <Redirect href="/(auth)/login" />;
}


```

### backend/src/index.ts

```typescript
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import { authRouter } from './routes/auth.js';
import { deviceRouter } from './routes/device.js';
import { esp32Router } from './routes/esp32.js';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(cors());
app.use(express.json());

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// Routes
app.use('/api/auth', authRouter);
app.use('/api/device', deviceRouter);
app.use('/api/esp32', esp32Router);

// Error handler
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal server error' });
});

app.listen(PORT, () => {
  console.log(`🚀 Trackstar API running on port ${PORT}`);
});


```

### mobile/app/_layout.tsx

```typescript
import { useEffect } from 'react';
import { Stack, useRouter, useSegments } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { View, ActivityIndicator, Text, TextInput, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { AuthProvider, useAuth } from '@/lib/auth';
import { ThemeProvider, useTheme } from '@/lib/theme';
import { 
  registerForPushNotifications, 
  addNotificationResponseReceivedListener 
} from '@/lib/notifications';
import {
  useFonts,
  Oxygen_300Light,
  Oxygen_400Regular,
  Oxygen_700Bold,
} from '@expo-google-fonts/oxygen';

// Set default font for all Text and TextInput components
const setDefaultFont = () => {
  const oldTextRender = (Text as any).render;
  (Text as any).render = function (...args: any[]) {
    const origin = oldTextRender.call(this, ...args);
    return {
      ...origin,
      props: {
        ...origin.props,
        style: [{ fontFamily: 'Oxygen_400Regular' }, origin.props.style],
      },
    };
  };

  const oldTextInputRender = (TextInput as any).render;
  (TextInput as any).render = function (...args: any[]) {
    const origin = oldTextInputRender.call(this, ...args);
    return {
      ...origin,
      props: {
        ...origin.props,
        style: [{ fontFamily: 'Oxygen_400Regular' }, origin.props.style],
      },
    };
  };
};

setDefaultFont();

function RootLayoutNav() {
  const { isAuthenticated, isLoading } = useAuth();
  const { isDark, colors } = useTheme();
  const segments = useSegments();
  const router = useRouter();

  useEffect(() => {
    if (isLoading) return;

    const inAuthGroup = segments[0] === '(auth)';

    if (!isAuthenticated && !inAuthGroup) {
      // Redirect to login if not authenticated
      router.replace('/(auth)/login');
    } else if (isAuthenticated && inAuthGroup) {
      // Redirect to main app if authenticated
      router.replace('/(main)/watch');
    }
  }, [isAuthenticated, isLoading, segments]);

  useEffect(() => {
    if (isAuthenticated) {
      // Register for push notifications when authenticated
      registerForPushNotifications();

      // Handle notification taps
      const subscription = addNotificationResponseReceivedListener((response) => {
        const data = response.notification.request.content.data;
        if (data?.type === 'motion') {
          router.push('/(main)/watch');
        }
      });

      return () => subscription.remove();
    }
  }, [isAuthenticated]);

  return (
    <>
      <StatusBar style={isDark ? 'light' : 'dark'} />
      <Stack screenOptions={{ headerShown: false }}>
        <Stack.Screen name="(auth)" />
        <Stack.Screen name="(main)" />
        <Stack.Screen 
          name="scan" 
          options={{ 
            presentation: 'modal',
            headerShown: true,
            headerTitle: 'Scan QR Code',
            headerStyle: {
              backgroundColor: colors.card,
            },
            headerTitleStyle: {
              color: colors.text,
              fontFamily: 'Oxygen_700Bold',
            },
            headerRight: () => (
              <TouchableOpacity 
                onPress={() => router.back()}
                style={{ padding: 8 }}
              >
                <Ionicons name="close" size={24} color={colors.text} />
              </TouchableOpacity>
            ),
          }} 
        />
      </Stack>
    </>
  );
}

export default function RootLayout() {
  const [fontsLoaded] = useFonts({
    Oxygen_300Light,
    Oxygen_400Regular,
    Oxygen_700Bold,
  });

  if (!fontsLoaded) {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#FAFAFA' }}>
        <ActivityIndicator size="large" color="#10B981" />
      </View>
    );
  }

  return (
    <ThemeProvider>
    <AuthProvider>
      <RootLayoutNav />
    </AuthProvider>
    </ThemeProvider>
  );
}


```

### mobile/app/(auth)/_layout.tsx

```typescript
import { Stack } from 'expo-router';

export default function AuthLayout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="login" />
      <Stack.Screen name="signup" />
    </Stack>
  );
}


```

### mobile/app/(main)/_layout.tsx

```typescript
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useTheme } from '@/lib/theme';

export default function MainLayout() {
  const { colors } = useTheme();

  return (
    <Tabs
      screenOptions={{
        // Tab bar styling
        tabBarActiveTintColor: colors.primary,
        tabBarInactiveTintColor: colors.iconMuted,
        tabBarStyle: {
          backgroundColor: colors.card,
          borderTopColor: colors.borderLight,
          borderTopWidth: 0.5,
          paddingTop: 8,
          height: 88,
        },
        tabBarLabelStyle: {
          fontSize: 12,
          fontWeight: '500',
        },
        // Header styling
        headerStyle: {
          backgroundColor: colors.card,
          borderBottomColor: colors.borderLight,
          borderBottomWidth: 0.5,
        },
        headerTitleStyle: {
          fontWeight: '600',
          color: colors.text,
        },
        headerShadowVisible: false,
        // Screen background
        sceneStyle: {
          backgroundColor: colors.background,
        },
      }}
    >
      <Tabs.Screen
        name="watch"
        options={{
          title: 'Watch',
          headerTitle: 'Trackstar',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="shield-checkmark" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}


```

### backend/scripts/seed-device.ts

```typescript
/**
 * Seed script to create a test device for development
 * Run with: npx tsx scripts/seed-device.ts
 */

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
  // Create a test device
  const device = await prisma.device.upsert({
    where: { id: 'TEST-DEVICE-001' },
    update: {},
    create: {
      id: 'TEST-DEVICE-001',
      secret: 'test-secret-123',
    },
  });

  console.log('Created test device:', device);
  console.log('\nQR Code URL:');
  console.log(`https://pair.trackstar/dev?d=${device.id}&s=${device.secret}`);
  console.log('\nTest with curl:');
  console.log(`curl -H "x-device-id: ${device.id}" -H "x-device-secret: ${device.secret}" http://localhost:3000/api/esp32/poll`);
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect());


```

### mobile/lib/auth.tsx

```typescript
import React, { createContext, useContext, useEffect, useState } from 'react';
import { getToken, removeToken, getMe } from './api';

interface User {
  id: string;
  email: string;
}

interface AuthContextType {
  user: User | null;
  isLoading: boolean;
  isAuthenticated: boolean;
  checkAuth: () => Promise<void>;
  logout: () => Promise<void>;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  const checkAuth = async () => {
    try {
      const token = await getToken();
      if (!token) {
        setUser(null);
        return;
      }

      const result = await getMe();
      if (result.data?.user) {
        setUser(result.data.user);
      } else {
        await removeToken();
        setUser(null);
      }
    } catch (error) {
      console.error('Auth check error:', error);
      setUser(null);
    } finally {
      setIsLoading(false);
    }
  };

  const logout = async () => {
    await removeToken();
    setUser(null);
  };

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

  return (
    <AuthContext.Provider
      value={{
        user,
        isLoading,
        isAuthenticated: !!user,
        checkAuth,
        logout,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}


```

### backend/scripts/simulate-motion.ts

```typescript
/**
 * Simulate ESP32 motion events for testing
 * Run with: npx tsx scripts/simulate-motion.ts
 */

const API_URL = process.env.API_URL || 'http://localhost:3000';
const DEVICE_ID = process.env.DEVICE_ID || 'TEST-DEVICE-001';
const DEVICE_SECRET = process.env.DEVICE_SECRET || 'test-secret-123';

async function poll() {
  const response = await fetch(`${API_URL}/api/esp32/poll`, {
    headers: {
      'x-device-id': DEVICE_ID,
      'x-device-secret': DEVICE_SECRET,
    },
  });
  const data = await response.json();
  console.log('[POLL]', data);
  return data;
}

async function reportMotion() {
  const response = await fetch(`${API_URL}/api/esp32/motion`, {
    method: 'POST',
    headers: {
      'x-device-id': DEVICE_ID,
      'x-device-secret': DEVICE_SECRET,
      'Content-Type': 'application/json',
    },
  });
  const data = await response.json();
  console.log('[MOTION]', data);
  return data;
}

async function main() {
  console.log('=== Trackstar Device Simulator ===');
  console.log(`Device: ${DEVICE_ID}`);
  console.log(`API: ${API_URL}`);
  console.log('');

  // Initial poll
  console.log('1. Initial poll...');
  let state = await poll();

  if (state.state === 'IDLE') {
    console.log('\nDevice is IDLE. Set it to WATCH mode in the app first!');
    console.log('Then run this script again.\n');
    return;
  }

  console.log('\n2. Device is in WATCH mode. Simulating motion...');
  await reportMotion();

  console.log('\n3. Polling for updated state...');
  state = await poll();

  console.log('\n4. Check your phone for a push notification!');
  console.log('   You should see "Motion Detected!" alert.\n');

  // Keep polling to show state changes
  console.log('5. Polling every 2 seconds (Ctrl+C to stop)...\n');
  setInterval(async () => {
    await poll();
  }, 2000);
}

main().catch(console.error);


```

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