Skip to content

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

bash
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-types

Routes

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.tsx

Partial 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

ConcernChoice
FrameworkNext.js 15, App Router
React19
Data fetchingTanStack Query + axios
Dev serverTurbopack
FontsGeist (self-hosted in app/fonts/)
Outputstandalone — required by the Dockerfile

Configuration

VariableValue in productionNotes
NEXT_PUBLIC_API_URLhttps://api.playpalz.gg/api/v1Used as the axios baseURL — must include /api/v1
NEXT_PUBLIC_API_TIMEOUT10000Milliseconds
PORT3000
HOSTNAME0.0.0.0Required in a container
NEXT_TELEMETRY_DISABLED1

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:

  1. base — Node 22 Alpine with pnpm via corepack
  2. deps — installs from the lockfile with only the manifests copied, for layer caching
  3. builder — copies source and runs pnpm build
  4. runnerdumb-init as PID 1, non-root nextjs user (uid 1001)

Standalone output in a monorepo places files at particular paths, which is why the runner stage copies static assets explicitly:

dockerfile
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

dockerfile
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

PropertyValue
Manifestinfra/k8s/web-deployment.yaml
Replicas3, rolling update with maxUnavailable: 0
ProbesLiveness and readiness on GET /
SecurityNon-root uid 1001, capabilities dropped
PlacementPod anti-affinity across nodes
Ingresswww.playpalz.gg and playpalz.gg
bash
./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.

Internal documentation — PlayPalz platform