web — Marketing Site
@playpals/web · port 3000 · apps/web
Next.js 15 with the App Router and React 19. Deliberately minimal — the product lives in the mobile app, and this is the public surface around it.
Running it
pnpm --filter @playpals/web dev # next dev --turbopack --port 3000
pnpm --filter @playpals/web build
pnpm --filter @playpals/web lint # next lint --max-warnings 0
pnpm --filter @playpals/web check-typesRoutes
app/
├── layout.tsx root layout, Geist fonts
├── page.tsx landing page
├── privacy/page.tsx privacy policy — required by the app stores
├── (auth)/
│ ├── layout.tsx
│ ├── login/page.tsx
│ └── register/page.tsx
├── (dashboard)/
│ ├── layout.tsx
│ └── dashboard/page.tsx
└── (admin)/
├── layout.tsx
└── admin/page.tsxPartial The auth, dashboard, and admin route groups are scaffolding. The landing and privacy pages are the parts that matter today.
The privacy page is not decorative — Apple and Google both require a reachable privacy policy URL for store listings.
Stack
| Concern | Choice |
|---|---|
| Framework | Next.js 15, App Router |
| React | 19 |
| Data fetching | TanStack Query + axios |
| Dev server | Turbopack |
| Fonts | Geist (self-hosted in app/fonts/) |
| Output | standalone — required by the Dockerfile |
Configuration
| Variable | Value in production | Notes |
|---|---|---|
NEXT_PUBLIC_API_URL | https://api.playpalz.gg/api/v1 | Used as the axios baseURL — must include /api/v1 |
NEXT_PUBLIC_API_TIMEOUT | 10000 | Milliseconds |
PORT | 3000 | |
HOSTNAME | 0.0.0.0 | Required in a container |
NEXT_TELEMETRY_DISABLED | 1 |
NEXT_PUBLIC_* variables are inlined into the client bundle at build time, not read at runtime. Changing one means rebuilding and pushing a new image — setting it in the deployment manifest alone has no effect on already-built client code.
Docker
apps/web/Dockerfile is a four-stage build producing a Next.js standalone bundle:
- base — Node 22 Alpine with pnpm via corepack
- deps — installs from the lockfile with only the manifests copied, for layer caching
- builder — copies source and runs
pnpm build - runner —
dumb-initas PID 1, non-rootnextjsuser (uid 1001)
Standalone output in a monorepo places files at particular paths, which is why the runner stage copies static assets explicitly:
COPY --from=builder /app/apps/web/.next/standalone ./
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder /app/apps/web/public ./apps/web/public
CMD ["node", "apps/web/server.js"]The Docker health check targets a route that does not exist
HEALTHCHECK … CMD node -e "require('http').get('http://localhost:3000/api/health', …)"There is no app/api/health/route.ts. The check treats any status under 500 as healthy and Next.js returns 404 for the missing route, so it passes for the wrong reason. The Kubernetes probes use GET / and are unaffected. Adding the route is a two-line fix.
Deployment
| Property | Value |
|---|---|
| Manifest | infra/k8s/web-deployment.yaml |
| Replicas | 3, rolling update with maxUnavailable: 0 |
| Probes | Liveness and readiness on GET / |
| Security | Non-root uid 1001, capabilities dropped |
| Placement | Pod anti-affinity across nodes |
| Ingress | www.playpalz.gg and playpalz.gg |
./scripts/build-web.sh [tag]Both apex and www route to the same service; the redirect to www is expected to be handled by the app.
