MERN foundation for an AI-powered developer productivity platform. Part 1 laid
down authentication, protected routes, and a dashboard shell. Part 2 added
GitHub OAuth so a user can connect their account and browse repositories.
Part 3 completes the core loop: fetch a connected repository's source files
from GitHub, send them to Gemini (gemini-2.5-flash) for review, and show
the results — score, severity breakdown, and per-issue recommendations — on
an analysis dashboard.
- Frontend: React 18 + Vite + Tailwind CSS + React Router
- Backend: Node.js + Express + Axios
- Database: MongoDB + Mongoose
- Auth: JWT (Bearer header or httpOnly cookie) + bcrypt password hashing
- Integrations: GitHub OAuth (Part 2), Gemini
gemini-2.5-flashcode analysis (Part 3)
devplatform/
├── server/
│ ├── src/
│ │ ├── config/ # env loader, MongoDB connection
│ │ ├── controllers/ # authController.js, githubController.js, analysisController.js
│ │ ├── middleware/ # auth (protect/authorize), centralized error handler
│ │ ├── models/ # User.js, Repository.js, Analysis.js
│ │ ├── routes/ # authRoutes.js, githubRoutes.js, analysisRoutes.js, index.js
│ │ ├── services/ # authService.js, githubService.js (OAuth + GitHub API +
│ │ │ # file fetching), geminiService.js, analysisService.js
│ │ ├── utils/ # ApiError, JWT helpers, fileFilters.js (ignore rules)
│ │ └── app.js # Express app setup
│ ├── server.js # entry point
│ ├── package.json
│ └── .env.example
└── client/
├── src/
│ ├── components/ # Sidebar, Navbar, StatCard, FormInput, PlaceholderPage,
│ │ # GithubConnectionCard, RepositoryCard, ScoreGauge,
│ │ # SeveritySummary, IssueList
│ ├── pages/ # Login, Register, Dashboard, Repositories, AnalysisResult
│ ├── layouts/ # AuthLayout, DashboardLayout
│ ├── services/ # api.js (axios), authService.js, githubService.js,
│ │ # analysisService.js
│ ├── hooks/ # useAuth, useGithubConnection
│ ├── context/ # AuthContext (global auth state)
│ └── routes/ # ProtectedRoute
├── package.json
└── .env.example
server/.env (copy from server/.env.example):
| Variable | Description |
|---|---|
NODE_ENV |
development / production |
PORT |
API port (default 5000) |
CLIENT_URL |
Frontend origin, for CORS (default http://localhost:5173) |
MONGO_URI |
MongoDB connection string |
JWT_SECRET |
Long random secret used to sign JWTs |
JWT_EXPIRES_IN |
Token lifetime (default 7d) |
JWT_COOKIE_EXPIRES_DAYS |
httpOnly cookie lifetime in days (default 7) |
GITHUB_CLIENT_ID |
OAuth App client ID from GitHub |
GITHUB_CLIENT_SECRET |
OAuth App client secret from GitHub — server only, never sent to the frontend |
GITHUB_CALLBACK_URL |
Must exactly match the "Authorization callback URL" configured on the GitHub OAuth App (default http://localhost:5000/api/github/callback) |
GEMINI_API_KEY |
API key for the Gemini API (Google AI Studio) — server only, never sent to the frontend |
GEMINI_MODEL |
Model used for code analysis (default gemini-2.5-flash) |
client/.env (copy from client/.env.example):
| Variable | Description |
|---|---|
VITE_API_BASE_URL |
Backend API base URL (default http://localhost:5000/api) |
cd server
cp .env.example .env # then fill in MONGO_URI and JWT_SECRET
npm install
npm run dev # nodemon, http://localhost:5000You'll need a MongoDB instance — either local (mongod) or a connection string
from MongoDB Atlas — set as MONGO_URI.
cd client
cp .env.example .env
npm install
npm run dev # http://localhost:5173Open http://localhost:5173, register an account, and you'll land on the
protected dashboard. Logging out or visiting /dashboard while unauthenticated
redirects to /login.
- On GitHub: Settings → Developer settings → OAuth Apps → New OAuth App.
- Fill in:
- Application name: anything, e.g.
DevPlatform (local) - Homepage URL:
http://localhost:5173 - Authorization callback URL:
http://localhost:5000/api/github/callback(must matchGITHUB_CALLBACK_URLinserver/.envexactly)
- Application name: anything, e.g.
- Register the app, then copy the Client ID and generate a Client secret.
- Paste both into
server/.envasGITHUB_CLIENT_IDandGITHUB_CLIENT_SECRET. - Restart the backend so the new env vars are picked up.
User clicks "Connect GitHub" (browser navigation, not fetch/axios)
→ GET /api/github/connect [protect: reads the httpOnly cookie]
→ 302 redirect to GitHub's authorize screen, with a signed `state` param
→ user approves on GitHub
→ GitHub redirects to GET /api/github/callback?code=...&state=...
→ state is verified (binds this callback to the user who started the flow)
→ code is exchanged server-side for a GitHub access token (client secret
used only here, server-to-server)
→ GitHub profile is fetched and saved onto the User document
→ 302 redirect back to the frontend: /dashboard?github=connected
The /connect step is a real browser navigation (window.location.href = ".../github/connect"), not an axios call — that's required for the OAuth
redirect chain to work, and it's why that route is authenticated via the
httpOnly cookie set at login rather than the Bearer token kept in
localStorage.
GITHUB_CLIENT_ID=your_oauth_app_client_id
GITHUB_CLIENT_SECRET=your_oauth_app_client_secret
GITHUB_CALLBACK_URL=http://localhost:5000/api/github/callback
- Get an API key from Google AI Studio.
- Paste it into
server/.envasGEMINI_API_KEY.GEMINI_MODELalready defaults togemini-2.5-flash, so you only need to set it if you want a different model. - Restart the backend.
User clicks "Analyze Repository" on a repo card
→ (if not already connected) POST /api/github/repositories/:githubId/connect
→ POST /api/analysis/:repositoryId/run [protect]
1. Verify the repository belongs to the requesting user
2. Fetch the repo's file tree from GitHub (git/trees, recursive)
3. Filter out node_modules, .git, dist/build, .env, lockfiles, binaries,
and anything outside a source-code extension allowlist
4. Fetch blob content for the remaining files, capped at 40 files /
40KB per file / 150K combined characters
5. Send the files to Gemini (gemini-2.5-flash) with a JSON response
schema (severity, category, file, line, description, recommendation,
suggestedFix) so the output is always structured, valid JSON
6. Compute an overall 0-100 score and severity counts from the issues
7. Save an Analysis document in MongoDB, linked to the Repository
→ frontend redirects to /dashboard/repositories/:repositoryId/analysis
and renders the score, severity breakdown, and issue details
GEMINI_API_KEY=your_gemini_api_key
GEMINI_MODEL=gemini-2.5-flash
| Method | Route | Access | Description |
|---|---|---|---|
| POST | /api/auth/register |
Public | Create an account, returns user + JWT |
| POST | /api/auth/login |
Public | Authenticate, returns user + JWT |
| POST | /api/auth/logout |
Private | Clears the auth cookie |
| GET | /api/auth/me |
Private | Returns the current authenticated user |
| GET | /api/github/connect |
Private | Redirects to GitHub's OAuth consent screen |
| GET | /api/github/callback |
Public* | GitHub's redirect target; exchanges code, saves connection |
| GET | /api/github/profile |
Private | Connection status + public GitHub profile info |
| GET | /api/github/repositories |
Private | Live list of the user's GitHub repos, including analysis status |
| POST | /api/github/repositories/:githubId/connect |
Private | Persists a repo for future analysis |
| DELETE | /api/github/disconnect |
Private | Removes the stored GitHub connection |
| POST | /api/analysis/:repositoryId/run |
Private | Runs the full GitHub → Gemini → MongoDB pipeline for a connected repo |
| GET | /api/analysis/:repositoryId/latest |
Private | Most recent analysis for a repository (full issue detail) |
| GET | /api/analysis/:repositoryId/history |
Private | Past analyses for a repository (score/summary only, no issue detail) |
| GET | /api/analysis/result/:analysisId |
Private | A single analysis by id (full issue detail) |
| GET | /api/health |
Public | Health check |
* /callback has no protect middleware (GitHub's redirect can't carry your
app's cookie/token), but it can't be used to act on an arbitrary account — the
signed, short-lived state param is the only thing that ties it to a user.
Private routes require a Bearer token (Authorization: Bearer <token>) or the
token httpOnly cookie set at login/register. Every /api/analysis and
/api/github route additionally checks that the resource (repository/
analysis) belongs to req.user.id — one user can never trigger or read
another user's analyses.
Part 1 (still passing): register → duplicate-email rejection → login → wrong-password rejection → protected route blocked without a token → protected route succeeds with a token, no password leaked → logout → 404 handling.
Part 2 (still passing): ran 13 functional tests against the real Express
app, routes, controllers, and middleware — OAuth state correctly binds the
flow to the initiating user, tampered state is rejected, successful callback
saves the connection, the GitHub access token never appears in any API
response, repository listing/connect/disconnect all work and are scoped to
the requesting user.
Part 3: GitHub's and Gemini's APIs aren't reachable from this sandbox, so
outbound calls at those two service boundaries were stubbed; everything else
— routes, controllers, analysisService's orchestration logic, the
Analysis/Repository Mongoose models, and the file-filtering rules — ran
as real code:
- 22 unit tests on
fileFilters.isAnalyzablePath(node_modules, .git, dist/build, .env variants, lockfiles, binaries, and unknown extensions are all correctly excluded; real source files across JS/TS/Python/Go/etc. are correctly included) - A Mongoose schema-validation check confirming the
Analysismodel rejects invalidseverity/categoryenum values and accepts a well-formed document - 10 end-to-end pipeline tests: unauthenticated run rejected (401); a full
successful run correctly computes
overallScoreand severity counts from the issues Gemini returned, persists anAnalysisdocument, and updatesRepository.lastAnalysis/lastAnalyzedAt;/latest,/history(issue-free), and/result/:id(full detail) all return the right shape; a second user is correctly blocked (404) from another user's repository/analysis; a no-analyzable-files result and a Gemini failure both persist astatus: 'failed'Analysisrecord and surface the right error code (422 / 429) instead of crashing - A full-stack regression chaining Part 1 → Part 2 → Part 3 together in one
run (register → login → connect GitHub → list repos → connect a repo → run
analysis → verify the repository listing now reflects
hasAnalysisand the newrepositoryIdfield) to confirm nothing in the earlier parts broke - Frontend: clean
vite buildand avite previewsmoke test after each change
Part 2:
- The GitHub client secret never leaves
server/.env— used only in the server-to-server token exchange call. - The GitHub access token is stored with
select: falseand stripped again intoJSONas defense in depth — it never appears in any API response. - Every GitHub-data route requires
protect, and lookups are scoped toreq.user.id. - GitHub API errors map to proper status codes, including a dedicated 429
for rate limits (via
X-RateLimit-Remaining: 0). - Only repository metadata is persisted — never repository contents/code.
Part 3:
- The Gemini API key never leaves
server/.env/ thegeminiServicemodule — it's not returned in any response and isn't exposed to the client. - Every
/api/analysisroute requiresprotect, andanalysisServicere-verifies repository ownership (Repository.findOne({ _id, user })) before touching GitHub or Gemini on the user's behalf — one user cannot trigger analysis on, or read the results of, another user's repository. - Source fetching is capped (40 files / 40KB per file / 150K total characters) so a large or malicious repository can't blow up token usage or request time.
.env, lockfiles, and binaries are excluded before anything is ever read from GitHub — secrets in a.envfile are never sent to Gemini.- The Gemini system instruction explicitly tells the model to treat file contents as untrusted data, not instructions, to reduce prompt-injection risk from adversarial repository content.
- Gemini's output is constrained by a JSON
responseSchemaand then re-validated/normalized server-side (severity and category are checked against fixed enums,filemust be one of the paths actually sent, and string fields are length-capped) before anything reaches MongoDB. - Gemini and GitHub API failures are caught, translated into proper HTTP
status codes (429 for rate limits, 422 for "no analyzable files", 502 for
upstream failures), and persisted as a
status: 'failed'Analysisrecord rather than left unrecorded or crashing the request.
- Redis, BullMQ, or any background job queue — analysis runs synchronously within the request (fine for the file/size caps in place, but a real queue would be the next step for larger repositories)
- Docker
- Pull request review
- A cross-repository "Issues" aggregation view and a repository-independent
"Analyses" history page (both still placeholders in the dashboard nav —
per-repository history is available via
GET /api/analysis/:repositoryId/historyand the "View last analysis" link on each repository card)
These will be added in later parts without needing to restructure what's here.
This repo includes a root-level vercel.json and api/index.js so the whole
app deploys as one Vercel project: the React app builds as static output,
and the Express API runs as a single serverless function. Nothing in
server/src was changed to make this work — api/index.js just wraps the
existing app.
vercel.json(repo root) — buildsclient, servesclient/distas static output, and routes/api/*to the serverless function; everything else falls back toindex.htmlfor React Router.api/index.js(repo root) — the actual Vercel Function. It imports the unmodifiedserver/src/app.jsand calls it per-request.server/src/config/db.js— updated to cache the MongoDB connection promise at module scope instead of opening a new connection every call, and tothrowon failure instead ofprocess.exit(1)(a serverless function should never callprocess.exit).server/server.js(used for localnpm run dev) now does its owntry/catcharoundconnectDB()and exits itself if the DB is unreachable, so local behavior is unchanged.
Vercel Functions don't have a fixed IP, so in Atlas: Network Access → Add IP Address → Allow Access from Anywhere (0.0.0.0/0). (If you need tighter access control, use Atlas's Vercel integration instead of a raw IP allowlist.)
Vercel → Add New → Project → import the repo. Leave the Root
Directory as the repo root (not client or server) — vercel.json at
the root handles both halves.
NODE_ENV=production
MONGO_URI=your_atlas_connection_string
JWT_SECRET=a_long_random_secret
JWT_EXPIRES_IN=7d
JWT_COOKIE_EXPIRES_DAYS=7
# Set these to your actual Vercel URL once you have it (redeploy after setting)
CLIENT_URL=https://your-app.vercel.app
GITHUB_CALLBACK_URL=https://your-app.vercel.app/api/github/callback
GITHUB_CLIENT_ID=your_oauth_app_client_id
GITHUB_CLIENT_SECRET=your_oauth_app_client_secret
GEMINI_API_KEY=your_gemini_api_key
GEMINI_MODEL=gemini-2.5-flash
# Frontend build-time var - relative path works since both live on one domain
VITE_API_BASE_URL=/api
In GitHub → Settings → Developer settings → OAuth Apps → your app, set
Authorization callback URL to
https://your-app.vercel.app/api/github/callback (must exactly match
GITHUB_CALLBACK_URL above). You can keep a second, separate OAuth App for
local development if you don't want to keep flipping this back and forth.
The first deploy will get you a *.vercel.app URL. Put that exact URL into
CLIENT_URL / GITHUB_CALLBACK_URL / the GitHub OAuth App / VITE_API_BASE_URL
(if you prefer the absolute form) and redeploy so those env vars take effect
— environment variables are baked in at build/deploy time, not read live.
- Function duration:
vercel.jsonsetsmaxDuration: 60for the API function. Gemini analysis on a repo near the file cap can take a while; if you seeFUNCTION_INVOCATION_TIMEOUT(504) on/api/analysis/:id/run, raisemaxDuration(Hobby plans support up to 300s with Fluid Compute enabled, Pro up to 800s) or lowerMAX_FILESingithubService.js. - Cold starts: the first request after idle time will be slower (new
MongoDB connection, GitHub/Gemini clients initialized). Subsequent requests
on a warm function reuse the cached connection from the
db.jschange above. - Cookies now work same-origin: since the frontend and API share one
domain in this setup, the GitHub OAuth flow's httpOnly cookie no longer
crosses origins the way it would with the frontend and backend on separate
domains — no extra CORS/cookie configuration needed beyond what's already
in
app.js. - Local development is unaffected —
npm run devinserver/andclient/still works exactly as in Parts 1-3; the Vercel deployment path (api/index.js) is a separate, additive entry point.