Personal portfolio platform — public-facing site with an admin dashboard for managing all content live, without touching the codebase.
Live at achille.tech API on Render · Database on MongoDB Atlas · Infrastructure provisioned with Terraform
Browser
│
├── achille.tech (Vercel CDN)
│ React 19 + TypeScript + Vite + Tailwind CSS
│ └── /api/* proxied to Render
│
└── resume-platform-api.onrender.com (Render Web Service)
Node.js + Express + TypeScript
└── MongoDB Atlas M0 (AWS us-east-1)
provisioned via Terraform (mongodbatlas provider)
CI/CD flow on every push to main:
push → GitHub Actions
├── server (TypeScript build + smoke tests) ─┐
├── client (ESLint + Vite build) ├─ parallel
└── audit (npm audit --audit-level=high) ─┘
│
└── deploy (only if all three pass)
├── POST Render deploy hook
└── poll GET /api/health until 200
(Vercel deploys automatically on same push)
- React 19 with TypeScript and strict mode
- Vite for dev server and production builds
- Tailwind CSS v4 for styling
- React Router for client-side routing
- Vite proxy forwards
/apito the local Express server during development — no env var needed locally
- Node.js with Express and TypeScript
- MongoDB via Mongoose for data persistence
- JWT authentication with httpOnly cookie sessions
- TOTP MFA using
otplib— admin login requires a 6-digit code from Google Authenticator - Zod for request validation
- Nodemailer for contact form email delivery (Gmail SMTP)
- Terraform with the official
mongodbatlasprovider (v2.8.0) - Provisions the Atlas project and an M0 free-tier replica set cluster on AWS us-east-1
- Service account authentication (client ID + secret) — no personal API keys
- State files and
.tfvarsare gitignored; keep them local or use remote state (S3, Terraform Cloud) - IP allowlist managed separately through the Atlas CLI — not tracked in Terraform
- GitHub Actions on every push and pull request
- Four jobs:
server,client,auditrun in parallel —deploygates on all three - Deploy triggers a Render webhook then health-checks the API before marking success
- Vercel picks up the same push automatically via its GitHub integration
- Node.js 24 throughout (
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true)
- Admin session uses an httpOnly, SameSite=Strict cookie — no tokens in localStorage
- TOTP MFA with recovery codes (hashed with bcrypt, stored as env vars)
- CORS locked to
CLIENT_ORIGIN— only the production Vercel URL is accepted npm auditruns on every push and blocks deploy on high or critical findings
Install from the repo root:
npm installStart the API (port 4000):
cd server
npm run devStart the client (port 5173) in a separate terminal:
cd client
npm run devThe Vite proxy forwards /api requests to http://localhost:4000, so VITE_API_BASE_URL does not need to be set locally.
cp server/.env.example server/.envPORT=4000
CLIENT_ORIGIN=http://localhost:5173
MONGODB_URI=mongodb+srv://...
ADMIN_EMAIL=you@example.com
ADMIN_PASSWORD=strong-password
JWT_SECRET=long-random-string
JWT_EXPIRES_IN=12h
ADMIN_MFA_SECRET=
ADMIN_MFA_RECOVERY_CODE_HASHES=
ADMIN_MFA_ISSUER=achille.tech Admin
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your@gmail.com
SMTP_PASS=your-app-password
GMAIL_CLIENT_ID=
GMAIL_CLIENT_SECRET=
GMAIL_REFRESH_TOKEN=Contact-form notifications are sent to SMTP_USER. Use SMTP_PASS for the
SMTP path, or use the Gmail API variables below for hosts that block SMTP.
Render Free blocks outbound SMTP ports, so production can use the Gmail API
instead. Set SMTP_USER to t.achille.tech@gmail.com, then set
GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, and GMAIL_REFRESH_TOKEN from a
Google OAuth client with the https://www.googleapis.com/auth/gmail.send scope.
When those three Gmail API variables are present, the API sends notifications
over HTTPS instead of SMTP.
If the contact form reports SMTP notification failed, one of the Gmail API
variables is missing and the server fell back to SMTP. If it reports Gmail API notification failed, check the Render logs for the OAuth or Gmail API error.
For production, set VITE_API_BASE_URL in Vercel environment variables:
VITE_API_BASE_URL=https://resume-platform-api.onrender.comRun once before the first production deploy:
npm --workspace server run admin:mfa:setupThis prints the ADMIN_MFA_SECRET, ADMIN_MFA_RECOVERY_CODE_HASHES, and an otpauth:// URI. Scan the URI in Google Authenticator (or Authy, 1Password, etc.). Store recovery codes offline — never in the repo.
To print the current 6-digit code during local testing:
npm --workspace server run admin:mfa:codeFrom the repo root:
npm run dev:client # client dev server
npm run dev:server # API dev server
npm run build:client # Vite production build
npm run build:server # TypeScript compile
npm run lint:client # ESLint
npm run test:server # build + smoke tests
npm run ci # full check (server + client + audit)From server/:
npm run admin:login # print a JWT for API testing (curl, Postman)
npm run admin:mfa:setup # generate MFA secret + recovery codes
npm run admin:mfa:code # print current TOTP code
npm run content:reset # reset DB to seed dataSmoke tests cover the critical server paths without requiring a live database:
GET /api/health- Admin login — success, wrong password, missing MFA
- Session check after login
- Admin route guard (401 without valid session)
- Public testimonial submission with MongoDB unavailable
cd server
npm run testThe infra/ directory manages the MongoDB Atlas project and cluster. Requires Terraform CLI and a MongoDB Atlas service account.
cd infra
cp terraform.tfvars.example terraform.tfvars
# fill in atlas_org_id, MONGODB_ATLAS_CLIENT_ID, MONGODB_ATLAS_CLIENT_SECRET
terraform init
terraform plan
terraform applyTo add your local IP to the Atlas allowlist temporarily:
atlas accessLists create --currentIp --projectId YOUR_PROJECT_IDThe cluster has prevent_destroy = true on the Terraform lifecycle to guard against accidental teardown.
| Secret | Where to get it |
|---|---|
RENDER_DEPLOY_HOOK |
Render → service → Settings → Deploy Hook |
RENDER_SERVICE_URL |
Your Render service URL, e.g. https://resume-platform-api.onrender.com |
- Root Directory:
server - Build Command:
npm ci && npm run build - Start Command:
npm run start - Health Check Path:
/api/health
Set all server env vars. Set CLIENT_ORIGIN to the exact production URL — the admin cookie will be rejected otherwise.
- Root Directory:
client - Framework Preset: Vite
- Build Command:
npm run build - Output Directory:
dist - Environment variable:
VITE_API_BASE_URL→ Render API URL
- Deploy Render, confirm
GET /api/healthreturns{"status":"ok"} - Deploy Vercel, copy the production URL
- Set
CLIENT_ORIGINin Render env vars to the Vercel URL - Redeploy Render
- Run
admin:mfa:setup, setADMIN_MFA_SECRETandADMIN_MFA_RECOVERY_CODE_HASHESin Render - Redeploy Render, test admin login at
/admin
- Vercel — Deployments tab → redeploy any previous build instantly
- Render — roll back to a previous deploy or push a revert commit
- MFA loss — use a recovery code; if all are gone, rerun
admin:mfa:setup, update the two MFA env vars in Render, redeploy
The dashboard is at /admin. Session is managed with a secure httpOnly cookie — nothing is stored in localStorage.
For API testing outside the browser:
cd server
npm run admin:login
# use the printed JWT as: Authorization: Bearer <token>MIT License
Copyright (c) 2025 Achille Traore
Permission is hereby granted, free of charge, to any person obtaining a copy...