Claude on Vertex AI authenticates with Google Cloud credentials: requests carry an OAuth access token for a principal that holds the aiplatform.endpoints.predict permission (part of the Vertex AI User role). That model is built for servers. A browser is the one place those credentials must never go — anything embedded in frontend code or fetched into a browser tab is readable by the person driving it, and by any script that finds its way onto the page.
Why direct browser calls fail the security review
The credential is worth more than the chat. A Google Cloud access token is not a scoped "Claude key" — it authenticates a principal that may be able to do anything its IAM roles allow, against any API. An attacker who extracts it from the browser doesn't just get free Claude calls billed to you; they get whatever that identity can reach.
You lose the ability to say who did what. If every user's request arrives at Vertex under one shared service identity handed to the frontend, your audit logs collapse into a single anonymous caller — the exact opposite of the answerable record described in the audit log cookbook.
You lose every control point. Rate limiting per user, input validation, prompt hardening, abuse detection, spend caps — all of these need a point in the path that you control and the user does not. With browser-to-Vertex calls, that point doesn't exist: your quota (requests and tokens per minute, shared per model lineage) and your bill are directly steerable by whoever opens DevTools.
The proxy pattern
The fix is a small backend you own between the browser and Vertex: browser → your API → Vertex AI. Cloud Run is a natural home for it — the service runs with an attached service account, Application Default Credentials pick that identity up automatically, and no key material exists anywhere in the system. The browser authenticates to your endpoint with your users' sessions; only the proxy holds Google Cloud identity.
# Minimal Cloud Run proxy (Flask)
from flask import Flask, request, jsonify
from anthropic import AnthropicVertex
app = Flask(__name__)
client = AnthropicVertex(project_id="my-project", region="global")
@app.post("/api/chat")
def chat():
text = str(request.json.get("message", ""))[:8000] # cap input
msg = client.messages.create(
model="claude-sonnet-5", max_tokens=1024,
messages=[{"role": "user", "content": text}])
return jsonify({"reply": msg.content[0].text})
A production version adds, in roughly this order: authentication of your end users (so requests map to accounts, not IP addresses), per-user rate limiting, a server-side system prompt the browser can never see or override, streaming pass-through (the SDK's client.messages.stream(...) pairs naturally with server-sent events to the browser), and structured logging per request. If you want managed enforcement of keys, quotas, and routing in front of the proxy, put API Gateway or a similar layer ahead of Cloud Run — the principle is unchanged: Google Cloud credentials terminate server-side.
What the proxy buys you beyond safety
Once the path runs through your backend, the rest of this site's operational advice becomes implementable: model tiering by request type, prompt caching for the shared system prompt, per-user cost attribution, and a single choke point for incident response — one deploy turns off a misbehaving feature. It also insulates the frontend from platform details: swapping regions, models, or even platforms (see the gateway pattern) never touches browser code.
Where to go next
For deploying the proxy itself, see Cloud Run deployment; for hardening what users can make the model do, system prompt hardening and prompt injection 101; for the streaming leg, streaming responses on Vertex.