Roybal Chat
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.
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