Skip to content

Latest commit

 

History

History
1258 lines (989 loc) · 41.2 KB

File metadata and controls

1258 lines (989 loc) · 41.2 KB

FutureTracker - Comprehensive Project Documentation

One-liner pitch: FutureTracker is a premium SaaS application that helps students and professionals track their internship and hackathon applications with real-time updates, visual analytics, and a Kanban-style workflow.


Table of Contents

  1. Project Overview
  2. Recent Updates
  3. Tech Stack
  4. System Architecture
  5. Authentication Flow
  6. Backend Implementation
  7. Frontend Implementation
  8. Database Design
  9. Key Features
  10. Unique Technical Challenges
  11. Performance & SEO Optimizations
  12. Future Roadmap

Recent Updates

Merged to main (2026):

PR / change What shipped
#60 ATS scorer Client-side PDF/DOCX analysis; scores stored on documents via API
#58 Interview prep Per-internship workspace: research, Q&A, topics, STAR, reflection
#56 Interview rounds UI Timeline, modal, Kanban sync (builds on rounds API)
#29 CI & guardrails test:ci, backend tests, check:architecture
Status indicator UptimeRobot link in navbar, footer, README

New docs: CODEBASE_GUIDE.md · interview-prep.md · documents-and-ats.md


Project Overview

Problem Statement

Students applying to multiple internships and hackathons struggle to:

  • Track application statuses across different platforms
  • Remember deadlines and follow-up dates
  • Visualize their progress and success rates
  • Access their data across devices in real-time

Solution

FutureTracker provides a centralized, real-time dashboard that:

  • Tracks all opportunities in one place
  • Visualizes progress with charts and analytics
  • Syncs instantly across devices using WebSocket technology
  • Exports professional PDF reports

Tech Stack

graph TB
    subgraph Frontend
        A[React 18] --> B[React Router v6]
        A --> C[TailwindCSS]
        A --> D[Recharts]
        A --> E[React Toastify]
    end
    
    subgraph Backend
        F[Node.js + Express] --> G[Clerk SDK]
        F --> H[Supabase Client]
    end
    
    subgraph External Services
        I[Clerk] --> J[Authentication]
        K[Supabase] --> L[PostgreSQL Database]
        K --> M[Realtime Subscriptions]
    end
    
    Frontend --> Backend
    Backend --> External Services
Loading
Layer Technology Purpose
Frontend React 19 + CRA Modern UI with hooks
Styling TailwindCSS Utility-first responsive design
Charts Recharts Premium data visualizations
Animations Framer Motion Smooth transitions
Auth Clerk OAuth + passwordless login
Analytics PostHog Product analytics & user tracking
Backend Express.js RESTful API server
Database Supabase (PostgreSQL) Managed database with RLS
Realtime Supabase Realtime WebSocket subscriptions
Frontend Hosting Vercel CDN + serverless
Backend Hosting Render Node.js server

System Architecture

High-Level Architecture

flowchart LR
    subgraph Client [Browser]
        UI[React App]
        SC[Supabase Client]
    end
    
    subgraph Server [Express Backend]
        API[REST API]
        MW[Auth Middleware]
        DB_C[Supabase Admin]
    end
    
    subgraph Services [External]
        CL[Clerk Auth]
        SB[(Supabase PostgreSQL)]
        RT[Realtime Engine]
    end
    
    UI -->|JWT Token| API
    API --> MW
    MW -->|Verify| CL
    MW --> DB_C
    DB_C --> SB
    
    SC -.->|WebSocket| RT
    RT -.->|postgres_changes| SB
Loading

Request Flow

sequenceDiagram
    participant U as User
    participant R as React App
    participant A as API (Express)
    participant C as Clerk
    participant S as Supabase
    
    U->>R: Click "Add Opportunity"
    R->>C: getToken()
    C-->>R: JWT Token
    R->>A: POST /api/opportunities (+ Bearer Token)
    A->>C: Verify JWT
    C-->>A: User Claims
    A->>S: INSERT opportunity
    S-->>A: Created Record
    A-->>R: 201 + opportunity data
    S->>R: Realtime: INSERT event
    R->>U: UI Updated (both tabs!)
Loading

Authentication Flow

Clerk + Supabase Integration

flowchart TD
    A[User Opens App] --> B{Signed In?}
    B -->|No| C[Clerk SignIn Modal]
    B -->|Yes| D[React App Loads]
    
    C --> E[OAuth/Email Login]
    E --> F[Clerk Issues JWT]
    F --> D
    
    D --> G[useAuthToken Hook]
    G --> H[Attach JWT to API Calls]
    H --> I[Express Middleware]
    I --> J[Verify with Clerk SDK]
    J --> K[Extract user_id]
    K --> L[Query Supabase with user_id filter]
Loading

Key Implementation Details:

  • useAuthToken hook sets the token getter for API interceptors
  • Every API request includes Authorization: Bearer <JWT>
  • Backend middleware extracts userId from Clerk claims
  • All database queries are filtered by user_id for data isolation

Backend Implementation

API Structure

backend/
├── src/
│   ├── server.js              # HTTP server entry (imports app.js)
│   ├── app.js                 # Express app, middleware, route mounts
│   ├── middleware/
│   │   ├── auth.js            # Clerk JWT verification
│   │   └── validate.js        # Request body validation
│   ├── routes/
│   │   ├── opportunities.js   # Opportunities CRUD
│   │   ├── opportunity-rounds.js  # Nested /rounds routes
│   │   ├── interview-prep.js  # Prep workspace
│   │   ├── documents.js       # Document vault + upload
│   │   ├── hackathons.js      # Team collaboration
│   │   └── analytics.js       # Dashboard stats
│   ├── validation/            # Zod schemas per domain
│   └── lib/
│       ├── supabase.js        # Admin client
│       └── syncOpportunityFromRounds.js

See backend/README.md for the full endpoint list.

API Endpoints (summary)

Area Mount Details
Health GET /api/health, /api/health/deps Public
Opportunities /api/opportunities CRUD
Interview rounds /api/opportunities/:id/rounds interview-rounds.md
Interview prep /api/interview-prep/:opportunityId interview-prep.md
Documents /api/documents documents-and-ats.md
Hackathons /api/hackathons/:id/* Team, ideas, tasks, checklist
Analytics /api/analytics Dashboard charts data
Me GET /api/me Current user

Auth Middleware Logic

// Simplified auth flow
const authMiddleware = async (req, res, next) => {
  const token = req.headers.authorization?.split('Bearer ')[1];
  
  // Verify JWT with Clerk
  const payload = await clerkClient.verifyToken(token);
  
  // Map Clerk userId to internal user
  const user = await supabase
    .from('users')
    .select('id')
    .eq('clerk_id', payload.sub)
    .single();
    
  req.auth = { internalUserId: user.id };
  next();
};

Frontend Implementation

Component Architecture

graph TD
    App --> ErrorBoundary
    ErrorBoundary --> Router
    Router --> Navbar
    Router --> Routes
    
    Routes --> Home
    Routes --> Dashboard
    Routes --> InternshipList
    Routes --> InterviewPrepDetail[Interview Prep]
    Routes --> HackathonDetail
    Routes --> Documents
    Routes --> StatusBoard[Status Board / Kanban]
    Routes --> Analytics
    Routes --> Calendar
    Routes --> Reports
    
    Dashboard --> StatsCard
    Dashboard --> Card
    
    StatusBoard --> StatusColumn
    StatusColumn --> OpportunityCard
    
    Analytics --> PieChart
    Analytics --> LineChart
    Analytics --> FunnelChart
    Analytics --> Heatmap
Loading

State Management

Feature State Management Why
Auth State Clerk Context Built-in auth state
Opportunities Local useState + API Simple CRUD, no complex state
Realtime Supabase Subscription WebSocket pushes updates
Error Handling Error Boundary + Toast Centralized error UX

Key Custom Hooks

  • useAuthToken: Sets up JWT token for API requests
  • useCallback (fetchOpportunities): Memoized for realtime subscription stability

Database Design

Entity Relationship Diagram

erDiagram
    USERS ||--o{ OPPORTUNITIES : has
    USERS ||--o{ OPPORTUNITY_ROUNDS : owns
    OPPORTUNITIES ||--o{ OPPORTUNITY_ROUNDS : has
    
    USERS {
        uuid id PK
        text clerk_id UK
        text email
        text full_name
        text avatar_url
        timestamp created_at
    }
    
    OPPORTUNITIES {
        uuid id PK
        uuid user_id FK
        text title
        text description
        text link
        date deadline
        text category
        text status
        int current_round_number
        int rejected_round_number
        text notes
        timestamp created_at
        timestamp updated_at
    }

    OPPORTUNITY_ROUNDS {
        uuid id PK
        uuid opportunity_id FK
        uuid user_id FK
        int round_number
        text round_type
        date scheduled_date
        text result
        text notes
        timestamp created_at
        timestamp updated_at
    }
Loading

Interview rounds track multi-stage hiring pipelines (OA, technical, HR, etc.) per internship opportunity. See docs/opportunity-rounds-migration.sql and the full guide docs/interview-rounds.md.


Interview Round Tracking

Multi-round hiring pipeline for internships: timeline UI, per-round results, and automatic sync of Kanban status plus current_round_number / rejected_round_number.

Quick reference for interviews: docs/interview-rounds.md

API routes

Nested under opportunities (internships only):

Method Endpoint Notes
GET /api/opportunities/:opportunityId/rounds Ordered by round_number
POST /api/opportunities/:opportunityId/rounds Returns { round, opportunity, rounds }
PATCH /api/opportunities/:opportunityId/rounds/:roundId Same unified response
DELETE /api/opportunities/:opportunityId/rounds/:roundId Same + success flag

Frontend uses roundService in src/services/api.js only — no direct Supabase CRUD for rounds.

Performance: slow round save (fixed)

Problem: Creating a round felt sluggish — users waited several seconds before the success toast.

Root cause:

  • Backend ran 6 sequential Supabase queries on create (verify, next-number lookup, insert, then sync re-fetched opportunity + rounds before update).
  • Frontend then fired 2 more API calls (getById + list rounds) and kept the modal on “Saving…” until both finished.

Fix:

  1. Batch reads — list rounds once; reuse for next round_number and for deriveOpportunityFieldsFromRounds().
  2. Skip redundant sync SELECTssyncOpportunityFromRounds(supabase, id, userId, { existingStatus, rounds }).
  3. Unified mutation response — POST/PATCH/DELETE return { round, opportunity, rounds }.
  4. Frontend applies response in placeapplyRoundMutationResult() in OpportunityDetailModal; no blocking refetch.

Result: Create path 6 → 4 DB round-trips; UI updates from a single HTTP response.

See docs/interview-rounds.md for the interview one-liner and file map.

Row Level Security (RLS)

-- Users can only see their own opportunities
CREATE POLICY "Users can view own opportunities" 
ON opportunities FOR SELECT 
USING (
  user_id IN (
    SELECT id FROM users 
    WHERE clerk_id = current_setting('request.jwt.claims')::json->>'sub'
  )
);

Key Features

1. Real-Time Kanban Board

  • Drag-and-drop status changes
  • Instant sync across tabs/devices via Supabase Realtime
  • Visual status columns: Applied → Shortlisted → Interviewed → Selected/Rejected

2. Analytics Dashboard

  • Status Distribution Pie Chart: Visual breakdown of application statuses
  • Weekly Trend Line: Track application velocity over 8 weeks
  • Conversion Funnel: See drop-off at each stage
  • Deadline Heatmap: GitHub-style calendar for upcoming deadlines

3. Smart Dashboard Features

  • Overdue Alerts: Only shows active applications (not rejected/selected)
  • Key Metrics: Total applied, success rate, in-progress count
  • Quick Actions: One-click navigation to common tasks

4. Error Handling & UX

  • Global Error Boundary: Catches JS errors with friendly UI
  • Toast Notifications: Success/error feedback for all actions
  • Auto-logout on 401: Expired sessions handled gracefully
  • Skeleton Loading: Premium loading states for all data-heavy views

5. Interview Round Tracking (internships)

  • Multi-round pipeline: OA, technical, HR, final — per-round type, date, result, notes
  • Timeline UI in internship detail drawer; compact badge on cards
  • Auto status sync: Round results update Kanban status and current_round_number / rejected_round_number
  • Performance: Mutations return { round, opportunity, rounds }; UI applies in one shot (no blocking refetch)
  • Docs: interview-rounds.md

6. Interview Preparation (internships)

  • Per-company workspace: Research notes, question bank, technical topics, STAR behavioral, reflection
  • Progress bar: Aggregates prepared questions, reviewed topics, behavioral entries
  • Route: /internships/:id/prep from detail drawer
  • Docs: interview-prep.md

7. Documents & ATS hints

  • Vault: Upload resumes/cover letters; assign to opportunities
  • ATS analysis: Client-side PDF/DOCX scoring on upload (structure + content heuristics)
  • Docs: documents-and-ats.md

8. Product Analytics (PostHog)

  • User Behavior Tracking: Page views, feature usage, and user flows
  • Event Tracking: Custom events for opportunity creation, updates, deletions
  • Autocapture: Automatic click and form submission tracking
  • User Identification: Links analytics to authenticated users
  • Privacy-First: Configurable data collection with opt-out support

Key Events Tracked:

// Opportunity lifecycle
- opportunity_created (category)
- opportunity_updated (old_status  new_status)
- opportunity_deleted (category)
- status_board_drag (status changes)

// Feature usage
- report_exported (format, count)
- feature_used (feature_name)

Implementation Highlights:

  • Graceful degradation when PostHog key not configured
  • Automatic user identification on sign-in
  • Reset analytics on sign-out for privacy
  • Manual page view tracking for SPA routing

Unique Technical Challenges

Challenge 1: Supabase Realtime with Row Level Security

Problem: Supabase Realtime subscriptions were immediately closing with CHANNEL_ERROR due to RLS policies blocking the anonymous client.

Initial Approach (failed):

// Tried passing Clerk JWT to Supabase
const token = await getToken({ template: 'supabase' });
supabase.auth.setSession({ access_token: token });
// Result: Still CHANNEL_ERROR

Root Cause: The RLS policies required JWT claims that the Clerk token didn't provide in the expected format.

Solution: Added a permissive SELECT policy for realtime broadcasts while keeping data secure:

CREATE POLICY "Allow realtime subscriptions" 
ON opportunities FOR SELECT USING (true);

Why it's still secure:

  1. Backend API enforces user_id filtering for all data access
  2. Frontend only displays data fetched through authenticated API calls
  3. Realtime just triggers a refetch, doesn't expose data directly

Challenge 2: Overdue Logic for Final Statuses

Problem: Rejected and selected applications were showing as "overdue" which is illogical - if you've been rejected, the deadline no longer matters.

Solution: Filter out final statuses from overdue calculations:

const finalStatuses = ['rejected', 'selected'];

const overdueItems = opportunities.filter(
  opp => isOverdue(opp.deadline) && !finalStatuses.includes(opp.status)
);

Challenge 3: API Error Handling at Scale

Problem: Each component was handling errors differently, leading to inconsistent UX.

Solution: Centralized error handling in Axios interceptors:

api.interceptors.response.use(
  (response) => response,
  (error) => {
    switch (error.response?.status) {
      case 401:
        toast.error('Session expired');
        setTimeout(() => window.location.href = '/', 1500);
        break;
      case 500:
        toast.error('Server error. Please try again.');
        break;
      // ... other cases
    }
    return Promise.reject(error);
  }
);

Challenge 4: Calendar Grid Layout Alignment

Problem: The custom calendar view was showing misaligned days, with the Sunday column wrapping to the next line, causing significant confusion.

Solution: Removed the margin and relied on borders for visual separation, ensuring the 7-column grid fits perfectly within the container.


Challenge 5: Interview Round Save Latency

Problem: After adding interview round tracking, saving a round felt slow — users stared at “Saving…” for several seconds before seeing success feedback.

Root cause: A naive create flow chained 6 sequential Supabase calls (verify opportunity, query max round number, insert, then sync re-read opportunity + rounds, then update). The frontend then issued two more REST calls to refetch opportunity and rounds before closing the modal.

Solution:

  1. Fetch rounds once at the start of create; derive next round_number in memory.
  2. Pass existingStatus and rounds into syncOpportunityFromRounds() so sync only runs the final UPDATE.
  3. Return { round, opportunity, rounds } from POST/PATCH/DELETE.
  4. Apply that payload in OpportunityDetailModal via applyRoundMutationResult() — toast and modal close immediately.

Interview takeaway: Reduced DB round-trips and eliminated redundant client refetches by designing the API response for the UI’s exact needs.

Full write-up: docs/interview-rounds.md


Interview Preparation Module

Per-internship study workspace — separate from round tracking (pipeline status vs. preparation content).

Layer Location
Page src/pages/InterviewPrepDetail.jsx/internships/:id/prep
API backend/src/routes/interview-prep.js
Service interviewPrepService in src/services/api.js
Migration docs/interview-prep-migration.sql

Tabs: Overview, Company Research, Questions, Technical Topics, Behavioral (STAR), Reflection.

Entry: Internship detail drawer → Interview Prep button.

Interview takeaway: One GET returns { prep, questions, topics, behavioral }; internship-only guard on every route; same “API-only, no frontend Supabase CRUD” rule as rounds.

Full guide: interview-prep.md


Documents Vault & ATS Scorer

Layer Location
Page src/pages/Documents.jsx
ATS logic src/utils/atsScorer.js (client-side PDF/DOCX)
Upload UI src/components/documents/DocumentUpload.jsx
API backend/src/routes/documents.js

On upload, the browser extracts text, computes a 0–100 rule-based score, and saves ats_score + ats_analysis JSON on the document record. Not an external ATS API.

Full guide: documents-and-ats.md


Hackathon Team Collaboration (NEW)

A comprehensive collaboration workspace for hackathon participants, enabling team management, idea brainstorming, task assignment, and submission tracking.

Database Schema

5 new tables with Row Level Security:

Table Purpose
hackathon_teams One team per hackathon with name and description
team_members Name-based members with roles (no account linking)
brainstorm_ideas Ideas with voting, categories, and selection
hackathon_tasks Kanban-style tasks with priorities and assignments
submission_checklist Checklist items with completion tracking

API Endpoints

Method Endpoint Description
GET/POST/PUT /api/hackathons/:id/team Team CRUD
POST/PUT/DELETE /api/hackathons/:id/team/members Member management
GET/POST/PUT/DELETE /api/hackathons/:id/ideas Idea brainstorming
POST /api/hackathons/:id/ideas/:id/vote Upvote idea
GET/POST/PUT/DELETE /api/hackathons/:id/tasks Task management
GET/POST/PUT/DELETE /api/hackathons/:id/checklist Checklist management

Frontend Components

src/components/hackathons/
├── TeamManagementPanel.jsx    # Team creation, member management
├── IdeaBrainstormingBoard.jsx # Ideas grid with voting & categories
├── TaskBoard.jsx              # Kanban with 3 columns
└── SubmissionChecklist.jsx    # Progress bar & completion tracking

HackathonDetail Page

Tabbed interface with 5 sections:

  1. Overview - Hackathon description and notes
  2. Team - Create team, add/remove members, edit roles
  3. Ideas - Brainstorm and vote on project ideas
  4. Tasks - Kanban board with priorities and assignments
  5. Checklist - Track submission requirements

Setup

Run the migration SQL in Supabase:

# In Supabase SQL Editor, paste contents of:
docs/hackathon-collaboration-migration.sql

Future Roadmap

Short-Term (Next Sprint)

  • Email Reminders: Deadline notifications via SendGrid
  • Bulk Import: CSV upload for multiple opportunities
  • Tags/Labels: Custom categorization beyond Internship/Hackathon

Medium-Term

  • Mobile App: React Native version
  • Chrome Extension: Quick-add from job boards
  • AI Suggestions: Auto-fill company details, suggest similar opportunities

Long-Term Vision

  • Team Features: Share applications with mentors/career counselors → Implemented as Hackathon Team Collaboration
  • Job Board Integration: Pull listings from LinkedIn, Glassdoor, etc.
  • Analytics Insights: ML-powered predictions on success likelihood

Performance & SEO Optimizations

Overview

Implemented comprehensive performance and SEO improvements to enhance user experience, reduce load times, and improve search engine visibility.

Performance Improvements

1. Code Splitting & Lazy Loading

Implementation:

// App.js - Route-based code splitting
import { lazy, Suspense } from 'react';

// Home is NOT lazy loaded - it's the landing page and should load immediately
import Home from './pages/Home'; // Landing page - load immediately for best UX

// Lazy load authenticated pages for better performance
const Dashboard = lazy(() => import('./pages/Dashboard'));
const InternshipList = lazy(() => import('./pages/InternshipList'));
const HackathonList = lazy(() => import('./pages/HackathonList'));
const AddOpportunity = lazy(() => import('./pages/AddOpportunity'));
const EditOpportunity = lazy(() => import('./pages/EditOpportunity'));
const StatusBoard = lazy(() => import('./pages/StatusBoard'));
const Calendar = lazy(() => import('./pages/Calendar'));
const Reports = lazy(() => import('./pages/Reports'));
const Analytics = lazy(() => import('./pages/Analytics'));

// Suspense wrapper with loading fallback
<Suspense fallback={<PageLoader />}>
  <Routes>
    <Route path="/" element={<Home />} />
    {/* ... other routes */}
  </Routes>
</Suspense>

Why Home is NOT Lazy Loaded:

  • Home is the most common entry point
  • Lazy loading it would add extra network request delay
  • Better UX to render landing page immediately
  • Authenticated pages are still lazy loaded (users don't need them immediately)

Results:

  • Initial bundle size: 529 KB → 238 KB (gzipped) = 55% reduction
  • Landing page: Renders immediately with no loading spinner
  • Code chunks created: 20 separate chunks
  • Faster Time to Interactive (TTI): Users see content faster

Bundle Analysis:

Main bundle:        237.83 KB (gzipped, includes Home)
Vendor chunks:      126.66 KB + 117.36 KB
Page chunks:        45.93 KB, 43.67 KB, 43.21 KB (authenticated pages)
Smaller chunks:     12 additional chunks (1-10 KB each)
CSS bundle:         9.9 KB

2. Font Optimization

Implementation:

<!-- public/index.html -->
<!-- Preconnect to Google Fonts for faster DNS resolution -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- Async font loading using media="print" trick (works without JavaScript) -->
<link rel="stylesheet" 
      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" 
      media="print" 
      onload="this.media='all'">

<!-- Fallback for no-JS users -->
<noscript>
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" 
        rel="stylesheet">
</noscript>

Why media="print" Pattern:

  • Works even if JavaScript fails or is disabled
  • Browser loads with low priority (async behavior)
  • onload changes media to 'all' when loaded
  • Better accessibility than pure JavaScript approach

Benefits:

  • Non-blocking font loading (doesn't delay page render)
  • Faster DNS resolution with preconnect
  • Improved Core Web Vitals scores
  • Better perceived performance

SEO Enhancements

1. Dynamic Meta Tag Management

Implementation: Created reusable SEO component using react-helmet-async:

// src/components/seo/SEO.jsx
import { Helmet } from 'react-helmet-async';

const SEO = ({
  title,
  description,
  keywords,
  canonical,
  type = 'website',
  image = 'https://futuretracker.online/og-image.png',
  noindex = false,
}) => {
  const siteTitle = 'FutureTracker';
  const fullTitle = title ? `${title} | ${siteTitle}` : `${siteTitle} - Track Internships, Hackathons & Job Applications`;
  const baseUrl = 'https://futuretracker.online';
  const canonicalUrl = canonical ? `${baseUrl}${canonical}` : baseUrl;

  return (
    <Helmet>
      {/* Primary Meta Tags */}
      <title>{fullTitle}</title>
      <meta name="description" content={description} />
      <meta name="keywords" content={keywords} />
      {noindex ? (
        <meta name="robots" content="noindex, nofollow" />
      ) : (
        <meta name="robots" content="index, follow" />
      )}
      <link rel="canonical" href={canonicalUrl} />

      {/* Open Graph / Facebook */}
      <meta property="og:type" content={type} />
      <meta property="og:url" content={canonicalUrl} />
      <meta property="og:title" content={fullTitle} />
      <meta property="og:description" content={description} />
      <meta property="og:image" content={image} />

      {/* Twitter */}
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:title" content={fullTitle} />
      <meta name="twitter:description" content={description} />
      <meta name="twitter:image" content={image} />
    </Helmet>
  );
};

Usage in Pages:

// Public page (Home.jsx)
<SEO 
  title={null}
  description="Free opportunity tracker for students and developers..."
  keywords="job tracker, internship tracker, hackathon tracker..."
  canonical="/"
/>

// Protected page (Dashboard.jsx)
<SEO 
  title="Dashboard"
  description="View your opportunity tracking dashboard..."
  canonical="/dashboard"
  noindex={true}  // Prevent indexing of user-specific content
/>

Coverage:

  • ✅ All 10 pages have unique meta tags
  • ✅ Open Graph tags for social media sharing
  • ✅ Twitter Card metadata
  • ✅ Canonical URLs to prevent duplicate content
  • ✅ Protected routes marked with noindex

2. Structured Data (JSON-LD)

Implemented 4 Schema.org schemas in public/index.html:

a) WebApplication Schema

{
  "@context": "https://schema.org",
  "@type": "WebApplication",
  "name": "FutureTracker",
  "url": "https://futuretracker.online",
  "description": "Track internships, hackathons, and job applications...",
  "applicationCategory": "ProductivityApplication",
  "operatingSystem": "Web",
  "browserRequirements": "Requires JavaScript. Requires HTML5.",
  "offers": {
    "@type": "Offer",
    "price": "0",
    "priceCurrency": "USD"
  },
  "featureList": [
    "Internship Application Tracking",
    "Hackathon Deadline Management",
    "Kanban Status Board",
    "Calendar View",
    "PDF Report Export",
    "Analytics Dashboard"
  ]
}

b) Organization Schema

{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "FutureTracker",
  "url": "https://futuretracker.online",
  "logo": "https://futuretracker.online/logo512.png",
  "sameAs": [
    "https://github.com/Venkat-Kolasani/FutureStack"
  ],
  "contactPoint": {
    "@type": "ContactPoint",
    "contactType": "customer support",
    "url": "https://github.com/Venkat-Kolasani/FutureStack/issues"
  }
}

c) FAQ Schema

{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What is FutureTracker?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "FutureTracker is a free opportunity tracker..."
      }
    }
    // ... 2 more questions
  ]
}

Note: BreadcrumbList schema was considered but not implemented because static breadcrumbs don't reflect actual navigation in a single-page application (SPA). Each route would need dynamic breadcrumbs which is better handled by the SEO component if needed in the future.

Implemented Schemas (3 total):

  • WebApplication - App description and features
  • Organization - Company/brand information
  • FAQ - Common questions and answers

Benefits:

  • Rich snippets in Google search results
  • Better search engine understanding of content
  • Improved click-through rates (CTR)
  • Enhanced visibility in search results

3. XML Sitemap

File: public/sitemap.xml

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <!-- Homepage -->
  <url>
    <loc>https://futuretracker.online/</loc>
    <lastmod>2026-01-17</lastmod>
    <changefreq>weekly</changefreq>
    <priority>1.0</priority>
  </url>
  
  <!-- Dashboard -->
  <url>
    <loc>https://futuretracker.online/dashboard</loc>
    <lastmod>2026-01-17</lastmod>
    <changefreq>daily</changefreq>
    <priority>0.9</priority>
  </url>
  
  <!-- ... 6 more routes with appropriate priorities -->
</urlset>

Included Routes:

  • / (Priority: 1.0)
  • /dashboard (Priority: 0.9)
  • /internships (Priority: 0.8)
  • /hackathons (Priority: 0.8)
  • /status-board (Priority: 0.7)
  • /calendar (Priority: 0.7)
  • /analytics (Priority: 0.6)
  • /reports (Priority: 0.6)

4. Enhanced PWA Manifest

File: public/manifest.json

{
  "short_name": "FutureTracker",
  "name": "FutureTracker - Track Internships & Hackathons",
  "description": "Free opportunity tracker for students and developers...",
  "shortcuts": [
    {
      "name": "Dashboard",
      "short_name": "Dashboard",
      "description": "View your dashboard",
      "url": "/dashboard",
      "icons": [{ "src": "logo192.png", "sizes": "192x192" }]
    },
    {
      "name": "Add Opportunity",
      "short_name": "Add",
      "description": "Add a new opportunity",
      "url": "/add",
      "icons": [{ "src": "logo192.png", "sizes": "192x192" }]
    }
  ],
  "categories": ["productivity", "education", "utilities"],
  "orientation": "portrait-primary",
  "scope": "/",
  "display": "standalone"
}

Features:

  • App shortcuts for quick access (Android/Windows)
  • Proper categorization for app stores
  • Optimized orientation for mobile
  • Enhanced installation experience

Accessibility Improvements

Skip Navigation Link

// App.js
<a 
  href="#main-content" 
  className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:bg-white focus:text-black focus:rounded-lg focus:font-semibold"
>
  Skip to main content
</a>

Benefits:

  • Keyboard users can skip repetitive navigation
  • Improves accessibility for screen reader users
  • WCAG 2.1 compliance

Semantic HTML

<main id="main-content" role="main">
  {/* Page content */}
</main>

Benefits:

  • Better screen reader navigation
  • Improved SEO (search engines understand content hierarchy)
  • Proper document structure

Performance Metrics

Metric Before Optimization After Optimization Improvement
Initial Bundle Size 529 KB (gzipped) 238 KB (gzipped) -55%
Code Chunks 1 monolithic bundle 20 split chunks +1900%
Time to Interactive ~3.5s ~1.2s -66%
First Contentful Paint ~1.8s ~0.8s -56%
Lighthouse Performance ~65 ~85 +31%
SEO Score 6.6/10 9/10 +36%

SEO Score Breakdown

Category Before After Status
Meta Tags 9/10 10/10 ✅ Perfect
Structured Data 7/10 10/10 ✅ Excellent
Performance 4/10 7/10 ✅ Improved
Technical SEO 6/10 9/10 ✅ Excellent
Accessibility 7/10 9/10 ✅ Excellent

Technical Implementation Details

For Interview Discussions:

  1. Code Splitting Strategy

    • Used React.lazy() for route-based splitting
    • Implemented Suspense boundaries with loading fallbacks
    • Avoided over-splitting (kept common components in main bundle)
  2. SEO Component Architecture

    • Created reusable SEO component with prop validation
    • Used react-helmet-async for SSR compatibility
    • Implemented dynamic canonical URL generation
    • Protected user-specific routes with noindex
  3. Font Loading Optimization

    • Preconnect for faster DNS resolution
    • Preload with async loading to prevent render blocking
    • Noscript fallback for accessibility
  4. Structured Data Implementation

    • Chose appropriate Schema.org types for content
    • Validated JSON-LD with Google's Rich Results Test
    • Implemented breadcrumb navigation for better UX
  5. PWA Enhancements

    • Added app shortcuts for common user flows
    • Proper categorization for discoverability
    • Optimized for mobile-first experience
  6. Build Optimization

    • Configured webpack (via CRA) for optimal chunking
    • Analyzed bundle with source-map-explorer
    • Identified and lazy-loaded heavy dependencies (Recharts, jsPDF)

User Experience Impact

Positive Changes:

  • ✅ 63% faster initial page load
  • ✅ Smoother navigation with instant route transitions
  • ✅ Better mobile experience with PWA shortcuts
  • ✅ Improved accessibility for keyboard/screen reader users
  • ✅ Better social sharing with Open Graph tags

No Negative Impact:

  • ❌ No breaking changes to existing functionality
  • ❌ No UI/UX regressions
  • ❌ SEO changes are transparent to users
  • ❌ All existing features preserved

Tools & Technologies Used

Tool Purpose
react-helmet-async Dynamic meta tag management
React.lazy() Code splitting
Suspense Loading state management
Lighthouse Performance auditing
Google Rich Results Test Structured data validation
webpack-bundle-analyzer Bundle size analysis

Project Metrics

Metric Value
Frontend Bundle 476 KB (production)
API Response Time < 200ms (avg)
Lighthouse Performance 90+
Mobile Responsive ✅ 100%
Build Status ✅ Passing
Production Status ✅ Live
Frontend URL futuretracker.online
Backend Platform Render (Free Tier)

Quick Pitch Summary

"FutureTracker is a full-stack SaaS application I built to solve my own pain point of tracking internship applications. The standout features are real-time sync using Supabase WebSockets, multi-round interview pipelines with a performance-optimized API, per-internship prep workspaces, client-side ATS resume hints, and production-grade error handling. The biggest challenge was integrating Supabase Realtime with Clerk authentication - I solved it by understanding RLS policy conflicts and implementing a hybrid security model. The app is mobile-responsive, follows React best practices, and is deployed in production at futuretracker.online with a Render-hosted Express backend."


Repository & Deployment

GitHub: Venkat-Kolasani/FutureTracker
Branch: main

Live URLs

Service URL Platform
Frontend https://futuretracker.online Vercel
Backend API https://futurestack-api.onrender.com Render

Environment Variables Required

Vercel (Frontend):

Render (Backend):

  • NODE_ENV - Set to production
  • CORS_ORIGIN - Frontend URL (https://futuretracker.online)
  • CLERK_SECRET_KEY - Clerk secret key (must match frontend's publishable key environment)
  • CLERK_JWT_KEY - (Recommended) Clerk JWT public key (PEM format) for networkless token verification. Get from: Clerk Dashboard > API Keys > Advanced > JWT Public Key. Without this, the backend must fetch JWKS from Clerk's API on every request, which can fail on Render with TypeError: fetch failed.
  • SUPABASE_URL - Supabase project URL
  • SUPABASE_SERVICE_ROLE_KEY - Supabase service role key

Troubleshooting

"Session expired" / "Failed to load opportunities" on Dashboard

Symptoms: Dashboard shows loading state, then "Failed to load opportunities". In some cases users may also see a session-related toast and get redirected.

Incident Note (April 2026):

  • During local debugging, the primary outage was Supabase being inactive/offline.
  • Because backend failure paths were not clearly classified, the issue looked like auth/session expiry.
  • This led to debugging on Clerk/auth for longer than necessary.

Root Cause Categories:

  • Auth mismatch: Clerk token cannot be verified (401).
  • Database/backend unavailable: Supabase connection/bootstrap fails (503 recommended).

Check Render Logs for:

Log Message Cause Fix
Auth: JWT verification failed: invalid signature Clerk key mismatch between frontend/backend Ensure REACT_APP_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY, and JWT key are from the same Clerk instance
TypeError: fetch failed during JWT verify Backend can't reach Clerk JWKS endpoint Set CLERK_JWT_KEY / CLERK_JWT_PUBLIC_KEY for local JWT verification
Token has expired JWT token is stale Check Clerk dashboard for token lifetime settings
Auth bootstrap error or Supabase fetch/network errors Supabase unavailable Verify Supabase project status, SUPABASE_URL, and SUPABASE_SERVICE_ROLE_KEY
API returns 503 Service Unavailable Dependency outage (DB/auth bootstrap) Surface clear UI message and retry after dependency recovers

Quick Diagnostic Commands:

# Check if backend is alive
curl https://futurestack-api.onrender.com/api/health

# Check opportunities endpoint without auth (expect 401 Unauthorized)
curl -i https://futurestack-api.onrender.com/api/opportunities

# Test CORS preflight
curl -X OPTIONS -H "Origin: https://futuretracker.online" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: Authorization" \
  https://futurestack-api.onrender.com/api/opportunities

# Test authenticated request (replace token)
curl -i -H "Authorization: Bearer YOUR_CLERK_TOKEN" \
  https://futurestack-api.onrender.com/api/opportunities

Improvements To Prevent Recurrence

  1. Strict error classification in backend

    • Use 401 only for actual token/auth failures.
    • Use 503 for Supabase/dependency outages.
    • Include compact structured logs (type, service, code, message) for faster triage.
  2. Frontend messaging by status code

    • Avoid redirecting users on non-auth errors.
    • Show explicit “Database temporarily unavailable” message for 503.
    • Keep generic fallback only when no API message is provided.
  3. Dependency health checks

    • Add a deeper health endpoint (for example /api/health/deps) that validates Supabase reachability.
    • Use this in uptime monitors and deployment smoke tests.
  4. Post-deploy smoke tests

    • Validate sign-in, /api/opportunities, and one DB query after every deploy.
    • Fail deployment checks if auth passes but DB is unavailable.
  5. Runbook updates

    • First check service status (Supabase/Render) before rotating auth keys.
    • Add a short decision tree: 401 path (auth), 503 path (dependency), 500 path (application bug).