Next.js on Tawa
Next.js frontend services deployed on Tawa have a critical constraint: **environment variables injected by the platform are only available at runtime, not at…
Ruledeploy
About
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.
Skill Content
This is the raw markdown that gets installed as a Claude Code rule.
# Next.js on Tawa
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.
## The Rule
> **Never use `rewrites()` in `next.config.js` to 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.
```javascript
// WRONG — BINDDESK_API_URL is not set at build time,
// so this always falls back to localhost:4000 in production
async rewrites() {
const apiUrl = process.env.BINDDESK_API_URL || 'http://localhost:4000'
return [{ source: '/api/:path*', destination: `${apiUrl}/api/:path*` }]
}
```
## Correct Pattern: Catch-All API Route Proxy
Create `app/api/[...path]/route.ts`. This is a Next.js App Router route handler — it runs on the server at request time, so it reads env vars correctly from the live pod environment.
```typescript
// app/api/[...path]/route.ts
import { NextRequest, NextResponse } from 'next/server'
const API_URL = process.env.BINDDESK_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 — `app/api/health/route.ts` will still be served by Next.js and won't be forwarded upstream.
## catalog-info.yaml
Declare the backend as an `internalDependency` so the builder injects `{SERVICE}_URL`:
```yaml
spec:
internalDependencies:
- service: my-api # injects MY_API_URL (Janus proxy URL) into the frontend pod
```
The injected URL is the Janus internal proxy URL. Do not hardcode it.
## What IS Safe to Read at Build Time
Some Next.js features are designed for build-time values:
| 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 JS bundle) |
| Route handler body | **Runtime** | All platform-injected env vars (`BINDDESK_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`, or `auth` is runtime-only. Never reference them in `rewrites()`, `headers()`, `redirects()`, or `NEXT_PUBLIC_*`.
## Common Mistakes
| Wrong | Right |
|-------|-------|
| `process.env.MY_API_URL` in `rewrites()` | Catch-all API route proxy |
| `NEXT_PUBLIC_API_URL=http://...` for internal URLs | 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 might change per environment | Catch-all API route proxy |
Install
Copy the skill content and save it to:
~/.claude/rules/tawa-nextjs.mdComing soon via CLI:
tawa chaac install tawa-nextjsDetails
- Format
- Rule
- Category
- deploy
- Version
- 1.0.49672
- Tokens
- ~946
- Updated
- 2026-06-24
platformnextjsfrontendenv-varsproxyruntimebuild-time