FitTrack now uses a lightweight Express backend that handles data persistence via JSON files. This design allows you to:
- Run the application outside the GitHub Spark environment
- Migrate to cloud backends (Firebase, Supabase, etc.) without changing component code
- Scale incrementally from JSON files to databases as needed
┌─────────────────────────────────────────┐
│ React Components │
│ (PlansView, WorkoutView, etc.) │
└────────────────┬────────────────────────┘
│
│ usePersistentState hook
▼
┌─────────────────────────────────────────┐
│ API Service Layer (src/lib/api.ts) │
│ - apiGet(), apiSet(), apiDelete() │
│ - fetch-based HTTP client │
└────────────────┬────────────────────────┘
│
│ fetch() via HTTP
▼
┌─────────────────────────────────────────┐
│ Express Backend (server.ts) │
│ - REST endpoints for CRUD │
│ - JSON file persistence (.data/) │
│ - CORS & error handling │
└─────────────────────────────────────────┘
Replaces GitHub Spark's useKV with a custom hook that communicates with the backend.
Interface remains identical for drop-in replacement:
const [data, setData] = usePersistentState<T>(key, defaultValue);Features:
- Automatic persistence on state change
- Fallback to default value on API errors
- Optional debouncing for frequent updates
- LocalStorage fallback for offline resilience
Key implementation details:
- Uses
apiSet()to persist changes to backend - Loads initial data with
apiGet()on mount - Handles updater functions:
setData((current) => ...)
Generic fetch-based client for backend communication.
Available functions:
// Retrieve data for a key
const data = await apiGet<WorkoutPlan[]>('workout-plans');
// Store/update data
const result = await apiSet('workout-plans', newPlans);
// Delete a key
await apiDelete('active-session');
// List all keys
const keys = await apiListKeys();Environment configuration:
VITE_API_URLenvironment variable controls backend URL- Development:
http://localhost:3000/api(.env.development) - Production:
/api(.env.production) - assumes backend is same domain
Error handling:
- 404 responses return
nullinstead of throwing - Other errors log to console but don't break the UI
- Components continue with default values on API failures
Lightweight HTTP server handling data persistence.
Key features:
- CORS enabled for requests from Vite dev server (localhost:5173)
- Atomic writes using temp files + rename to prevent corruption
- Key sanitization to prevent directory traversal attacks
- JSON formatting for human-readable data files
Storage model:
- Each key is stored as a separate JSON file in
.data/directory - Filenames are sanitized versions of keys (special chars → underscores)
- Directory is created automatically on first run
- Added to
.gitignoreby default (local development only)
Retrieve data for a specific key.
curl http://localhost:3000/api/data/workout-plansResponses:
200 OK:{ "success": true, "data": [...] }404 Not Found:{ "success": false, "error": "Key not found" }500 Server Error:{ "success": false, "error": "Internal server error" }
Store or update data for a key.
curl -X PUT http://localhost:3000/api/data/workout-plans \
-H "Content-Type: application/json" \
-d '{"data": [...]}'Request body:
{
"data": <any JSON-serializable value>
}Responses:
200 OK:{ "success": true, "data": [...] }400 Bad Request: Missing or invalid data field500 Server Error: File write failed
Delete data for a key.
curl -X DELETE http://localhost:3000/api/data/active-sessionResponses:
200 OK:{ "success": true }500 Server Error:{ "success": false, "error": "Internal server error" }
List all stored keys.
curl http://localhost:3000/api/keysResponse:
{
"success": true,
"keys": ["workout-plans", "workout-sessions", "active-session"]
}Health check endpoint.
curl http://localhost:3000/api/healthResponse:
{
"success": true,
"message": "Backend is running"
}Export all data as JSON (useful for backups).
curl -X POST http://localhost:3000/api/exportResponse:
{
"success": true,
"data": {
"workout-plans": [...],
"workout-sessions": [...],
...
}
}Import data from JSON (useful for restores).
curl -X POST http://localhost:3000/api/import \
-H "Content-Type: application/json" \
-d '{"data": {"workout-plans": [...], ...}}'Response:
{
"success": true,
"imported": 3
}Run both backend and frontend with one command:
npm run devThis starts:
- Backend: Express server on http://localhost:3000
- Frontend: Vite dev server on http://localhost:5173
npm run dev:backend # Backend only
npm run dev:frontend # Frontend only.env.development (local development):
VITE_API_URL=http://localhost:3000/api
.env.production (production deployment):
VITE_API_URL=/api
For custom deployment, set VITE_API_URL to your backend URL.
npm run buildThis creates a dist/ folder with optimized frontend code. Backend must be deployed separately.
The architecture is designed for easy backend swapping. To migrate to Firebase, Supabase, or other services:
-
Replace API functions in
src/lib/api.ts:export async function apiGet<T>(key: string): Promise<T | null> { // Call your cloud backend instead of local Express const response = await firebase.collection('data').doc(key).get(); return response.data(); }
-
Components remain unchanged - they still use
usePersistentState()the same way -
Type definitions stay identical - no need to refactor components
This makes it safe to start with JSON files and scale to a robust cloud backend as your application grows.
FitTrack stores three main data types:
{
id: string;
name: string;
description: string;
exercises: WorkoutExercise[];
createdAt: number;
}{
id: string;
planId: string;
planName: string;
startedAt: number;
completedAt?: number;
exercises: {
exerciseId: string;
exerciseName: string;
completedSets: CompletedSet[];
}[];
status: 'in-progress' | 'completed' | 'abandoned';
}The current workout in progress, if any.
- Suitable for personal use or small deployments
- ~100ms latency per request (local filesystem)
- No scalability concerns for single-user scenarios
- Easy debugging (inspect
.data/directory)
- Consider indexing on
planId,status,startedAt - Implement pagination for
workout-sessionshistory - Add database transactions if needed (rarely needed for this schema)
- Monitor cold-start latencies for cloud functions
# Check if port 3000 is in use
lsof -i :3000
# Kill any process on port 3000
kill -9 <PID>
# Or change port
PORT=4000 npm run backend- Check
.data/directory exists and is writable - Verify API responses return
success: true - Check browser console for API errors
- Ensure
VITE_API_URLis set correctly
- Backend should automatically allow localhost:5173
- For other origins, update CORS config in
server.ts:app.use(cors({ origin: ['http://localhost:5173', 'https://yourdomain.com'] }));
For production, add:
- Authentication - Verify users can only access their data
- Input validation - Validate all user inputs
- Rate limiting - Prevent abuse
- HTTPS - Encrypt data in transit
- Database - Replace JSON files with a real database
- Audit logging - Track data changes
The current implementation is optimized for rapid development and educational purposes.