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
Place screenshots of the running workspace in
public/screenshots/(dashboard.png,resume-studio.png,ai-tailoring.png,kanban-board.png) or a walkthrough recording atpublic/screenshots/demo.gif.
| Dashboard & Career Heatmap | Resume Studio (Visual + LaTeX) |
|---|---|
![]() |
![]() |
| Overview metrics, career tracks, and 18-month activity grid | 800ms debounced autosave, live A4 preview, and optimistic conflict detection |
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
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
}
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
-
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, andmigrateLocalStorageToSupabaseOnceperforms a one-time import of legacy local items upon sign-in, remapping localtrack-*andres-*IDs to newly generated database UUIDs. - Trade-off: Maintaining a dual path in
lib/user-data-store.tsadds 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.
- Decision: All authenticated reads and writes go through typed Supabase repositories (
-
Optimistic Concurrency Control (
resumes.version) over Real-Time CRDTs- Decision: Resume edits are debounced by 800ms (
hooks/use-debounce.ts) and persisted viaUPDATE resumes SET ... WHERE id = $1 AND version = $expectedVersion. A PostgreSQLBEFORE UPDATEtrigger (trg_resumes_version_and_updated_at) automatically incrementsversion. If 0 rows match because another tab saved a newer version,updateResumefetches the latest server row and throwsVersionConflictError, 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.
- Decision: Resume edits are debounced by 800ms (
-
Atomic Postgres Fixed-Window Rate Limiting (
public.increment_ai_usage) over External Redis- Decision: Per-user, per-route rate limiting (
enforceRateLimit) calls thepublic.increment_ai_usagePostgreSQL 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.
- Decision: Per-user, per-route rate limiting (
-
Template Foreign-Key Normalization (
normalizeTemplateForDb)- Decision: The
resume_templatestable seeds three canonical IDs (velora-professional,modern-minimalist,dev-terminal), while the UI also supportscreative-accent.lib/data/resumes.tsstores the exact UI template ID intheme_config.__templateIdand maps unseeded template IDs to a valid FK value onresumes.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.
- Decision: The
- 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 andWITH CHECK (auth.uid() = user_id)on inserts/updates). - Child tables without a direct
user_idcolumn (ai_suggestions,application_timeline,recruiter_contacts,interview_questions,cover_letter_ai_suggestions) enforce ownership viaEXISTS (...)subqueries against their parent table'suser_id. - Automated multi-tenant isolation test suite in
supabase/tests/rls.sqlverifies inside a rolled-back transaction that User A cannot read, update, delete, or attach child rows to User B's data.
- Every user-owned table has RLS enabled with explicit owner-scoped policies (
- Server-Side Authentication Guard (
requireUser):- Every
/api/ai/*endpoint verifies the caller's session viasupabase.auth.getUser()before executing any logic. Unauthenticated requests immediately receive401 UNAUTHORIZED.
- Every
- Per-User Rate Limiting (
enforceRateLimit):- Limits each authenticated user to
10requests per60-second window per AI route (configurable viaRateLimitOptions). Exceeding the limit returns429 RATE_LIMITEDwith a standardRetry-Afterheader.
- Limits each authenticated user to
- Input Size Caps & Validation:
validateStringLengthandvalidateJsonSizecap short strings at500chars, long text (JDs/notes) at15,000chars, serialized JSON at100,000chars, and base64 uploads at7,000,000chars (~5 MB), returning400or413before hitting Gemini.
- Resilient Upstream AI Calls (
callWithRetry&handleAIError):callWithRetryenforces a 25-second timeout per Gemini attempt (504 AI_TIMEOUT) and retries429/ quota errors with exponential backoff and jitter, mapping persistent upstream rate limits to503 UPSTREAM_RATE_LIMITED(Retry-After: 30) and other failures to502 AI_GENERATION_FAILEDβnever returning fabricated success payloads.
- Fixed-Window Boundary Bursts:
enforceRateLimituses 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. - Synchronous AI Route Execution:
/api/ai/*routes await full JSON responses from Gemini before returning, holding serverless function invocations open for 2β15 seconds. - 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. - Full-Document JSONB Resume Updates: Autosave writes the full
resumes.contentJSONB document on each debounced save rather than section-level diffs.
- Edge Sliding-Window Rate Limiting: Move rate limiting to an edge Redis/Upstash sliding-window limiter in
middleware.tsto smooth boundary bursts and shed excess traffic before invoking serverless functions or Postgres. - 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. - Server-Side Vector PDF & TeX Compilation: Offload PDF generation to an isolated worker running headless Chromium (Playwright) for HTML templates and a sandboxed
tectonic/pdflatexcontainer for the LaTeX editor so exported PDFs contain selectable text for ATS parsers. - Section-Level Diffs & Cursor Pagination: Split large
resumes.contentJSONB updates into section-level patches and add keyset pagination ((updated_at, id)) onlistResumesandlistApplications.
- Framework: Next.js 15 (App Router, Server Actions, Route Handlers)
- Language & UI: React 19, TypeScript 5, TailwindCSS 3, Framer Motion 12, Lucide React
- Code Editor:
@monaco-editor/react(LaTeX source mode) - AI SDK:
@google/generative-ai(gemini-flash-latest) - Database & Auth: Supabase (
@supabase/supabase-js,@supabase/ssr) - Testing & CI: Vitest & GitHub Actions (
.github/workflows/ci.yml)
- Node.js: v20+ recommended (v18.17.0+)
- npm
- A Supabase project and a Google Gemini API key
git clone https://github.com/Slash-495/Velora.git
cd Veloranpm installCreate 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:3000Link 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 pushTo 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.sqlnpm run devOpen http://localhost:3000 in your browser.
npm run devβ Launch the Next.js development server onhttp://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.
This project is open-source and available under the MIT License.



