Proxy routes
Call AI APIs and databases from a static site — secrets stay server-side
Proxy routes let your published site make authenticated calls to external APIs — OpenAI, OpenRouter, Supabase, anything — without ever shipping the API key to the browser. You declare routes in a manifest; shiply injects the secrets server-side at request time.
How it works
- Store your key in Variables (encrypted at rest).
- Publish a
.shiply/proxy.jsonmanifest with your site's files. - Your page calls a relative URL — shiply matches it, adds the headers
with
${VAR}resolved from your Variables, and forwards to the upstream.
The manifest itself is never served to visitors, and Set-Cookie from
upstreams is stripped.
Manifest: .shiply/proxy.json
{
"proxies": {
"/api/chat": {
"upstream": "https://openrouter.ai/api/v1/chat/completions",
"method": "POST",
"headers": { "Authorization": "Bearer ${OPENROUTER_API_KEY}" }
},
"/api/db/*": {
"upstream": "https://xyz.supabase.co/rest/v1",
"headers": { "apikey": "${SUPABASE_ANON_KEY}" }
}
}
}- Exact routes (
/api/chat) match only that path. An optionalmethodrestricts the HTTP verb. - Prefix routes (
/api/db/*) match the prefix and append the remainder (plus query string) to the upstream:/api/db/users?limit=1→https://xyz.supabase.co/rest/v1/users?limit=1. ${VAR_NAME}tokens in header values resolve from your account Variables at request time. A missing variable returns502 {"error":"proxy_var_missing"}naming the variable.
Calling it from your page
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
model: 'anthropic/claude-sonnet-4-6',
messages: [{ role: 'user', content: 'hello' }],
}),
})
const data = await res.json()Streaming responses (SSE) pass through, so chat UIs can stream tokens.
Rules & limits
| Rule | Value |
|---|---|
| Site must be owned | anonymous sites get 403 proxy_requires_account — claim first |
| Upstreams | https:// only; public hosts only (no localhost/private IPs) |
| Routes per site | 20 |
| Request body | 10 MB max |
| Rate limit | 100 requests/hour per IP per route |
| Visitor cookies | not forwarded upstream; upstream Set-Cookie stripped |
Errors
| Status | Body error | Fix |
|---|---|---|
| 403 | proxy_requires_account | claim the site / publish with an API key |
| 502 | proxy_var_missing | add the named variable in Dashboard → Variables |
| 502 | proxy_upstream_unreachable | check the upstream URL |
| 429 | rate_limited | back off; raise limits on a paid plan (contact support) |
| 400 | proxy_manifest_invalid | fix .shiply/proxy.json and re-publish |