← Back to Skills

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.md
Download .md

Coming soon via CLI:

tawa chaac install tawa-nextjs

Details

Format
Rule
Category
deploy
Version
1.0.49672
Tokens
~946
Updated
2026-06-24
platformnextjsfrontendenv-varsproxyruntimebuild-time