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 |
Why two services?
Section titled “Why two services?”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.
portal-business-web (Caddy)
Section titled “portal-business-web (Caddy)”# 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.
portal-business (Fastify BFF)
Section titled “portal-business (Fastify BFF)”| 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 |
Auth flow
Section titled “Auth flow”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
Frontend (SPA)
Section titled “Frontend (SPA)”| 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) |
Build process
Section titled “Build process”The SPA and BFF build separately:
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.
Vite proxy in dev
Section titled “Vite proxy in dev”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.