UCLA CS35L group project — a marketplace where UCLA students can list and book services from one another. Vite + React frontend, Express API server, Neon Postgres database, Descope for authentication.
client/ Vite + React app
src/
components/ Navbar, CalendarPicker, PhotoGallery, RequireAuth, RequireRole
constants/ shared constants (e.g. service categories)
hooks/ useRole — reads roles from the Descope session JWT
pages/ route components (Home, Login, Browse, ListingDetail,
ProviderProfile, Dashboard, Booking, Chat, SavedListings)
tests/ Playwright end-to-end tests (+ auth/ setup helpers)
server/ Express API
middleware/ auth.js — requireAuth / requireRole (Descope token validation)
routes/ auth, listings, providers, dashboard, bookings, chat,
reviews, flags
migrations/ SQL migrations (run in filename order)
scripts/ migrate.js, seed.js
db.js Neon Postgres client
index.js Express entrypoint
- Node.js 20+
- npm 10+
- Access to the team's Vercel project — the Neon Postgres database and the Descope project IDs are stored as Vercel environment variables.
From the repo root:
npm installThis installs workspaces for both client/ and server/.
The Neon DATABASE_URL and the Descope project IDs live in Vercel's environment
variables — do not commit them. Pull them into a local env file:
npm i -g vercel # if you don't already have the CLI
vercel login # use the account that has access to the project
vercel link # select the existing "bruin-service-cs35l" project
vercel env pull .env.development.localAfter this, .env.development.local (at the repo root) should contain:
DATABASE_URL=postgres://...— Neon connection string (plus several related Neon/Postgres vars)DESCOPE_PROJECT_ID=...— used by the server middleware to validate sessionsVITE_DESCOPE_PROJECT_ID=...— used by the client's DescopeAuthProviderDESCOPE_MANAGEMENT_KEY=...— secret used by the Playwright test setup (see Tests)
Both the server and the Vite client read this single root file: the server scripts
pass --env-file=../.env.development.local, and vite.config.js sets envDir: '../'.
The file is already gitignored.
If you don't have Vercel access, ask a teammate to send you the values above and paste them in manually:
DATABASE_URL=postgres://<user>:<password>@<host>/<db>?sslmode=require
DESCOPE_PROJECT_ID=<project-id>
VITE_DESCOPE_PROJECT_ID=<project-id>
DESCOPE_MANAGEMENT_KEY=<management-key>
This creates and evolves the schema (users, listings, bookings, reviews, conversations, messages, flags, provider_profiles):
npm run db:migrate --workspace=serverMigrations are idempotent (CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS,
etc.), so it's safe to re-run.
Loads the sample listings used by the booking and listing-detail pages:
npm run db:seed --workspace=serverRe-running won't duplicate rows (ON CONFLICT (id) DO NOTHING).
From the repo root:
npm run devThis starts both the API server and the Vite dev server concurrently:
- API server →
http://localhost:3001 - Vite dev server →
http://localhost:5173
The Vite dev server proxies /api requests to the API server (see vite.config.js),
so the frontend can call relative /api/... paths in development.
You can also run them separately:
npm run dev:server
npm run dev:clientAuth is handled by Descope, not by the API server directly.
- The client wraps the app in Descope's
AuthProvider(client/src/main.jsx) and sends the Descope session token on API requests. - The server validates that token in
requireAuthmiddleware and enforces roles withrequireRole(...)(server/middleware/auth.js). - There are two roles,
customerandprovider. TheuseRolehook (client/src/hooks/useRole.js) reads them from the session JWT, andRequireAuth/RequireRoleguard the client routes.
The /api/auth/login and /api/auth/logout routes are currently stubs — login/logout
flow through the Descope SDK on the client.
Most API routes require a valid session; /api/dashboard/* additionally requires the
provider role.
| Mount | Auth | Endpoints |
|---|---|---|
/api/auth |
none | POST /login, POST /logout (stubs) |
/api/listings |
requireAuth | GET /, GET /categories, GET /:id |
/api/listings (reviews) |
requireAuth | GET /:id/reviews, POST /:id/reviews |
/api/listings (flags) |
requireAuth | POST /:id/flag |
/api/providers |
requireAuth | GET /:id (stub) |
/api/dashboard |
requireAuth + provider | GET/POST /listings, PUT/DELETE /listings/:id, GET/PUT /profile |
/api/bookings |
requireAuth | GET /, GET /mine, GET /:id, POST /, DELETE /:id, PATCH /:id/cancel-provider |
/api/chat |
requireAuth* | conversations + messages CRUD |
* /api/chat is mounted without the global requireAuth guard; check the route file
before relying on its auth behavior.
End-to-end tests live in client/tests/ and use Playwright. There is no npm test
script yet; run them from the client/ workspace:
cd client
npx playwright install # first time only, installs browsers
npx playwright testKey locations:
- Test specs →
client/tests/*.spec.js - Auth setup →
client/tests/auth/global-setup.js - Playwright config →
client/playwright.config.js
The test setup creates a temporary Descope test user and logs it in, so it needs a
DESCOPE_MANAGEMENT_KEY in addition to the project ID. Pull all required vars via
vercel env pull .env.development.local — never commit that file.
Playwright starts its own dev server with VITE_TEST_MODE=true.
- Create a new file in
server/migrations/with the next number, e.g.009_add_something.sql. - Write idempotent SQL (
CREATE TABLE IF NOT EXISTS,ALTER TABLE ... ADD COLUMN IF NOT EXISTS, etc.). - Run
npm run db:migrate --workspace=server.
Files run in filename order, so prefix with a zero-padded number and keep prefixes
unique — there are currently two 008_ files, which is fragile; avoid adding to that.
Note that migrate.js splits each file on ; followed by a newline, so keep one
statement per line ending in a semicolon and avoid semicolons mid-statement.
users— id (uuid), email, password_hash, name, created_at. Note: auth now goes through Descope, sopassword_hashis no longer used.listings— id (text), provider_id (text — holds Descope user IDs), name, category, location, description, price, duration, taken_down (bool), photos/services/available_dates/reviews (jsonb), created_atbookings— id (uuid), listing_id (→ listings), user_id, date, time, customer_name, customer_email, status (defaults toconfirmed), created_atreviews— id (uuid), listing_id (→ listings), user_id, rating (1–5), comment, created_at; one review per (listing, user)conversations— id (uuid), customer_id, provider_id, listing_id (→ listings), listing_name, created_at; unique per (customer, provider, listing)messages— id (uuid), conversation_id (→ conversations), sender_id, sender_name, body, created_atflags— id (uuid), listing_id (→ listings), user_id, created_at; unique per (listing, user)provider_profiles— provider_id (text, pk), bio, name
The migrations in server/migrations/ are the source of truth; 001_init.sql is the
baseline and 002–008 evolve it.
DATABASE_URL is not set—.env.development.localis missing or empty. Re-runvercel env pull .env.development.localfrom the repo root.password authentication failed— yourDATABASE_URLis stale; pull it again from Vercel.- 401 / "Invalid token" on API calls — the Descope session is missing or expired,
or
DESCOPE_PROJECT_ID/VITE_DESCOPE_PROJECT_IDaren't set. Re-pull env vars and make sure you're logged in on the client. - 403 / "Forbidden" — you're authenticated but lack the required role (e.g. hitting
a
/api/dashboardroute without theproviderrole). - Playwright auth setup fails —
DESCOPE_MANAGEMENT_KEYis missing or stale; re-runvercel env pull .env.development.local(see the Tests section). - Migration says "extension pgcrypto does not exist" — only happens on non-Neon
Postgres; on Neon,
pgcryptois preinstalled.