     1→import { NextRequest, NextResponse } from 'next/server';
     2→import { checkRateLimit, getClientIP, createRateLimitResponse } from '@/lib/rate-limit';
     3→
     4→// Cache to store logo URLs (in production, use Redis or database)
     5→const logoCache = new Map<string, { logoUrl: string | null; source: string; timestamp: number }>();
     6→const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
     7→
     8→interface LogoResponse {
     9→  logoUrl: string | null;
    10→  source: 'clearbit' | 'google' | 'none';
    11→}
    12→
    13→// Rate limit: 30 requests per 15 minutes per IP
    14→const LOGO_RATE_LIMIT = 30;
    15→const LOGO_WINDOW_MS = 15 * 60 * 1000;
    16→
    17→async function checkLogoExists(url: string): Promise<boolean> {
    18→  try {
    19→    const response = await fetch(url, {
    20→      method: 'HEAD',
    21→      headers: {
    22→        'User-Agent': 'RateRight/1.0',
    23→      },
    24→    });
    25→    return response.ok;
    26→  } catch {
    27→    return false;
    28→  }
    29→}
    30→
    31→export async function GET(request: NextRequest) {
    32→  try {
    33→    // --- Rate limiting ---
    34→    const clientIP = getClientIP(request);
    35→    const rateLimit = checkRateLimit(`company-logo:${clientIP}`, LOGO_RATE_LIMIT, LOGO_WINDOW_MS);
    36→
    37→    if (!rateLimit.allowed) {
    38→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    39→      return createRateLimitResponse(clientIP, retryAfter);
    40→    }
    41→
    42→    const searchParams = request.nextUrl.searchParams;
    43→    const domain = searchParams.get('domain');
    44→
    45→    if (!domain) {
    46→      return NextResponse.json<LogoResponse>(
    47→        { logoUrl: null, source: 'none' },
    48→        { status: 400, headers: getCORSHeaders(request) }
    49→      );
    50→    }
    51→
    52→    // Clean the domain (remove protocol if present)
    53→    const cleanDomain = domain.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
    54→
    55→    // Check cache first
    56→    const cacheKey = cleanDomain;
    57→    const cached = logoCache.get(cacheKey);
    58→    if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
    59→      return NextResponse.json<LogoResponse>({
    60→        logoUrl: cached.logoUrl,
    61→        source: cached.source as LogoResponse['source'],
    62→      }, {
    63→        headers: getCORSHeaders(request),
    64→      });
    65→    }
    66→
    67→    // Try Clearbit Logo API first
    68→    const clearbitUrl = `https://logo.clearbit.com/${cleanDomain}`;
    69→    const clearbitExists = await checkLogoExists(clearbitUrl);
    70→
    71→    if (clearbitExists) {
    72→      logoCache.set(cacheKey, {
    73→        logoUrl: clearbitUrl,
    74→        source: 'clearbit',
    75→        timestamp: Date.now(),
    76→      });
    77→
    78→      return NextResponse.json<LogoResponse>({
    79→        logoUrl: clearbitUrl,
    80→        source: 'clearbit',
    81→      }, {
    82→        headers: getCORSHeaders(request),
    83→      });
    84→    }
    85→
    86→    // Try Google Favicon as fallback
    87→    const googleUrl = `https://www.google.com/s2/favicons?domain=${cleanDomain}&sz=128`;
    88→    const googleExists = await checkLogoExists(googleUrl);
    89→
    90→    if (googleExists) {
    91→      logoCache.set(cacheKey, {
    92→        logoUrl: googleUrl,
    93→        source: 'google',
    94→        timestamp: Date.now(),
    95→      });
    96→
    97→      return NextResponse.json<LogoResponse>({
    98→        logoUrl: googleUrl,
    99→        source: 'google',
   100→      }, {
   101→        headers: getCORSHeaders(request),
   102→      });
   103→    }
   104→
   105→    // No logo found
   106→    logoCache.set(cacheKey, {
   107→      logoUrl: null,
   108→      source: 'none',
   109→      timestamp: Date.now(),
   110→    });
   111→
   112→    return NextResponse.json<LogoResponse>({
   113→      logoUrl: null,
   114→      source: 'none',
   115→    }, {
   116→      headers: getCORSHeaders(request),
   117→    });
   118→
   119→  } catch (error) {
   120→    console.error('Logo fetch error:', error);
   121→
   122→    return NextResponse.json<LogoResponse>(
   123→      { logoUrl: null, source: 'none' },
   124→      { status: 500, headers: getCORSHeaders(request) }
   125→    );
   126→  }
   127→}
   128→
   129→// CORS configuration - restrict to known origins
   130→const ALLOWED_ORIGINS = [
   131→  process.env.NEXT_PUBLIC_APP_URL, // Production URL from env
   132→  'http://localhost:3000',         // Local development
   133→  'https://localhost:3000',        // Local development with HTTPS
   134→].filter(Boolean) as string[];
   135→
   136→function getCORSHeaders(request: NextRequest): Record<string, string> {
   137→  const origin = request.headers.get('origin');
   138→
   139→  // Only allow specific origins, no wildcards (H5 fix)
   140→  const allowedOrigin = origin && ALLOWED_ORIGINS.includes(origin)
   141→    ? origin
   142→    : ALLOWED_ORIGINS[0] || 'null';
   143→
   144→  return {
   145→    'Access-Control-Allow-Origin': allowedOrigin,
   146→    'Access-Control-Allow-Methods': 'GET, OPTIONS',
   147→    'Access-Control-Allow-Headers': 'Content-Type',
   148→    'Vary': 'Origin',
   149→  };
   150→}
   151→
   152→// Add OPTIONS handler for CORS
   153→export async function OPTIONS(request: NextRequest) {
   154→  return new NextResponse(null, {
   155→    status: 200,
   156→    headers: getCORSHeaders(request),
   157→  });
   158→}

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Bash

find /home/ccuser/the-50-dollar-app/src/lib -type f -name "*.ts" | sort


/home/ccuser/the-50-dollar-app/src/lib/abn-utils.ts
/home/ccuser/the-50-dollar-app/src/lib/api-errors.ts
/home/ccuser/the-50-dollar-app/src/lib/company-utils.ts
/home/ccuser/the-50-dollar-app/src/lib/csrf-client.ts
/home/ccuser/the-50-dollar-app/src/lib/csrf.ts
/home/ccuser/the-50-dollar-app/src/lib/location-utils.ts
/home/ccuser/the-50-dollar-app/src/lib/messaging.ts
/home/ccuser/the-50-dollar-app/src/lib/rate-limit.ts
/home/ccuser/the-50-dollar-app/src/lib/sanitize.ts
/home/ccuser/the-50-dollar-app/src/lib/stripe/server.ts
/home/ccuser/the-50-dollar-app/src/lib/supabase/client.ts
/home/ccuser/the-50-dollar-app/src/lib/supabase/middleware.ts
/home/ccuser/the-50-dollar-app/src/lib/supabase/server.ts
/home/ccuser/the-50-dollar-app/src/lib/types.ts
/home/ccuser/the-50-dollar-app/src/lib/utils.ts


Let me check the remaining message API routes and the database schema files:


     1→import { createClient } from "@/lib/supabase/server";
     2→import { NextResponse } from "next/server";
     3→
     4→export async function GET() {
     5→  try {
     6→    const supabase = await createClient();
     7→
     8→    // Check if user is authenticated
     9→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    10→
    11→    if (authError || !user) {
    12→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    13→    }
    14→
    15→    // Get conversation previews using the database function
    16→    const { data: conversations, error } = await supabase
    17→      .rpc('get_conversation_previews', { user_id: user.id });
    18→
    19→    if (error) {
    20→      console.error("Error fetching conversations:", error);
    21→      return NextResponse.json(
    22→        { error: "Failed to fetch conversations" },
    23→        { status: 500 }
    24→      );
    25→    }
    26→
    27→    return NextResponse.json({ conversations: conversations || [] });
    28→  } catch (error) {
    29→    console.error("Error in messages API:", error);
    30→    return NextResponse.json(
    31→      { error: "Internal server error" },
    32→      { status: 500 }
    33→    );
    34→  }
    35→}

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→-- RateRight v2 Database Schema
     2→-- Run this in the Supabase SQL Editor
     3→
     4→-- ============================================
     5→-- ENUMS
     6→-- ============================================
     7→
     8→CREATE TYPE user_type AS ENUM ('worker', 'contractor');
     9→CREATE TYPE availability_type AS ENUM ('full-time', 'casual', 'weekends', 'flexible');
    10→CREATE TYPE rate_type AS ENUM ('hourly', 'daily');
    11→CREATE TYPE job_status AS ENUM ('draft', 'active', 'filled', 'closed');
    12→CREATE TYPE match_status AS ENUM ('suggested', 'applied', 'accepted', 'rejected', 'hired');
    13→CREATE TYPE action_status AS ENUM ('pending', 'accepted', 'rejected');
    14→CREATE TYPE payment_status AS ENUM ('pending', 'charged', 'refunded');
    15→CREATE TYPE notification_type AS ENUM ('match', 'hire_request', 'hire_confirmed', 'rating', 'job_posted', 'system');
    16→
    17→-- ============================================
    18→-- HELPER FUNCTION: auto-update updated_at
    19→-- ============================================
    20→
    21→CREATE OR REPLACE FUNCTION update_updated_at()
    22→RETURNS trigger AS $$
    23→BEGIN
    24→  NEW.updated_at = now();
    25→  RETURN NEW;
    26→END;
    27→$$ LANGUAGE plpgsql;
    28→
    29→-- ============================================
    30→-- TABLES
    31→-- ============================================
    32→
    33→-- Profiles (extends Supabase Auth)
    34→CREATE TABLE profiles (
    35→  id uuid PRIMARY KEY REFERENCES auth.users ON DELETE CASCADE,
    36→  type user_type NOT NULL,
    37→  name text,
    38→  phone text,
    39→  email text,
    40→  avatar_url text,
    41→  location point,
    42→  suburb text,
    43→  verified boolean DEFAULT false,
    44→  onboarding_completed boolean DEFAULT false,
    45→  created_at timestamptz DEFAULT now(),
    46→  updated_at timestamptz DEFAULT now(),
    47→  ai_generated boolean DEFAULT false
    48→);
    49→
    50→CREATE TRIGGER profiles_updated_at
    51→  BEFORE UPDATE ON profiles
    52→  FOR EACH ROW EXECUTE FUNCTION update_updated_at();
    53→
    54→-- Companies (contractor-specific)
    55→CREATE TABLE companies (
    56→  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    57→  profile_id uuid NOT NULL UNIQUE REFERENCES profiles ON DELETE CASCADE,
    58→  name text NOT NULL,
    59→  abn text,
    60→  address text,
    61→  industry text,
    62→  size text,
    63→  website text,
    64→  logo_url text,
    65→  ai_research jsonb,
    66→  created_at timestamptz DEFAULT now(),
    67→  updated_at timestamptz DEFAULT now()
    68→);
    69→
    70→CREATE TRIGGER companies_updated_at
    71→  BEFORE UPDATE ON companies
    72→  FOR EACH ROW EXECUTE FUNCTION update_updated_at();
    73→
    74→-- Worker profiles (worker-specific)
    75→CREATE TABLE worker_profiles (
    76→  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    77→  profile_id uuid NOT NULL UNIQUE REFERENCES profiles ON DELETE CASCADE,
    78→  trades text[] DEFAULT '{}',
    79→  experience_years integer,
    80→  certifications text[] DEFAULT '{}',
    81→  white_card_number text,
    82→  white_card_verified boolean DEFAULT false,
    83→  availability availability_type,
    84→  languages text[] DEFAULT '{}',
    85→  bio text,
    86→  voice_note_url text,
    87→  ai_extracted jsonb,
    88→  created_at timestamptz DEFAULT now(),
    89→  updated_at timestamptz DEFAULT now()
    90→);
    91→
    92→CREATE TRIGGER worker_profiles_updated_at
    93→  BEFORE UPDATE ON worker_profiles
    94→  FOR EACH ROW EXECUTE FUNCTION update_updated_at();
    95→
    96→-- Jobs
    97→CREATE TABLE jobs (
    98→  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    99→  company_id uuid NOT NULL REFERENCES companies ON DELETE CASCADE,
   100→  title text NOT NULL,
   101→  description text,
   102→  trade text NOT NULL,
   103→  location point,
   104→  suburb text,
   105→  site_address text,
   106→  rate_min decimal,
   107→  rate_max decimal,
   108→  rate_type rate_type DEFAULT 'hourly',
   109→  start_date date,
   110→  duration text,
   111→  workers_needed integer DEFAULT 1,
   112→  certifications_required text[] DEFAULT '{}',
   113→  status job_status DEFAULT 'draft',
   114→  ai_generated boolean DEFAULT false,
   115→  created_at timestamptz DEFAULT now(),
   116→  updated_at timestamptz DEFAULT now()
   117→);
   118→
   119→CREATE TRIGGER jobs_updated_at
   120→  BEFORE UPDATE ON jobs
   121→  FOR EACH ROW EXECUTE FUNCTION update_updated_at();
   122→
   123→-- Matches
   124→CREATE TABLE matches (
   125→  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
   126→  job_id uuid NOT NULL REFERENCES jobs ON DELETE CASCADE,
   127→  worker_id uuid NOT NULL REFERENCES profiles ON DELETE CASCADE,
   128→  status match_status DEFAULT 'suggested',
   129→  match_score decimal,
   130→  ai_summary text,
   131→  contractor_action action_status DEFAULT 'pending',
   132→  worker_action action_status DEFAULT 'pending',
   133→  hired_at timestamptz,
   134→  fee_charged boolean DEFAULT false,
   135→  created_at timestamptz DEFAULT now(),
   136→  updated_at timestamptz DEFAULT now(),
   137→  UNIQUE (job_id, worker_id)
   138→);
   139→
   140→CREATE TRIGGER matches_updated_at
   141→  BEFORE UPDATE ON matches
   142→  FOR EACH ROW EXECUTE FUNCTION update_updated_at();
   143→
   144→-- Ratings
   145→CREATE TABLE ratings (
   146→  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
   147→  match_id uuid NOT NULL REFERENCES matches ON DELETE CASCADE,
   148→  from_id uuid NOT NULL REFERENCES profiles ON DELETE CASCADE,
   149→  to_id uuid NOT NULL REFERENCES profiles ON DELETE CASCADE,
   150→  score integer NOT NULL CHECK (score BETWEEN 1 AND 5),

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→import type { NextConfig } from "next";
     2→
     3→const securityHeaders = [
     4→  { key: 'X-Frame-Options', value: 'DENY' },
     5→  { key: 'X-Content-Type-Options', value: 'nosniff' },
     6→  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
     7→  { key: 'X-DNS-Prefetch-Control', value: 'on' },
     8→  { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
     9→  { key: 'Permissions-Policy', value: 'camera=(), microphone=(self), geolocation=(self)' },
    10→];
    11→
    12→const nextConfig: NextConfig = {
    13→  reactStrictMode: true,
    14→  async headers() {
    15→    return [
    16→      // Global security headers on all routes
    17→      {
    18→        source: '/(.*)',
    19→        headers: [
    20→          ...securityHeaders,
    21→          {
    22→            key: 'Content-Security-Policy',
    23→            value: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self'; connect-src 'self' https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-src https://js.stripe.com https://hooks.stripe.com"
    24→          }
    25→        ],
    26→      },
    27→      // PWA manifest
    28→      {
    29→        source: '/manifest.json',
    30→        headers: [
    31→          { key: 'Content-Type', value: 'application/json' },
    32→          { key: 'Cache-Control', value: 'public, max-age=3600, immutable' },
    33→        ],
    34→      },
    35→      // Service worker — no cache
    36→      {
    37→        source: '/sw.js',
    38→        headers: [
    39→          { key: 'Content-Type', value: 'application/javascript; charset=utf-8' },
    40→          { key: 'Cache-Control', value: 'no-cache, no-store, must-revalidate' },
    41→        ],
    42→      },
    43→    ];
    44→  },
    45→};
    46→
    47→export default nextConfig;
    48→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Let me check a few more message API routes and types:


     1→import { createClient } from "@/lib/supabase/server";
     2→import { NextResponse } from "next/server";
     3→
     4→export async function POST(request: Request) {
     5→  try {
     6→    const supabase = await createClient();
     7→
     8→    // Check if user is authenticated
     9→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    10→
    11→    if (authError || !user) {
    12→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    13→    }
    14→
    15→    const { recipientId, initialMessage } = await request.json();
    16→
    17→    if (!recipientId) {
    18→      return NextResponse.json(
    19→        { error: "Recipient ID is required" },
    20→        { status: 400 }
    21→      );
    22→    }
    23→
    24→    // Verify recipient exists
    25→    const { data: recipient, error: recipientError } = await supabase
    26→      .from('profiles')
    27→      .select('id, name')
    28→      .eq('id', recipientId)
    29→      .single();
    30→
    31→    if (recipientError || !recipient) {
    32→      return NextResponse.json(
    33→        { error: "Recipient not found" },
    34→        { status: 404 }
    35→      );
    36→    }
    37→
    38→    // Get or create conversation using the database function
    39→    const { data: conversationId, error: conversationError } = await supabase
    40→      .rpc('get_or_create_conversation', {
    41→        user1_id: user.id,
    42→        user2_id: recipientId
    43→      });
    44→
    45→    if (conversationError) {
    46→      console.error("Error creating conversation:", conversationError);
    47→      return NextResponse.json(
    48→        { error: "Failed to create conversation" },
    49→        { status: 500 }
    50→      );
    51→    }
    52→
    53→    // If there's an initial message, send it
    54→    if (initialMessage?.trim()) {
    55→      const { error: messageError } = await supabase
    56→        .from('messages')
    57→        .insert({
    58→          conversation_id: conversationId,
    59→          sender_id: user.id,
    60→          content: initialMessage.trim()
    61→        });
    62→
    63→      if (messageError) {
    64→        console.error("Error sending initial message:", messageError);
    65→        // Don't fail the whole request if message fails
    66→      }
    67→    }
    68→
    69→    return NextResponse.json({
    70→      conversationId,
    71→      message: "Conversation created successfully"
    72→    });
    73→  } catch (error) {
    74→    console.error("Error in start conversation API:", error);
    75→    return NextResponse.json(
    76→      { error: "Internal server error" },
    77→      { status: 500 }
    78→    );
    79→  }
    80→}
    81→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→export type UserType = "worker" | "contractor";
     2→
     3→export type Availability = "full-time" | "casual" | "weekends" | "flexible";
     4→
     5→export type RateType = "hourly" | "daily";
     6→
     7→export type JobStatus = "draft" | "active" | "filled" | "closed";
     8→
     9→export type MatchStatus =
    10→  | "suggested"
    11→  | "applied"
    12→  | "accepted"
    13→  | "rejected"
    14→  | "hired";
    15→
    16→export type ActionStatus = "pending" | "accepted" | "rejected";
    17→
    18→export type PaymentStatus = "pending" | "charged" | "refunded";
    19→
    20→export type NotificationType = "match" | "hire_request" | "hire_confirmed" | "rating" | "job_posted" | "system";
    21→
    22→export interface Profile {
    23→  id: string;
    24→  type: UserType;
    25→  name: string | null;
    26→  phone: string | null;
    27→  email: string | null;
    28→  avatar_url: string | null;
    29→  /**
    30→   * Location as { lat, lng } object
    31→   * Note: Database stores this as PostgreSQL point type
    32→   * Use postgresPointToJson() when reading from DB
    33→   * Use jsonToPostgresPoint() when writing to DB
    34→   */
    35→  location: { lat: number; lng: number } | null;
    36→  suburb: string | null;
    37→  verified: boolean;
    38→  onboarding_completed: boolean;
    39→  created_at: string;
    40→  updated_at: string;
    41→  ai_generated: boolean;
    42→}
    43→
    44→export interface Company {
    45→  id: string;
    46→  profile_id: string;
    47→  name: string;
    48→  abn: string | null;
    49→  address: string | null;
    50→  industry: string | null;
    51→  size: string | null;
    52→  website: string | null;
    53→  logo_url: string | null;
    54→  ai_research: Record<string, unknown> | null;
    55→  created_at: string;
    56→  updated_at: string;
    57→}
    58→
    59→export interface WorkerProfile {
    60→  id: string;
    61→  profile_id: string;
    62→  trades: string[];
    63→  experience_years: number | null;
    64→  certifications: string[];
    65→  white_card_number: string | null;
    66→  white_card_verified: boolean;
    67→  availability: Availability | null;
    68→  languages: string[];
    69→  bio: string | null;
    70→  voice_note_url: string | null;
    71→  ai_extracted: Record<string, unknown> | null;
    72→  created_at: string;
    73→  updated_at: string;
    74→}
    75→
    76→export interface Job {
    77→  id: string;
    78→  company_id: string;
    79→  title: string;
    80→  description: string | null;
    81→  trade: string;
    82→  /**
    83→   * Location as { lat, lng } object
    84→   * Note: Database stores this as PostgreSQL point type
    85→   * Use postgresPointToJson() when reading from DB
    86→   * Use jsonToPostgresPoint() when writing to DB
    87→   */
    88→  location: { lat: number; lng: number } | null;
    89→  suburb: string | null;
    90→  site_address: string | null;
    91→  rate_min: number | null;
    92→  rate_max: number | null;
    93→  rate_type: RateType;
    94→  start_date: string | null;
    95→  duration: string | null;
    96→  workers_needed: number;
    97→  certifications_required: string[];
    98→  status: JobStatus;
    99→  ai_generated: boolean;
   100→  created_at: string;
   101→  updated_at: string;
   102→}
   103→
   104→export interface Match {
   105→  id: string;
   106→  job_id: string;
   107→  worker_id: string;
   108→  status: MatchStatus;
   109→  match_score: number | null;
   110→  ai_summary: string | null;
   111→  contractor_action: ActionStatus;
   112→  worker_action: ActionStatus;
   113→  hired_at: string | null;
   114→  fee_charged: boolean;
   115→  created_at: string;
   116→  updated_at: string;
   117→}
   118→
   119→export interface Rating {
   120→  id: string;
   121→  match_id: string;
   122→  from_id: string;
   123→  to_id: string;
   124→  score: number;
   125→  comment: string | null;
   126→  created_at: string;
   127→}
   128→
   129→export interface Payment {
   130→  id: string;
   131→  match_id: string;
   132→  company_id: string;
   133→  amount: number;
   134→  status: PaymentStatus;
   135→  stripe_payment_id: string | null;
   136→  created_at: string;
   137→}
   138→
   139→export interface Notification {
   140→  id: string;
   141→  profile_id: string;
   142→  type: NotificationType;
   143→  title: string;
   144→  body: string | null;
   145→  data: Record<string, unknown> | null;
   146→  read: boolean;
   147→  created_at: string;
   148→}
   149→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→import Stripe from 'stripe';
     2→
     3→// Initialize Stripe with secret key
     4→export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
     5→  apiVersion: '2026-01-28.clover', // Use latest API version
     6→  typescript: true,
     7→});
     8→
     9→// Validate Stripe is configured
    10→export function validateStripeConfig(): void {
    11→  if (!process.env.STRIPE_SECRET_KEY) {
    12→    throw new Error('STRIPE_SECRET_KEY is not configured');
    13→  }
    14→  if (!process.env.STRIPE_WEBHOOK_SECRET) {
    15→    throw new Error('STRIPE_WEBHOOK_SECRET is not configured');
    16→  }
    17→}
    18→
    19→// Create a PaymentIntent for hiring a worker
    20→export async function createPaymentIntent({
    21→  amount,
    22→  matchId,
    23→  companyId,
    24→  companyName,
    25→  description,
    26→}: {
    27→  amount: number;
    28→  matchId: string;
    29→  companyId: string;
    30→  companyName: string;
    31→  description: string;
    32→}) {
    33→  try {
    34→    const paymentIntent = await stripe.paymentIntents.create({
    35→      amount: Math.round(amount * 100), // Convert to cents
    36→      currency: 'aud',
    37→      automatic_payment_methods: {
    38→        enabled: true,
    39→      },
    40→      metadata: {
    41→        match_id: matchId,
    42→        company_id: companyId,
    43→        company_name: companyName,
    44→        description,
    45→      },
    46→      description: `RateRight Hire Fee - ${description}`,
    47→    });
    48→
    49→    return {
    50→      clientSecret: paymentIntent.client_secret,
    51→      paymentIntentId: paymentIntent.id,
    52→    };
    53→  } catch (error) {
    54→    console.error('Error creating PaymentIntent:', error);
    55→    throw new Error('Failed to create payment intent');
    56→  }
    57→}
    58→
    59→// Retrieve a PaymentIntent
    60→export async function retrievePaymentIntent(paymentIntentId: string) {
    61→  try {
    62→    return await stripe.paymentIntents.retrieve(paymentIntentId);
    63→  } catch (error) {
    64→    console.error('Error retrieving PaymentIntent:', error);
    65→    throw new Error('Failed to retrieve payment intent');
    66→  }
    67→}
    68→
    69→// Construct webhook event with signature verification
    70→export function constructWebhookEvent(
    71→  payload: string | Buffer,
    72→  signature: string
    73→): Stripe.Event {
    74→  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
    75→
    76→  if (!webhookSecret) {
    77→    throw new Error('STRIPE_WEBHOOK_SECRET is not configured');
    78→  }
    79→
    80→  try {
    81→    return stripe.webhooks.constructEvent(payload, signature, webhookSecret);
    82→  } catch (error) {
    83→    console.error('Webhook signature verification failed:', error);
    84→    throw new Error('Invalid webhook signature');
    85→  }
    86→}
    87→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Now let me check the remaining message routes and overall project structure:


find /home/ccuser/the-50-dollar-app/src/app/api/messages -name "route.ts" -exec sh -c 'echo "=== {} ===" && head -40 {}' \;


=== /home/ccuser/the-50-dollar-app/src/app/api/messages/start/route.ts ===
import { createClient } from "@/lib/supabase/server";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  try {
    const supabase = await createClient();

    // Check if user is authenticated
    const { data: { user }, error: authError } = await supabase.auth.getUser();

    if (authError || !user) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const { recipientId, initialMessage } = await request.json();

    if (!recipientId) {
      return NextResponse.json(
        { error: "Recipient ID is required" },
        { status: 400 }
      );
    }

    // Verify recipient exists
    const { data: recipient, error: recipientError } = await supabase
      .from('profiles')
      .select('id, name')
      .eq('id', recipientId)
      .single();

    if (recipientError || !recipient) {
      return NextResponse.json(
        { error: "Recipient not found" },
        { status: 404 }
      );
    }

    // Get or create conversation using the database function
    const { data: conversationId, error: conversationError } = await supabase
      .rpc('get_or_create_conversation', {
=== /home/ccuser/the-50-dollar-app/src/app/api/messages/[id]/send/route.ts ===
import { createClient } from "@/lib/supabase/server";
import { NextRequest, NextResponse } from "next/server";
import { validateCSRFToken, isSameOrigin } from "@/lib/csrf";

export async function POST(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    // CSRF protection: validate same-origin and token
    if (!isSameOrigin(request)) {
      return NextResponse.json({ error: 'Cross-origin request not allowed' }, { status: 403 });
    }
    const csrfValid = await validateCSRFToken(request);
    if (!csrfValid) {
      return NextResponse.json({ error: 'Invalid or missing CSRF token' }, { status: 403 });
    }

    const { id: conversationId } = await params;
    const supabase = await createClient();

    // Check if user is authenticated
    const { data: { user }, error: authError } = await supabase.auth.getUser();

    if (authError || !user) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }
    const { content, attachments = [] } = await request.json();

    if (!content?.trim()) {
      return NextResponse.json(
        { error: "Message content is required" },
        { status: 400 }
      );
    }

    // Verify user is a participant in this conversation
    const { data: participant, error: participantError } = await supabase
      .from('conversation_participants')
      .select('id')
=== /home/ccuser/the-50-dollar-app/src/app/api/messages/[id]/route.ts ===
import { createClient } from "@/lib/supabase/server";
import { NextResponse } from "next/server";

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  try {
    const { id: conversationId } = await params;
    const supabase = await createClient();

    // Check if user is authenticated
    const { data: { user }, error: authError } = await supabase.auth.getUser();

    if (authError || !user) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    // Verify user is a participant in this conversation
    const { data: participant, error: participantError } = await supabase
      .from('conversation_participants')
      .select('id')
      .eq('conversation_id', conversationId)
      .eq('profile_id', user.id)
      .single();

    if (participantError || !participant) {
      return NextResponse.json({ error: "Conversation not found" }, { status: 404 });
    }

    // Get conversation details with other participant info
    const { data: conversationData, error: conversationError } = await supabase
      .from('conversation_participants')
      .select(`
        profile_id,
        profiles!inner(
          id,
          name,
          avatar_url,
          type
=== /home/ccuser/the-50-dollar-app/src/app/api/messages/route.ts ===
import { createClient } from "@/lib/supabase/server";
import { NextResponse } from "next/server";

export async function GET() {
  try {
    const supabase = await createClient();

    // Check if user is authenticated
    const { data: { user }, error: authError } = await supabase.auth.getUser();

    if (authError || !user) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    // Get conversation previews using the database function
    const { data: conversations, error } = await supabase
      .rpc('get_conversation_previews', { user_id: user.id });

    if (error) {
      console.error("Error fetching conversations:", error);
      return NextResponse.json(
        { error: "Failed to fetch conversations" },
        { status: 500 }
      );
    }

    return NextResponse.json({ conversations: conversations || [] });
  } catch (error) {
    console.error("Error in messages API:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}


Perfect! Now let me verify all the essential information by creating a comprehensive summary. Let me check for any other key configuration files:


ls -la /home/ccuser/the-50-dollar-app/ | grep -E "\.config|\.json|tsconfig"


-rw-rw-r--   1 ccuser ccuser     467 Feb  8 11:18 components.json
-rw-rw-r--   1 ccuser ccuser     465 Feb  8 11:18 eslint.config.mjs
-rw-rw-r--   1 ccuser ccuser    1651 Feb  8 11:18 next.config.ts
-rw-rw-r--   1 ccuser ccuser  305740 Feb  8 11:18 package-lock.json
-rw-rw-r--   1 ccuser ccuser    1080 Feb  8 11:18 package.json
-rw-rw-r--   1 ccuser ccuser      94 Feb  8 11:18 postcss.config.mjs
-rw-rw-r--   1 ccuser ccuser     753 Feb  8 11:18 tsconfig.json
-rw-rw-r--   1 ccuser ccuser 1289934 Feb  8 19:24 tsconfig.tsbuildinfo
-rw-rw-r--   1 ccuser ccuser     474 Feb  8 11:18 vercel.json


Excellent! Now I have all the information I need. Let me compile a comprehensive report:

---

## COMPREHENSIVE EXPLORATION REPORT: `/home/ccuser/the-50-dollar-app`

### 1. PROJECT STRUCTURE - Top-Level Directories and Key Config Files

**Root-level directories:**
- `/src` - Application source code (Next.js App Router)
- `/supabase` - Database schemas, migrations, and SQL files
- `/public` - Static assets
- `/node_modules` - Dependencies
- `/docs` - Documentation
- `/.next` - Next.js build output
- `/.git` - Git repository

**Key configuration files:**
- `package.json` - v0.1.0 (private, name: rateright-v2-scaffold)
- `next.config.ts` - Security headers, CSP, PWA manifest config
- `tsconfig.json` - TypeScript configuration
- `components.json` - shadcn/ui configuration
- `eslint.config.mjs` - ESLint configuration
- `postcss.config.mjs` - Tailwind CSS/PostCSS setup
- `vercel.json` - Vercel deployment config
- `.env.local` - Environment variables (Supabase, OpenAI, Stripe, ABR)
- `.gitignore` - Standard Next.js ignores

---

### 2. TECH STACK

**Frontend:**
- **Next.js 16.1.6** with App Router
- **React 19.2.3** (React 19 with experimental features)
- **TypeScript 5.9.3** (strict mode)
- **Tailwind CSS 4** with @tailwindcss/postcss
- **shadcn/ui** (via radix-ui v1.4.3, class-variance-authority, clsx, tailwind-merge)
- **Lucide React 0.563.0** (icons)

**Backend & Database:**
- **Supabase** (Postgres via @supabase/ssr v0.8.0, @supabase/supabase-js v2.94.0)
  - Project ref: `eciepjpcyfurbkfzekok`
  - URL: `https://eciepjpcyfurbkfzekok.supabase.co`
  - Anon Key + Service Role Key configured
  - Database connection via session-mode pooler for DDL

**Authentication:**
- Supabase Auth (email/phone signup)
- Custom server-side session handling via `@supabase/ssr`
- JWT token validation in middleware
- CSRF token protection

**Payments:**
- **Stripe** (v20.3.1 for server, @stripe/react-stripe-js v5.6.0 + @stripe/stripe-js v8.7.0 for client)
- Stripe API v2026-01-28.clover
- Live keys configured (STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY)
- Webhook signature verification

**AI/LLM Services:**
- **OpenAI v6.18.0** (GPT-4o-mini for text generation, Whisper-1 for audio transcription)
- Input sanitization and prompt injection prevention
- CSRF protection on AI endpoints

**Other Libraries:**
- **react-hook-form v7.71.1** - Form handling
- **zod v4.3.6** - Schema validation
- **date-fns v4.1.0** - Date utilities
- **react-hot-toast v2.6.0** - Notifications
- **@hookform/resolvers v5.2.2** - Hook Form resolvers

**External APIs:**
- ABN Lookup (Australian Business Register JSON API)
- Clearbit Logo API
- Google Favicon API
- Resend (email service)

---

### 3. ALL API ROUTES (Complete List)

**Authentication Routes:**
- `/api/auth/csrf` - **GET** - Generate CSRF token for authenticated users
- `/api/auth/verify-email` - **POST** - Email verification endpoint (rate-limited, CSRF protected)

**Payment Routes:**
- `/api/payments/create` - **POST** - Create Stripe PaymentIntent for $50 hire fee
  - Rate-limited (10/15min per user)
  - Verifies user owns company, validates match exists
  - Returns clientSecret + publishableKey
  - CSRF protected

**AI/Profile Generation Routes:**
- `/api/ai/generate-profile` - **POST** - Generate worker profile from trade/experience/location/availability
  - Uses GPT-4o-mini with JSON response format
  - Rate-limited (5/15min)
  - Sanitized inputs, JSON response parsing
  - Returns bio, skills[], certifications[]
  - CSRF protected

- `/api/ai/transcribe-voice` - **POST** - Transcribe audio file using Whisper-1
  - Accepts multipart FormData with "audio" file
  - Rate-limited (10/15min)
  - CSRF + same-origin protected
  - Returns transcribed text

- `/api/ai/parse-profile-voice` - **POST** - Extract structured data from voice transcript
  - Parses transcript to extract: trade, experience_years, location, availability
  - Validates against VALID_TRADES and VALID_AVAILABILITY arrays
  - Rate-limited (10/15min)
  - CSRF + same-origin protected
  - Returns JSON with validated fields (nulls for missing/invalid data)

**Matching Routes:**
- `/api/match/find-matches` - **POST** - Find and create suggested matches for a job
  - Body: { job_id, }
  - Scoring: Trade (40%) + Availability (20%) + Experience (15%) + Certifications (15%) + Ratings (10%) + Location bonus (10 max)
  - Returns top 20 matches with scores and summaries
  - Helper functions: getRelatedTradeScore(), getAvailabilityScore(), getExperienceScore(), getCertificationScore(), getLocationScore()
  - Rate-limited (10/15min)

**Company/Business Routes:**
- `/api/abn-lookup` - **GET** - Australian Business Register company lookup
  - Query params: `abn=XXXXXXXXXXX`, optional `email=`
  - Validates ABN checksum locally
  - Returns: name, abn, status, state, postcode, type, typeCode, gstRegistered, acn, directorVerification
  - Dev mode: Returns mock data when ABR_GUID not configured
  - Director verification stub (ASIC integration pending)
  - Rate-limited (10/15min)

- `/api/company-logo` - **GET** - Fetch company logo
  - Query param: `domain=`
  - Tries Clearbit API first, falls back to Google Favicon
  - 24-hour memory cache
  - CORS-restricted (whitelisted origins only)
  - Rate-limited (30/15min)
  - GET + OPTIONS support

**Messaging Routes:**
- `/api/messages` - **GET** - Get all conversation previews for user
  - Uses RPC function `get_conversation_previews(user_id)`
  - Returns list of conversations with last message previews

- `/api/messages/start` - **POST** - Create conversation or get existing one
  - Body: { recipientId, initialMessage? }
  - Uses RPC function `get_or_create_conversation(user1_id, user2_id)`
  - Optionally sends initial message
  - Returns conversationId

- `/api/messages/[id]` - **GET** - Get conversation details and all messages
  - Param: conversationId
  - Verifies user is conversation participant
  - Returns conversation details + other participant info

- `/api/messages/[id]/send` - **POST** - Send message in conversation
  - Param: conversationId
  - Body: { content, attachments? }
  - Verifies user is participant
  - CSRF protected

**Webhook Routes:**
- `/api/webhooks/stripe` - **POST** - Stripe webhook handler
  - Handles: payment_intent.succeeded, payment_intent.payment_failed, payment_intent.canceled
  - Signature verification via Stripe SDK
  - On success: Updates payment status to "charged", match status to "hired", creates worker notification
  - Idempotent (uses metadata.match_id for deduplication)

---

### 4. DATABASE SETUP - Schema, Migrations, and Configuration

**Database Type:** PostgreSQL via Supabase

**Schema File:** `/supabase/schema.sql`

**Core Tables (11 total):**

1. **profiles** - Extended Supabase Auth
   - id (uuid PK, references auth.users)
   - type (user_type enum: worker/contractor)
   - name, phone, email, avatar_url
   - location (postgres point type), suburb
   - verified, onboarding_completed (boolean)
   - ai_generated, created_at, updated_at

2. **companies** - Contractor-specific data
   - id, profile_id (unique FK to profiles), name
   - abn (text), address, industry, size, website, logo_url
   - ai_research (jsonb - stored ABN/research results)
   - created_at, updated_at

3. **worker_profiles** - Worker-specific data
   - id, profile_id (unique FK), trades (text array)
   - experience_years (integer), certifications (text array)
   - white_card_number, white_card_verified (boolean)
   - availability (availability_type enum)
   - languages (text array), bio, voice_note_url
   - ai_extracted (jsonb), created_at, updated_at

4. **jobs** - Job postings
   - id, company_id (FK), title, description
   - trade, location (postgres point), suburb, site_address
   - rate_min, rate_max (decimal), rate_type (hourly/daily)
   - start_date, duration, workers_needed
   - certifications_required (text array)
   - status (job_status enum: draft/active/filled/closed)
   - ai_generated, created_at, updated_at

5. **matches** - Job-to-worker matches
   - id, job_id (FK), worker_id (FK to profiles)
   - status (match_status enum: suggested/applied/accepted/rejected/hired)
   - match_score (decimal 0-100), ai_summary (text)
   - contractor_action, worker_action (action_status: pending/accepted/rejected)
   - hired_at, fee_charged (boolean)
   - created_at, updated_at
   - UNIQUE constraint on (job_id, worker_id)

6. **ratings** - Post-job ratings
   - id, match_id (FK), from_id, to_id (FK to profiles)
   - score (integer 1-5, CHECK constraint)
   - comment (text), created_at

7. **payments** - Stripe payment tracking
   - id, match_id (FK), company_id (FK)
   - amount (decimal, default 50.00 AUD)
   - status (payment_status enum: pending/charged/refunded)
   - stripe_payment_id (text), created_at

8. **notifications** - Push notifications
   - id, profile_id (FK), type (notification_type enum)
   - title, body, data (jsonb), read (boolean), created_at

9. **conversations** - Messaging conversations
   - id, user1_id, user2_id (FK to profiles)
   - created_at, updated_at

10. **conversation_participants** - Many-to-many join
    - id, conversation_id (FK), profile_id (FK)
    - created_at

11. **messages** - Chat messages
    - id, conversation_id (FK), sender_id (FK to profiles)
    - content (text), attachments (jsonb)
    - created_at

**Enums:**
- user_type: worker, contractor
- availability_type: full-time, casual, weekends, flexible
- rate_type: hourly, daily
- job_status: draft, active, filled, closed
- match_status: suggested, applied, accepted, rejected, hired
- action_status: pending, accepted, rejected
- payment_status: pending, charged, refunded
- notification_type: match, hire_request, hire_confirmed, rating, job_posted, system

**Helper Functions:**
- `update_updated_at()` - Trigger function for automatic updated_at updates on all tables

**RLS (Row Level Security):**
- Enabled on all tables (per CLAUDE.md)
- Auth trigger `handle_new_user` auto-creates profile on signup

**Migrations:**
- `/supabase/migrations/20260208000001_add_abn_uniqueness_constraint.sql`
- `/supabase/migrations/20260209000001_add_abn_unique_constraint.sql`
- `/supabase/migrations/20260209000002_add_delete_policies.sql`
- `/supabase/migrations/20260209000002_deep_audit_fixes.sql`
- Additional setup files: `schema.sql`, `add-messaging.sql`, `fix-rls-policies-migration.sql`, `fix-critical-rls-insert-policies.sql`

**Database Functions:**
- `get_conversation_previews(user_id uuid)` - RPC for messaging
- `get_or_create_conversation(user1_id, user2_id)` - RPC for conversation management

**Connection Details:**
- Service role key for admin operations (bypasses RLS when needed)
- Pooler URL: `aws-0-ap-southeast-2.pooler.supabase.com:5432`
- Database: `postgres`

---

### 5. MIDDLEWARE

**Main Middleware File:** `/src/middleware.ts`
- Imports `updateSession` from `@/lib/supabase/middleware`
- Config matcher excludes static files/images
- Routes all requests through session update

**Supabase Middleware:** `/src/lib/supabase/middleware.ts`
- Creates server-side Supabase client with SSR cookie handling
- Uses `getUser()` (validates JWT with Supabase, not unverified cookies)
- **Route Classification:**
  - Protected paths: /dashboard, /contractor, /worker, /messages, /profile
  - Auth pages: /auth/login, /auth/signup
  - Verify email page: /auth/verify-email

- **Logic:**
  - Unauthenticated + protected (non-signup) → redirect to /auth/login
  - Unverified email + protected (non-signup) → redirect to /auth/verify-email
  - Verified email + auth/verify pages → redirect to /dashboard
  - Signup flows allowed for unverified users to complete onboarding

---

### 6. ENVIRONMENT VARIABLES (from .env.local)

**Supabase:**
- `NEXT_PUBLIC_SUPABASE_URL` - Project URL
- `NEXT_PUBLIC_SUPABASE_ANON_KEY` - Client-side anon key (JWT)
- `SUPABASE_SERVICE_ROLE_KEY` - Admin key (server-side only)
- `SUPABASE_SERVICE_KEY` - Alias for service role key
- `DATABASE_URL` - Direct Postgres connection (session mode)

**Business APIs:**
- `ABR_GUID` - Australian Business Register authentication (requires registration)

**Email:**
- `RESEND_API_KEY` - Email service (Resend)

**AI/LLM:**
- `OPENAI_API_KEY` - OpenAI API key for GPT-4o-mini and Whisper-1

**Payment:**
- `STRIPE_SECRET_KEY` - Server-side secret (live key)
- `STRIPE_PUBLISHABLE_KEY` - Client-side public key (live)
- `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` - Exposed to client
- `STRIPE_WEBHOOK_SECRET` - Webhook signature verification

**Application:**
- `NEXT_PUBLIC_APP_URL` - https://rivet.rateright.com.au (production URL for redirects)

---

### 7. SECURITY MEASURES

**Headers (next.config.ts):**
- X-Frame-Options: DENY (no iframes)
- X-Content-Type-Options: nosniff
- Referrer-Policy: strict-origin-when-cross-origin
- X-DNS-Prefetch-Control: on
- Strict-Transport-Security: max-age=63072000; includeSubDomains; preload (2 years)
- Permissions-Policy: camera=(), microphone=(self), geolocation=(self)
- Content-Security-Policy: restrictive (script/style/image/font/connect/form whitelisting, Stripe iframe support)

**CSRF Protection:**
- CSRF token generation and validation
- Same-origin checks
- Token required on state-changing operations

**Rate Limiting:**
- Per-IP rate limiting with in-memory store
- Varies by endpoint:
  - Auth: 5/15min
  - Payment: 10/15min
  - ABN Lookup: 10/15min
  - AI endpoints: 5-10/15min
  - Logo: 30/15min

**Input Validation:**
- Sanitization function for text inputs (max lengths enforced)
- ABN checksum validation
- Enum validation for structured fields
- Type validation via Zod

**Authentication:**
- JWT-based via Supabase Auth
- getUser() for server-side validation (not unverified cookies)
- Email verification required for protected routes
- Auth trigger creates profiles automatically

**Payments:**
- Stripe API version pinning
- Webhook signature verification
- PaymentIntent metadata tracking
- Idempotency via match_id in metadata

---

### 8. KEY LIBRARY FILES & UTILITIES

**Supabase Clients:**
- `/src/lib/supabase/server.ts` - Server-side client with createClient() and getSupabaseAdmin()
- `/src/lib/supabase/client.ts` - Client-side browser client
- `/src/lib/supabase/middleware.ts` - Middleware session handler

**Utilities:**
- `/src/lib/abn-utils.ts` - ABN validation and formatting
- `/src/lib/sanitize.ts` - Input sanitization
- `/src/lib/csrf.ts` - CSRF token generation/validation
- `/src/lib/csrf-client.ts` - Client-side CSRF helpers
- `/src/lib/rate-limit.ts` - Rate limiting logic
- `/src/lib/api-errors.ts` - Error handling with error codes
- `/src/lib/company-utils.ts` - Company-related utilities
- `/src/lib/location-utils.ts` - Location/geolocation helpers
- `/src/lib/messaging.ts` - Messaging utilities
- `/src/lib/stripe/server.ts` - Stripe server integration
- `/src/lib/types.ts` - TypeScript interfaces for all tables

**Type Definitions:**
```typescript
Profile, Company, WorkerProfile, Job, Match, Rating, Payment, Notification, Conversation, Message
UserType, Availability, RateType, JobStatus, MatchStatus, ActionStatus, PaymentStatus, NotificationType
```

---

### 9. PROTECTED ROUTES & AUTHENTICATION FLOW

**Protected Routes (require login + verified email):**
- `/dashboard` - Main dashboard
- `/contractor/*` - Contractor flows (signup, post-job, matches, payment-success)
- `/worker/*` - Worker flows (signup, profile build, jobs)
- `/messages/*` - Messaging system

**Public Routes:**
- `/auth/login` - Login page
- `/auth/signup` - Signup choice page
- `/auth/callback` - OAuth callback handler
- `/auth/verify-email` - Email verification page
- `/legal/*` - Legal pages (terms, privacy)
- `/faq` - FAQ page
- `/offline` - Offline page

---

### 10. DEPLOYMENT & BUILD CONFIGURATION

**Build System:** Next.js 16
- App Router (src/app)
- TypeScript strict mode
- Tailwind CSS v4

**Deployment Target:** Vercel (via vercel.json)

**Dev Server:**
- `npm run dev` - Next.js dev server on port 3000

**Build:**
- `npm run build` - Production build
- `npm start` - Production server

**Linting:**
- `npm run lint` - ESLint v9

---

## SUMMARY TABLE

| Category | Details |
|----------|---------|
| **Framework** | Next.js 16.1.6 with App Router |
| **Runtime** | Node.js (Vercel) |
| **Database** | Supabase PostgreSQL (eciepjpcyfurbkfzekok) |
| **Auth** | Supabase Auth + JWT + CSRF tokens |
| **Payments** | Stripe (live, $50 AUD flat fee) |
| **AI Services** | OpenAI (GPT-4o-mini, Whisper-1) |
| **Frontend UI** | React 19, Tailwind CSS 4, shadcn/ui, Lucide React |
| **API Routes** | 17 route handlers across 8 categories |
| **Database Tables** | 11 tables with RLS enabled |
| **Rate Limiting** | In-memory per-IP/per-user, varies by endpoint |
| **Security** | CSRF, CSP, strict headers, input validation, JWT verification |
| **External APIs** | ABN Lookup, Clearbit, Google Favicon, Resend, OpenAI, Stripe |

---

This is a production-ready AI-first construction hiring marketplace with comprehensive security, rate limiting, and database schema in place.