Skip to content

portal-business

The business owner portal is split across two Railway services that work together:

Service Role Railway hostname
portal-business-web Caddy: serves SPA HTML + proxies /api/* admin.stage.innerlight.dev (public)
portal-business Fastify BFF: validates JWT, proxies to scheduler-api Railway internal

The SPA must be served with specific cache headers (fingerprinted assets = 1 year, index.html = no-cache). The Fastify BFF needs to run server-side JWT validation before forwarding requests. Caddy handles the first; Fastify handles the second. A single service cannot do both cleanly.

# Caddyfile logic:
/api/* → reverse_proxy $PORTAL_BUSINESS_API_URL (Fastify BFF, Railway internal)
/* → try_files {path} /index.html (SPA fallback for TanStack Router)

The SPA assets are baked into the Docker image at build time. Caddy serves them statically; there is no runtime SSR.

Attribute Value
Framework Fastify (Node.js)
Port 3043
Entry point server/index.ts
Auth WorkOS AuthKit (server-side JWT validation)
API client Proxies to scheduler-api admin surface
sequenceDiagram
    actor BO as Business owner
    participant Caddy as portal-business-web\n(Caddy)
    participant BFF as portal-business\n(Fastify)
    participant SA as scheduler-api

    BO->>Caddy: GET /api/bookings
    Caddy->>BFF: reverse_proxy /api/bookings
    BFF->>BFF: validate WorkOS JWT
    BFF->>SA: tRPC /trpc/* admin.* (with validated token)
    SA-->>BFF: data
    BFF-->>Caddy: response
    Caddy-->>BO: JSON response
Attribute Value
Framework Vite + React
Router TanStack Router (client-side)
State React Query (data fetching)
Calendar FullCalendar v6
Charts Recharts
UI @nexus/ui (shadcn + Radix + Tailwind)

The SPA and BFF build separately:

Terminal window
pnpm --filter portal-business build # Builds SPA → dist/
pnpm --filter portal-business build:api # Builds Fastify BFF → dist/server/

The Docker build copies both outputs into the image; portal-business-web (Caddy) serves the SPA output, and portal-business (Fastify) runs the server output.

In development, Vite’s dev server proxies /api/* to the local Fastify BFF — masking the Caddy layer. A bug that only reproduces in production (e.g., missing /api/* route in Caddy) won’t be caught locally. Always verify Caddy routing changes in a staging deploy.