Next.js frontend services deployed on Tawa have a critical constraint: environment variables injected by the platform are only available at runtime, not at build time. This affects how you must proxy API calls to backend services.
Never use
rewrites()innext.config.jsto proxy to a platform-injected URL. Use a catch-all API route instead.
rewrites() are evaluated when npm run build runs inside the Docker container — before the platform has injected any runtime env vars. The destination URL gets baked into routes-manifest.json and cannot change at runtime.
// WRONG — MY_API_URL is not set at build time,
// this always falls back to localhost in production
async rewrites() {
const apiUrl = process.env.MY_API_URL || 'http://localhost:4000'
return [{ source: '/api/:path*', destination: `${apiUrl}/api/:path*` }]
}
Create app/api/[...path]/route.ts. Route handlers run at request time on the live pod — they read env vars correctly.
// app/api/[...path]/route.ts
import { NextRequest, NextResponse } from 'next/server'
const API_URL = process.env.MY_API_URL || 'http://localhost:4000'
async function proxy(
req: NextRequest,
{ params }: { params: { path: string[] } }
): Promise<NextResponse> {
const path = params.path.join('/')
const { search } = new URL(req.url)
const targetUrl = `${API_URL}/api/${path}${search}`
const headers = new Headers(req.headers)
headers.delete('host')
const init: RequestInit = { method: req.method, headers }
if (req.method !== 'GET' && req.method !== 'HEAD') {
// @ts-expect-error duplex is required for streaming request bodies
init.duplex = 'half'
init.body = req.body
}
const upstream = await fetch(targetUrl, init)
return new NextResponse(upstream.body, {
status: upstream.status,
headers: upstream.headers,
})
}
export { proxy as GET, proxy as POST, proxy as PUT, proxy as PATCH, proxy as DELETE }
Specific routes take precedence over the catch-all. If you have app/api/health/route.ts, it is served by Next.js and is not forwarded upstream.
Declare the backend as an internalDependency so the builder injects {SERVICE}_URL:
spec:
internalDependencies:
- service: my-api # injects MY_API_URL (Janus proxy URL) into the frontend pod
The injected URL points to the Janus internal proxy. Do not hardcode it.
| Feature | Timing | Use for |
|---|---|---|
rewrites() destination | Build time | Static external URLs only (e.g. https://api.example.com) |
NEXT_PUBLIC_* | Build time | Client-side public values baked into the JS bundle |
| Route handler body | Runtime | All platform-injected env vars (MY_API_URL, BIO_CLIENT_ID, etc.) |
| Server Components | Runtime | All platform-injected env vars |
next.config.js env: | Build time | Static values only |
Rule of thumb: Any env var from
internalDependencies,databases, orauthis runtime-only. Never reference them inrewrites(),headers(),redirects(), orNEXT_PUBLIC_*.
| Wrong | Right |
|---|---|
process.env.MY_API_URL in rewrites() | Catch-all API route proxy |
NEXT_PUBLIC_API_URL for an internal Janus URL | Never expose internal K8s URLs client-side |
Hardcoding K8s DNS in rewrites() | Use the injected {SERVICE}_URL in a route handler |
Using rewrites() for any URL that varies per environment | Catch-all API route proxy |
Last updated: March 11, 2026