# 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 under `spec.dependencies` so the builder injects `{SERVICE}_URL`:

```yaml
spec:
  dependencies:
    - service: my-api    # injects MY_API_URL into the frontend pod
      transport: direct  # same-org UI/API split — the one valid use of `direct`.
                         # Omit it to get the `janus` default (metered, JWT-verified).
```

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_*`.

## Build-time vars via `tawa config set` (NEXT_PUBLIC_* / REACT_APP_*)

For values you *do* want baked at build time (public client-side config like a public API base),
declare them in `insureco.io/env-vars` and set them with `tawa config set`. Before `docker build`,
the builder materializes those declared keys from the config store into a **`.env.production`** at
the build-context root, which CRA / Vite / Next.js read during `npm run build`. So you do **not**
need to commit a `.env` — set the value with `tawa config set NEXT_PUBLIC_FOO=...` and declare it in
`env-vars`. See `build-pipeline.md` → "Build-time env injection".

> Only the **declared** `env-vars` keys are written (public-by-design). Secrets and
> `internalDependencies`/`databases`/`auth` vars are never build-baked — they stay runtime-only.

**Static SPAs (CRA/Vite):** for resilience, also resolve the API base at **runtime** from
`window.location.hostname` (e.g. `*.example.com → https://api.example.com`) so the app works even if
build-time injection is misconfigured.

## `public/` — you no longer need one

The generated Dockerfile copies `public/` into the runtime image. It also **creates the
directory in the builder stage first** (`RUN mkdir -p /app/public`), so a Next.js service
that ships no static assets from disk builds fine without one.

This was not always true. Until 2026-08-21 the copy was unconditional, and a service with
no `public/` failed the image build with:

```
COPY --from=builder /app/public ./public
ERROR: failed to calculate checksum of ref ...: "/app/public": not found
```

— after a full dependency install and compile, which made it an expensive way to learn that
an optional Next.js directory was mandatory on Tawa. Twenty-six services had worked around
it with a committed `public/.gitkeep`.

**If your repo has a `public/.gitkeep` and nothing else in `public/`,** it is now dead weight
and safe to delete. There is no rush — an empty directory costs nothing — but nothing new
needs to add one.

## Common Mistakes

| Wrong | Right |
|-------|-------|
| Committing `public/.gitkeep` to satisfy the build | Not needed — the builder creates it |
| `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 |
