A full-stack relationship graph app for CS 144. The client is a Vite React SPA, the API is Express, authentication is Firebase Auth, user data is stored in Firestore, and AI/network-chat calls run through the backend.
- Node.js 20+
- npm
- Docker Desktop, for container testing
- Firebase project with Email/Password Authentication enabled
- Firebase/GCP service-account JSON for Firebase Admin
- Gemini API key from Google AI Studio, for AI chat/follow-up features
team7/
├── client/ # Vite React frontend
│ ├── src/
│ ├── public/
│ ├── .env.example # browser-safe Firebase/Vite values
│ └── package.json
├── server/ # Express backend
│ ├── lib/
│ ├── routes/
│ ├── .env.example # backend secrets and runtime config
│ └── package.json
├── k8s/ # Kubernetes manifests
├── Dockerfile
├── .dockerignore
├── .env.example # env file index
└── package.json # root scripts
Real env files are ignored by git. Create them once:
cp client/.env.example client/.env
cp server/.env.example server/.envPut only browser-safe Firebase Web App values here. Find these in Firebase Console -> Project settings -> General -> Your apps -> Web app.
VITE_FIREBASE_API_KEY=...
VITE_FIREBASE_AUTH_DOMAIN=...
VITE_FIREBASE_PROJECT_ID=...
VITE_FIREBASE_STORAGE_BUCKET=...
VITE_FIREBASE_MESSAGING_SENDER_ID=...
VITE_FIREBASE_APP_ID=...
VITE_FIREBASE_MEASUREMENT_ID=...
VITE_API_BASE_URL=http://localhost:3001Do not put Firebase Admin credentials, service-account JSON, private keys, or GEMINI_API_KEY in client/.env. Vite exposes VITE_* values to browser JavaScript.
Put backend-only config and secrets here.
NODE_ENV=development
PORT=3001
CLIENT_ORIGIN=http://localhost:5173
ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173,http://localhost:3000,http://127.0.0.1:3000
FIREBASE_PROJECT_ID=...
FIREBASE_CLIENT_EMAIL=...
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
GEMINI_API_KEY=...
GEMINI_MODELS=gemini-3.5-flash,gemini-3-flash-preview,gemini-3.1-flash-lite,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-3.1-pro-preview,gemini-2.5-pro
GEMINI_API_VERSION=v1beta
GEMINI_TIMEOUT_MS=12000
GEMINI_MAX_OUTPUT_TOKENS=1024Map Firebase service-account JSON fields like this:
project_id->FIREBASE_PROJECT_IDclient_email->FIREBASE_CLIENT_EMAILprivate_key->FIREBASE_PRIVATE_KEY
Keep FIREBASE_PRIVATE_KEY as one quoted line with escaped \n characters. GEMINI_API_KEY belongs only in server/.env. GEMINI_MODELS is tried in order when Google's API reports temporary model overload, an empty candidate, or a truncated candidate. GEMINI_MAX_OUTPUT_TOKENS controls the normal Gemini answer budget; retries can temporarily expand it up to 4096 tokens so chat messages do not get cut off.
From the repository root:
npm install
npm install --prefix client
npm install --prefix serverStart both Vite and Express:
npm run devLocal URLs:
- Frontend: http://localhost:5173
- Backend: http://localhost:3001
- Backend health: http://localhost:3001/health
- API health: http://localhost:3001/api/v1/health
Useful single-process commands:
npm run dev:client
npm run dev:servernpm run lint
npm run build --prefix clientThen open the frontend, sign up or sign in, and confirm:
- contacts can be added, edited, and deleted
- Meta Instagram followers/following JSON exports can be bulk imported into contacts without reimporting duplicate handles
- graph nodes can be dragged and reset
- network chat responds and marks whether the answer was made by Gemini
- sign out works
The Dockerfile builds the React client inside the image, copies the built SPA into the Express server, and runs production Express on port 3000.
Important env split:
client/.envis read at Docker build time because Vite bakes Firebase Web AppVITE_*values into the browser bundle.VITE_API_BASE_URLis intentionally not baked into Docker. Production uses same-origin API calls onlocalhost:3000.server/.envis read at Docker run time through--env-file server/.env.
Build:
npm run docker:buildnpm run docker:build passes only browser-safe Firebase Web App values from client/.env through a temporary BuildKit secret file. It does not pass Firebase Admin credentials, GEMINI_API_KEY, or the local dev VITE_API_BASE_URL.
Run:
npm run docker:runThis script is equivalent to:
docker run --env-file server/.env -e NODE_ENV=production -e PORT=3000 -p 3000:3000 team7:latestDocker URLs:
- App: http://localhost:3000
- Health: http://localhost:3000/health
- API health: http://localhost:3000/api/v1/health
Verify from another terminal:
curl http://localhost:3000/health
curl http://localhost:3000/api/v1/healthStop the container with Ctrl+C if using npm run docker:run.
For detached testing:
docker run --name team7-check --env-file server/.env -e NODE_ENV=production -e PORT=3000 -p 3000:3000 -d team7:latest
docker ps --filter name=team7-check
docker logs team7-check
curl http://localhost:3000/health
docker rm -f team7-check- If port
3000is busy, stop the old container or run with another host port:docker run --env-file server/.env -e NODE_ENV=production -e PORT=3000 -p 3002:3000 team7:latest
- If protected API routes return
503, Firebase Admin is not configured. Checkserver/.env. - If Firebase Admin reports a private-key parse error, make sure
FIREBASE_PRIVATE_KEYis one quoted line with escaped\ncharacters. - If Gemini answers show as local fallback, make sure
GEMINI_API_KEYis set inserver/.envand restart the server/container. - Real env files are excluded by
.dockerignore; Docker receives secrets only through--env-file server/.envat runtime.
- Docker/GKE should run on container port
3000. - Local Express development defaults to
3001. - The built SPA is served by Express in production, including non-API route fallback to
index.html. - Kubernetes manifests live in
k8s/.
Production runs on Google Kubernetes Engine. The full step-by-step guide (secrets, config map, service account, ingress, managed certificate, scaling, and self-healing) is in k8s/README.md. The short path:
# 1. Build and push the image to GCR (replace PROJECT_ID)
docker build --secret id=client_env,src=client/.env.production \
-t gcr.io/PROJECT_ID/team7:latest .
docker push gcr.io/PROJECT_ID/team7:latest
# 2. Point kubectl at the cluster
gcloud container clusters get-credentials CLUSTER_NAME --zone ZONE --project PROJECT_ID
# 3. Apply manifests (namespace first, then the rest)
kubectl apply -f k8s/namespace.yaml
kubectl create secret generic firebase-secrets \
--from-literal=project_id=... --from-literal=client_email=... \
--from-literal=private_key=... -n team7
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/serviceaccount.yaml
sed 's/PROJECT_ID/YOUR_PROJECT_ID/g' k8s/deployment.yaml | kubectl apply -f -
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/managed-certificate.yaml
kubectl apply -f k8s/ingress.yaml
# 4. Verify the rollout, scaling, and self-healing
kubectl rollout status deployment/team7-app -n team7
kubectl get pods -n team7
kubectl scale deployment team7-app --replicas=3 -n team7 # manual scaling.github/workflows/deploy.yml builds and pushes
the image, runs lint/tests, then rolls the new image onto GKE on every push to
main. It requires these GitHub repository secrets: GCP_PROJECT_ID,
GCP_SA_KEY, GKE_CLUSTER_NAME, GKE_ZONE, and CLIENT_ENV_PRODUCTION.
npm run dev # client + server
npm run dev:client # Vite only
npm run dev:server # Express only
npm run lint # client ESLint
npm run build # client build + server build script
npm run build:client # Vite production build
npm run docker:build # build team7:latest
npm run docker:run # run team7:latest on localhost:3000