Back to selected work

Roybal Chat

Roybal Center · University of Michigan
Roybal Center homepage with the headline “Preventing dementia, one step at a time,” a mosaic of older adults walking and checking a fitness watch, and a floating “Ask our AI Assistant a question” chat button.

A security proxy that lets a public chat widget talk to a university LLM service without putting that service's credentials anywhere near a browser.

Role
Sole developer: API, security, deployment
Stack
Node.js · Express 5 · Helmet.js · Google Cloud Run
Status
In production

What we were solving

The Roybal Center wanted a conversational AI helper on their site, backed by Maizey, the University's internal LLM service. Maizey's credentials can't live in frontend code, and the Center wouldn't be the only site to want this. Any future client would need the same capability without sharing secrets, sharing a rate-limit budget, or being able to take each other down.

Architecture

The proxy sits in front of Maizey and does two jobs: it decides who's allowed to ask, and it forwards each question to Maizey in the shape Maizey expects. The browser talks to the proxy, the proxy talks to Maizey, and the browser never sees a Maizey credential.

Credentials come in three tiers. The Maizey service key stays on the server and is never shared. A separate allow-list of per-client API keys lets each consuming site be issued, rate-limited, or revoked independently. A third, higher-privilege admin key protects the status and management endpoints. To onboard a new client, I add their domain to the CORS allow-list and generate a key.

Rate limiting happens at two layers that look the same from the outside. The proxy has its own configurable limit, which by default resets in about a minute. Maizey has its own upstream limit, which isn't configurable and can mean waits of several hours. An end user can't tell them apart, so the project docs spell out the difference, and a report like 'why is this taking five hours' doesn't get misdiagnosed as a proxy bug.

# three credential tiers, deliberately separate MAIZEY_API_KEY / MAIZEY_PROJECT_ID / MAIZEY_USER_ID # service creds: never leave the server API_KEYS # per-client allow-list: issue, rotate, revoke independently ADMIN_API_KEY # separate, higher-privilege, for management endpoints

How we solved it

The proxying itself was simple. The harder part was making the two rate-limit layers distinguishable. A user who hits Maizey's upstream limit and a user who hits the proxy's limit see the same kind of error, but the fix differs (wait a minute, or take it up with the University about upstream policy). The docs walk through both cases so nobody has to work it out during an incident.

The other constraint was making multi-tenancy safe. Handing the proxy to a second client site had to work without code changes, so key issuance is a script (generate-keys.js) instead of a manual step, and CORS origins and API keys both live in environment configuration.

  • Maizey credentials never reach the browser
  • Per-client key issuance and CORS allow-list, independent revocation
  • Two rate-limit layers, documented separately to avoid misdiagnosis
  • Docker on Cloud Run: scales to zero, pay per request
Live in production. Deployed via Docker on Google Cloud Run, scaling to zero when idle.