Skip to content
Slash-495Public

About

Velora is an all-in-one AI career workspace and resume studio designed to accelerate your job search with ATS scoring, mock interviews, and application tracking.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

57 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸš€ Velora β€” AI-Powered Career Intelligence & Resume Studio

CI

Velora is a full-stack AI career workspace built with Next.js 15, Supabase, and Google Gemini (gemini-flash-latest). It combines a live Visual/LaTeX Resume Studio with optimistic concurrency control, ATS resume scoring and job-description tailoring, an idempotent Kanban application tracker, an AI mock interview simulator, cover letter generation, and career activity analytics.

πŸ”— Live Demo: https://velora-ai.vercel.app


πŸ“Έ Workspace Preview

Place screenshots of the running workspace in public/screenshots/ (dashboard.png, resume-studio.png, ai-tailoring.png, kanban-board.png) or a walkthrough recording at public/screenshots/demo.gif.

Dashboard & Career Heatmap Resume Studio (Visual + LaTeX)
Dashboard Preview Resume Studio Preview
Overview metrics, career tracks, and 18-month activity grid 800ms debounced autosave, live A4 preview, and optimistic conflict detection
AI ATS Tailoring & Optimization Kanban Application Pipeline
AI Tailoring Preview Kanban Tracker Preview
Keyword gap analysis and one-click JD tailoring via Gemini Idempotent drag-and-drop across Wishlist, Applied, OA, Interview, Final Round, Offer, and Rejected

πŸ—οΈ System Architecture

flowchart LR
  subgraph Browser["Browser Client (React 19 / Next.js Client Components)"]
    UI["Dashboard Pages & Modals"]
    Studio["Resume Studio\n(800ms Debounced Autosave +\nOptimistic Version Check)"]
    Kanban["Kanban Board\n(Optimistic UI + In-Flight Dedup)"]
    Demo["Logged-Out Demo Store\n(localStorage velora_demo_*_v3 +\nOne-Time Sign-In Migration)"]
  end

  subgraph NextServer["Next.js 15 App Router Server"]
    MW["middleware.ts\n(Session Refresh & Route Protection)"]
    AuthActions["Server Actions\napp/(auth)/actions.ts"]
    subgraph AIRoutes["AI Route Handlers (/api/ai/*)"]
      Guard["lib/api-guard.ts\nrequireUser() + Input Caps +\nenforceRateLimit()"]
      Retry["lib/ai-retry-cache.ts\ncallWithRetry() (Exp Backoff + 25s Timeout)"]
      Analyzer["lib/ats-analyzer.ts\nPrompt Builder & Score Normalizer"]
    end
  end

  subgraph Supabase["Supabase Platform"]
    SBAuth["Supabase Auth\n(auth.users / OAuth & Password)"]
    SBDB[("PostgreSQL\n(Owner-Scoped RLS +\nversion & updated_at Triggers)")]
    SBStorage["Supabase Storage\n(resume-photos Bucket)"]
  end

  Gemini["Google Gemini API\n(gemini-flash-latest)"]

  UI --> MW
  UI --> AuthActions
  AuthActions --> SBAuth
  MW --> SBAuth

  Studio -->|lib/data/resumes.ts| SBDB
  Kanban -->|lib/data/applications.ts| SBDB
  UI -->|lib/data/tracks.ts| SBDB
  Studio -.->|Logged-out fallback| Demo
  Demo -->|One-time import after sign-in| SBDB
  Studio -->|Avatar Upload| SBStorage

  UI -->|POST /api/ai/*| Guard
  Guard -->|Verify JWT| SBAuth
  Guard -->|Check & increment ai_usage| SBDB
  Guard --> Analyzer
  Analyzer --> Retry
  Retry -->|HTTPS JSON Generation| Gemini
Loading

πŸ—„οΈ Database Schema (erDiagram)

All user-owned tables enforce user_id NOT NULL DEFAULT auth.uid() with ON DELETE CASCADE to auth.users(id) and Row-Level Security policies restricting SELECT, INSERT, UPDATE, and DELETE to auth.uid() = user_id.

erDiagram
  AUTH_USERS ||--o{ CAREER_TRACKS : owns
  AUTH_USERS ||--o{ RESUMES : owns
  AUTH_USERS ||--o{ RESUME_VERSIONS : owns
  AUTH_USERS ||--o{ SHARED_RESUMES : shares
  AUTH_USERS ||--o{ RESUME_THEME_CONFIGS : saves
  AUTH_USERS ||--o{ JOB_APPLICATIONS : tracks
  AUTH_USERS ||--o{ JD_ANALYSES : submits
  AUTH_USERS ||--o{ AI_RESUME_ANALYSIS : runs
  AUTH_USERS ||--o{ INTERVIEW_SESSIONS : practices
  AUTH_USERS ||--o{ COVER_LETTERS : drafts
  AUTH_USERS ||--o{ RESUME_REVIEWS : requests
  AUTH_USERS ||--o{ USER_SAVED_COMPANIES : bookmarks
  AUTH_USERS ||--o{ ANALYTICS_EVENTS : logs
  AUTH_USERS ||--o{ ATS_HISTORY : records
  AUTH_USERS ||--o{ USER_MILESTONES : achieves
  AUTH_USERS ||--o{ AI_USAGE : consumes

  CAREER_TRACKS ||--o{ RESUMES : groups
  RESUME_TEMPLATES ||--o{ RESUMES : styles
  RESUME_TEMPLATES ||--o{ RESUME_THEME_CONFIGS : configures
  RESUMES ||--o{ RESUME_VERSIONS : snapshots
  RESUMES ||--o{ SHARED_RESUMES : publishes
  RESUMES ||--o{ JOB_APPLICATIONS : links_to
  RESUMES ||--o{ COVER_LETTERS : backs
  RESUMES ||--o{ RESUME_REVIEWS : targets
  RESUMES ||--o{ ATS_HISTORY : tracks_score

  JOB_APPLICATIONS ||--o{ APPLICATION_TIMELINE : logs
  JOB_APPLICATIONS ||--o{ RECRUITER_CONTACTS : contacts
  JOB_APPLICATIONS ||--o{ INTERVIEW_SESSIONS : prepares_for

  JD_ANALYSES ||--o{ AI_RESUME_ANALYSIS : compares
  AI_RESUME_ANALYSIS ||--o{ AI_SUGGESTIONS : produces
  INTERVIEW_SESSIONS ||--o{ INTERVIEW_QUESTIONS : contains
  COVER_LETTERS ||--o{ COVER_LETTER_AI_SUGGESTIONS : refines
  COMPANIES ||--o{ USER_SAVED_COMPANIES : referenced_by

  CAREER_TRACKS {
    uuid id PK
    uuid user_id FK
    text name
    text role
    text status
    text color
    timestamptz created_at
    timestamptz updated_at
  }

  RESUME_TEMPLATES {
    text id PK
    text name
    text category
    boolean is_ats_friendly
    text thumbnail_url
  }

  RESUMES {
    uuid id PK
    uuid user_id FK
    uuid track_id FK
    text name
    integer ats_score
    text template_id FK
    jsonb theme_config
    jsonb content
    integer version
    timestamptz created_at
    timestamptz updated_at
  }

  RESUME_VERSIONS {
    uuid id PK
    uuid user_id FK
    uuid resume_id FK
    uuid parent_id FK
    text name
    text title
    integer ats_score
    jsonb content
    timestamptz created_at
    timestamptz updated_at
  }

  JOB_APPLICATIONS {
    uuid id PK
    uuid user_id FK
    text company_name
    text role_title
    text job_description
    uuid resume_id FK
    text status
    text location
    text salary_range
    text application_link
    timestamptz deadline
    text notes
    timestamptz created_at
    timestamptz updated_at
  }

  AI_USAGE {
    uuid user_id PK,FK
    text route PK
    timestamptz window_start PK
    integer count
  }
Loading

πŸ”„ Sequence Diagram: Tailor Resume to a Job Description

The sequence below shows the end-to-end execution path when a user clicks Tailor Resume in AISuggestionsPanel.tsx, including authentication, fixed-window rate limiting, input validation, exponential-backoff retries on upstream Gemini 429 errors, error handling, and persistence of the newly tailored resume.

sequenceDiagram
  autonumber
  actor User
  participant UI as AISuggestionsPanel (Browser)
  participant API as POST /api/ai/tailor-resume
  participant Guard as lib/api-guard.ts
  participant DB as Supabase (Auth + Postgres)
  participant Retry as callWithRetry (lib/ai-retry-cache.ts)
  participant Gemini as Google Gemini API

  User->>UI: Click "Tailor Resume"
  UI->>API: POST { resumeContent, jdText, company, role }

  API->>Guard: requireUser()
  Guard->>DB: supabase.auth.getUser()
  alt Unauthenticated / Expired Session
    DB-->>Guard: user = null
    Guard-->>API: 401 { error: { code: "UNAUTHORIZED", message } }
    API-->>UI: 401 Unauthorized
    UI-->>User: Display error banner
  else Authenticated
    DB-->>Guard: user (id)
    API->>Guard: enforceRateLimit(supabase, user.id, "tailor-resume")
    Guard->>DB: RPC increment_ai_usage(route, window_start)\n(INSERT ... ON CONFLICT DO UPDATE SET count = count + 1 RETURNING count)
    alt Over Rate Limit (returned count > 10 / 60s)
      Guard-->>API: 429 { error: { code: "RATE_LIMITED" } } + Header Retry-After
      API-->>UI: 429 Too Many Requests
      UI-->>User: Display rate-limit message
    else Within Rate Limit (returned count <= 10)
      API->>Guard: validateJsonSize(resumeContent) & validateStringLength(jdText)
      alt Payload Invalid or Exceeds Limit
        Guard-->>API: 400 BAD_REQUEST or 413 PAYLOAD_TOO_LARGE
        API-->>UI: 400 / 413 Error JSON
        UI-->>User: Display validation error
      else Payload Valid
        API->>Retry: callWithRetry(() => model.generateContent(prompt))
        loop Up to maxRetries (3) on 429 / Resource Exhausted
          Retry->>Gemini: generateContent(prompt) [25s timeout]
          alt Gemini returns 429 Rate Limit
            Gemini-->>Retry: 429 Too Many Requests
            Note over Retry: Exponential backoff (800ms * 2^attempt + jitter)
          else Gemini times out (>25s) or 5xx error
            Gemini-->>Retry: Error / ETIMEDOUT
            Retry-->>API: Throw error
            API->>Guard: handleAIError(error, "tailor resume")
            Guard-->>API: 502 / 503 (with Retry-After: 30) / 504
            API-->>UI: { error: { code, message } }
            UI-->>User: Display error banner (no fake fallback data)
          else Gemini succeeds
            Gemini-->>Retry: Strict JSON { tailoredContent, changesMade }
          end
        end
        Retry-->>API: Parsed tailoredResult
        API-->>UI: 200 OK { data: tailoredResult }
        UI->>UI: Merge tailoredContent with baseline resume fields
        UI->>DB: saveUserResume(newResume) -> INSERT INTO public.resumes
        DB-->>UI: Persisted Resume row (version = 1)
        UI-->>User: Render LivePreview + "Saved to Studio" badge
      end
    end
  end
Loading

βš–οΈ Design Decisions & Trade-offs

  1. Supabase Repository Layer + Explicit Logged-Out Demo Mode (lib/data/ & lib/user-data-store.ts)

    • Decision: All authenticated reads and writes go through typed Supabase repositories (lib/data/resumes.ts, lib/data/tracks.ts, lib/data/applications.ts). localStorage (velora_demo_*_v3) is strictly isolated to unauthenticated demo sessions, and migrateLocalStorageToSupabaseOnce performs a one-time import of legacy local items upon sign-in, remapping local track-* and res-* IDs to newly generated database UUIDs.
    • Trade-off: Maintaining a dual path in lib/user-data-store.ts adds a small amount of client-store coordination code, but allows visitors to test the Resume Studio and Kanban board before creating an account without polluting authenticated state.
  2. Optimistic Concurrency Control (resumes.version) over Real-Time CRDTs

    • Decision: Resume edits are debounced by 800ms (hooks/use-debounce.ts) and persisted via UPDATE resumes SET ... WHERE id = $1 AND version = $expectedVersion. A PostgreSQL BEFORE UPDATE trigger (trg_resumes_version_and_updated_at) automatically increments version. If 0 rows match because another tab saved a newer version, updateResume fetches the latest server row and throws VersionConflictError, pausing autosave and prompting the user to reload.
    • Trade-off: Simpler, stateless, and free of WebSocket connection overhead compared to Yjs/CRDTs, at the cost of whole-document conflict resolution if a single user edits the same resume simultaneously in two browser tabs.
  3. Atomic Postgres Fixed-Window Rate Limiting (public.increment_ai_usage) over External Redis

    • Decision: Per-user, per-route rate limiting (enforceRateLimit) calls the public.increment_ai_usage PostgreSQL RPC (INSERT INTO public.ai_usage ... ON CONFLICT (user_id, route, window_start) DO UPDATE SET count = public.ai_usage.count + 1 RETURNING count).
    • Trade-off: Eliminates race conditions under concurrent requests in a single atomic database round-trip without introducing a separate Redis/Upstash dependency, at the cost of one lightweight Postgres write per AI call.
  4. Template Foreign-Key Normalization (normalizeTemplateForDb)

    • Decision: The resume_templates table seeds three canonical IDs (velora-professional, modern-minimalist, dev-terminal), while the UI also supports creative-accent. lib/data/resumes.ts stores the exact UI template ID in theme_config.__templateId and maps unseeded template IDs to a valid FK value on resumes.template_id.
    • Trade-off: Preserves referential integrity against resume_templates(id) without requiring a schema migration every time a purely visual React template is added.

πŸ”’ Security

  • Row-Level Security (RLS) Everywhere:
    • Every user-owned table has RLS enabled with explicit owner-scoped policies (USING (auth.uid() = user_id) on reads/updates/deletes and WITH CHECK (auth.uid() = user_id) on inserts/updates).
    • Child tables without a direct user_id column (ai_suggestions, application_timeline, recruiter_contacts, interview_questions, cover_letter_ai_suggestions) enforce ownership via EXISTS (...) subqueries against their parent table's user_id.
    • Automated multi-tenant isolation test suite in supabase/tests/rls.sql verifies inside a rolled-back transaction that User A cannot read, update, delete, or attach child rows to User B's data.
  • Server-Side Authentication Guard (requireUser):
    • Every /api/ai/* endpoint verifies the caller's session via supabase.auth.getUser() before executing any logic. Unauthenticated requests immediately receive 401 UNAUTHORIZED.
  • Per-User Rate Limiting (enforceRateLimit):
    • Limits each authenticated user to 10 requests per 60-second window per AI route (configurable via RateLimitOptions). Exceeding the limit returns 429 RATE_LIMITED with a standard Retry-After header.
  • Input Size Caps & Validation:
    • validateStringLength and validateJsonSize cap short strings at 500 chars, long text (JDs/notes) at 15,000 chars, serialized JSON at 100,000 chars, and base64 uploads at 7,000,000 chars (~5 MB), returning 400 or 413 before hitting Gemini.
  • Resilient Upstream AI Calls (callWithRetry & handleAIError):
    • callWithRetry enforces a 25-second timeout per Gemini attempt (504 AI_TIMEOUT) and retries 429 / quota errors with exponential backoff and jitter, mapping persistent upstream rate limits to 503 UPSTREAM_RATE_LIMITED (Retry-After: 30) and other failures to 502 AI_GENERATION_FAILEDβ€”never returning fabricated success payloads.

⚠️ Known Limitations & What I'd Change at 100x Scale

Current Limitations

  1. Fixed-Window Boundary Bursts: enforceRateLimit uses an atomic 60-second fixed window (increment_ai_usage). A user can send 10 requests right before a minute boundary and 10 more immediately after the boundary.
  2. Synchronous AI Route Execution: /api/ai/* routes await full JSON responses from Gemini before returning, holding serverless function invocations open for 2–15 seconds.
  3. Client-Side PDF Rendering: PDF export in the Resume Studio and Cover Letters uses html2pdf.js (html2canvas + jsPDF) in the browser, which rasterizes DOM nodes rather than producing selectable vector text or compiling LaTeX server-side.
  4. Full-Document JSONB Resume Updates: Autosave writes the full resumes.content JSONB document on each debounced save rather than section-level diffs.

What Would Change at 100x Scale

  1. Edge Sliding-Window Rate Limiting: Move rate limiting to an edge Redis/Upstash sliding-window limiter in middleware.ts to smooth boundary bursts and shed excess traffic before invoking serverless functions or Postgres.
  2. Streaming & Background Queue for AI Workloads: Switch long-running Gemini prompts (resume tailoring, deep resume review, company insights) to streaming responses (generateContentStream) or enqueue jobs via a worker queue (e.g., BullMQ / Inngest / Trigger.dev) with Supabase Realtime progress notifications.
  3. Server-Side Vector PDF & TeX Compilation: Offload PDF generation to an isolated worker running headless Chromium (Playwright) for HTML templates and a sandboxed tectonic / pdflatex container for the LaTeX editor so exported PDFs contain selectable text for ATS parsers.
  4. Section-Level Diffs & Cursor Pagination: Split large resumes.content JSONB updates into section-level patches and add keyset pagination ((updated_at, id)) on listResumes and listApplications.

πŸ› οΈ Technology Stack


πŸš€ Getting Started

Prerequisites

  • Node.js: v20+ recommended (v18.17.0+)
  • npm
  • A Supabase project and a Google Gemini API key

1. Clone the Repository

git clone https://github.com/Slash-495/Velora.git
cd Velora

2. Install Dependencies

npm install

3. Configure Environment Variables

Create a .env.local file in the project root:

# Supabase Configuration
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key

# Google Gemini AI API Key
GEMINI_API_KEY=your-gemini-api-key

# Site URL (Optional β€” used only for server-side links like email confirmation; browser OAuth uses window.location.origin)
NEXT_PUBLIC_SITE_URL=http://localhost:3000

4. Apply Database Migrations & Verify RLS

Link the Supabase CLI to your development project and push all migrations in supabase/migrations/:

npx supabase link --project-ref <your-dev-project-ref>
npx supabase db push

To run the multi-user RLS verification test script against your development database:

psql "<your-dev-database-connection-string>" -v ON_ERROR_STOP=1 -f supabase/tests/rls.sql

5. Run the Development Server

npm run dev

Open http://localhost:3000 in your browser.


πŸ“œ Available Scripts

  • npm run dev β€” Launch the Next.js development server on http://localhost:3000.
  • npm run build β€” Run production lint, TypeScript checks, and build the Next.js bundle.
  • npm run start β€” Start the Next.js production server.
  • npm run lint β€” Run ESLint checks.
  • npm run typecheck β€” Run TypeScript compiler checks (tsc --noEmit).
  • npm test β€” Run the Vitest unit test suite (vitest run).
  • npm run test:watch β€” Run Vitest in interactive watch mode.

πŸ“„ License

This project is open-source and available under the MIT License.

About

Velora is an all-in-one AI career workspace and resume studio designed to accelerate your job search with ATS scoring, mock interviews, and application tracking.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages