Backend service for the Project Showcase platform: course catalog, semester offerings, rosters and teams, student project deployment (Docker builds and containers), Cornell Spark API keys, and admin tooling. Clients authenticate with Firebase ID tokens, then use JWT-backed API calls.
| Layer | Technology |
|---|---|
| Runtime & language | Node.js · TypeScript (ES modules) |
| HTTP API | Express 5 (src/app.ts / src/server.ts) |
| Validation | Zod (request bodies, params, env) |
| Data | Prisma · SQLite (prisma/schema.prisma, migrations under prisma/migrations/) |
| Auth | Firebase Admin SDK (verify ID tokens) · JWT access tokens |
| Deployments & ops | Dockerode (containers) · simple-git (clone before build) · node-cron (container monitor / pruning) |
| Tests | Vitest · supertest (test/, mirrors src/) |
For environment variables, Docker, data volumes, and curl examples, continue with the sections below. For architecture, folder layout, routing patterns, and where to change code, see docs/README.md (module guides under docs/).
This application uses environment variables for configuration. Follow these steps to set up your environment:
Copy the example environment file and customize it for your setup:
cp .env.example .envThe variables below match .env.example—use the commands in §1 first, then fill in real values (especially ACCESS_TOKEN_SECRET, DATABASE_URL, and Spark settings if needed).
| Variable | Description | Default in .env.example |
|---|---|---|
ADMIN_EMAILS |
Comma-separated emails seeded as admins when you run npm run seed |
(empty) |
ACCESS_TOKEN_SECRET |
Secret for JWT access tokens; minimum 16 characters (validated in src/config/env.ts) |
(must be set) |
PORT |
HTTP listen port (see server.ts; defaults to 8000 if unset at runtime) |
8000 |
NODE_ENV |
development, production, or test |
development |
DATABASE_URL |
Prisma connection string | file:./sqlite.db |
RATE_LIMIT_WINDOW_MS |
Per-user rate limit window (middleware/rateLimit.ts) |
900000 (15 minutes) |
RATE_LIMIT_MAX_REQUESTS |
Requests allowed per window per authenticated user | 100 |
FIREBASE_SERVICE_ACCOUNT_PATH |
Firebase Admin service-account JSON path for login verification | ./firebase-service-account.json |
PRISMA_LOG_QUERIES |
When true, enables query logging only while NODE_ENV=development (prisma.ts) |
true |
PRISMA_LOG_ERRORS |
When not false, log Prisma client errors |
true |
PRISMA_LOG_WARNINGS |
When not false, log Prisma warnings |
true |
DATA_FILES_DIR |
Upload directory for deployment data files; persist via volume in Docker | /app/data/project-data-files |
SPARK_BASE_URL |
Cornell Spark API base URL (required when hitting Spark endpoints; see spark/sparkService.ts) |
(empty until configured) |
SPARK_SECRET_KEY |
Server secret for Spark calls (required with SPARK_BASE_URL when using Spark) |
(empty until configured) |
Also used by code but not shipped in .env.example: add when needed—DATA_FILES_HOST_DIR (host path mapping for uploads / admin paths), DOCKER_SOCKET_PATH, and MAX_CONCURRENT_BUILDS (deploy queue concurrency).
Add admin emails to your .env file and run the seed script:
# Add to .env:
# ADMIN_EMAILS=admin1@example.com,admin2@example.com
# Run seed script
npm run seed# Development
npm run dev
# Production (after building)
npm run build
npm start
# Test environment
npm run testThis application includes Docker support with automatic database migrations and admin user seeding on startup.
The recommended way to run the backend is with Docker Compose, which ensures proper volume mounts for data persistence:
# Create required directories on host
mkdir -p data project-data-files
# Start the service
docker-compose up -d
# View logs
docker-compose logs -fImportant Volume Mounts:
./data:/app/data- Persists the SQLite database across container restarts./project-data-files:/app/data/project-data-files- Persists uploaded data files for deployed projects/var/run/docker.sock:/var/run/docker.sock- Allows the backend to manage student project containers
If you prefer to run without Docker Compose:
# Create required directories
mkdir -p data project-data-files
# Build the image
docker build -t project-showcase-backend .
# Run the container with all necessary volume mounts
docker run -d \
--name showcase-backend \
-p 8000:8000 \
-e ADMIN_EMAILS="admin1@example.com,admin2@example.com" \
-e DATABASE_URL="file:/app/data/sqlite.db" \
-e NODE_ENV="production" \
-v $(pwd)/data:/app/data \
-v $(pwd)/project-data-files:/app/data/project-data-files \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd)/firebase-service-account.json:/app/firebase-service-account.json:ro \
--network projects_network \
project-showcase-backend
# View logs
docker logs -f showcase-backendAll data is persisted on the host machine in the following directories:
./data/- Contains the SQLite database (sqlite.db) and Prisma migrations./project-data-files/- Contains uploaded data files that are mounted to student project containers
These directories will survive container restarts, rebuilds, and updates.
The Dockerfile CMD runs three commands sequentially:
npx prisma migrate deploy- Database migrations run automaticallynpm run seed- Admin users are seeded fromADMIN_EMAILSnpm start- Server starts and accepts connections
When deploying projects, you can now provide custom Docker build arguments to configure the build process. This is useful for setting build-time variables like environment-specific configurations, feature flags, or build optimization settings.
When deploying a project, include the optional buildArgs field in your request:
# Deploy with build arguments
curl -X POST http://localhost:3000/api/projects/deploy \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"teamId": 1,
"githubUrl": "https://github.com/username/repository",
"buildArgs": {
"NODE_ENV": "production",
"API_URL": "https://api.example.com",
"FEATURE_FLAG": "enabled"
}
}'The streaming deployment endpoint also supports build arguments:
# Deploy with streaming and build arguments
curl -X POST http://localhost:3000/api/projects/deploy/stream \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"teamId": 1,
"githubUrl": "https://github.com/username/repository",
"buildArgs": {
"BUILD_VERSION": "1.0.0",
"ENABLE_CACHE": "true"
}
}'To use the build arguments in your project's Dockerfile, declare them with ARG:
# Dockerfile example
FROM node:18-alpine
# Declare build arguments
ARG NODE_ENV=development
ARG API_URL
ARG FEATURE_FLAG=disabled
# Use build arguments as environment variables
ENV NODE_ENV=$NODE_ENV
ENV API_URL=$API_URL
ENV FEATURE_FLAG=$FEATURE_FLAG
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# Build arguments are available during build
RUN echo "Building with NODE_ENV=$NODE_ENV"
EXPOSE 3000
CMD ["npm", "start"]Build arguments are stored in the database along with the project deployment information and can be viewed in the project details.
Vite inlines import.meta.env.VITE_* values when you run vite build, so those values must be available during docker build, not only when the container starts.
This platform merges into Docker build args (then passes them to docker build), in order (later overrides earlier):
- Team-issued PRODUCTION variables whose names start with
VITE_. extraEnvVarson the deploy request — only keys starting withVITE_are added to the build (they are still merged into the container environment at runtime with all other keys).buildArgsfrom the deploy request (any keys, including non-VITE_).
If you only change VITE_* in extra env on deploy, those values must be included here so docker build sees a new --build-arg and the frontend layer cache invalidates. Previously, changing only runtime env did not affect the image build.
In your Dockerfile, declare each variable with ARG, expose it to the Vite build with ENV, and run the frontend build in that stage:
ARG VITE_API_BASE_URL
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
WORKDIR /app/frontend
RUN npm ci && npm run buildUse only non-secret values in VITE_* (anything in the frontend bundle is public).
When deploying projects, you can upload a data file that will be automatically mounted to the deployed container. This is useful for providing datasets, configuration files, or other resources that your project needs at runtime.
Data files must be uploaded using multipart/form-data:
# Deploy with a data file
curl -X POST http://localhost:3000/api/projects/deploy \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "teamId=1" \
-F "githubUrl=https://github.com/username/repository" \
-F "buildArgs={\"NODE_ENV\":\"production\"}" \
-F "dataFile=@/path/to/your/data.csv"# Deploy with streaming and data file
curl -X POST http://localhost:3000/api/projects/deploy-streaming \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "teamId=1" \
-F "githubUrl=https://github.com/username/repository" \
-F "dataFile=@/path/to/dataset.json"- Upload Limit: 1GB per file
- Storage: Files are stored on the host at
./project-data-files/(persists across restarts) - Mount Path: All uploaded files are mounted read-only at
/data/uploaded-datain the container - Access in Container: Your application can read the file at
/data/uploaded-data
# Python example
import pandas as pd
# Read the uploaded data file
df = pd.read_csv('/data/uploaded-data')
print(f"Loaded {len(df)} rows from uploaded data")// Node.js example
const fs = require('fs');
const path = require('path');
// Read the uploaded data file
const dataPath = '/data/uploaded-data';
const data = fs.readFileSync(dataPath, 'utf8');
console.log('Loaded data:', data);The data file path is stored in the database with the project deployment information.