Frontend Overview
Structure and conventions of the Basefloor TanStack Start web application.
Frontend Overview
The Basefloor web application is built with TanStack Start — a full-stack React framework from the TanStack ecosystem. It runs inside the basefloor-UI monorepo.
Repository Structure
basefloor-UI/
├── apps/
│ ├── web/ ← TanStack Start hotel staff app
│ │ ├── src/
│ │ │ ├── routes/ ← File-based routing
│ │ │ ├── components/ ← App-specific components
│ │ │ ├── services/ ← API communication layer
│ │ │ ├── hooks/ ← Custom React hooks
│ │ │ ├── store/ ← Global state (Zustand)
│ │ │ ├── types/ ← TypeScript types
│ │ │ └── utils/ ← Utilities (subdomain, etc.)
│ │ └── .env.example
│ └── marketing/ ← Next.js 15 marketing site
├── packages/
│ ├── ui/ ← @hms/ui shared component library
│ └── shared/ ← @hms/shared utilities and types
└── pnpm-workspace.yamlRouting
TanStack Start uses file-based routing. Routes are defined in src/routes/:
routes/
├── __root.tsx ← Root layout (auth check, providers)
├── index.tsx ← Landing / redirect
├── _auth/ ← Authenticated route group
│ └── $workspace/ ← Dynamic tenant workspace
│ ├── bookings/
│ ├── guests/
│ └── properties/
└── properties/ ← Public property browseThe $workspace segment corresponds to the tenant subdomain slug.
Subdomain Tenant Detection
On app load, the useSubdomain hook extracts the tenant from the current URL:
import { useSubdomain } from '../hooks/useSubdomain'
function App() {
const { subdomain, client, isLoading, isValid } = useSubdomain()
if (isLoading) return <Spinner />
if (!isValid) return <InvalidTenantPage />
return <Dashboard chain={client} />
}The hook:
- Reads
window.location.hostname - Extracts the subdomain (e.g.,
khyberfromkhyber.hms.com) - Fetches chain data from the API
- Returns the chain context to the component tree
API Layer
All API calls go through src/services/. Each resource has its own service file:
// src/services/bookings.ts
import { apiClient } from '../lib/api'
export const bookingsService = {
list: (params?: BookingFilters) =>
apiClient.get<Booking[]>('/bookings', { params }),
create: (data: CreateBookingInput) =>
apiClient.post<Booking>('/bookings', data),
checkIn: (id: number) =>
apiClient.post(`/bookings/${id}/check_in`),
}The apiClient is a configured Axios instance that automatically:
- Adds the
Authorization: Bearer <token>header from the auth store - Sets the base URL from
VITE_API_BASE_URL - Handles 401 errors by clearing the session and redirecting to login
State Management
Global state uses Zustand:
// src/store/auth.ts
export const useAuthStore = create<AuthState>((set) => ({
token: localStorage.getItem('token'),
user: null,
login: (token, user) => {
localStorage.setItem('token', token)
set({ token, user })
},
logout: () => {
localStorage.removeItem('token')
set({ token: null, user: null })
}
}))Shared UI Components — @hms/ui
All visual components come from the shared @hms/ui package, which wraps shadcn/ui:
import { Button, Card, DataTable } from '@hms/ui'Each app can have a different color theme while using the same components. The theme is applied via CSS custom properties in the app's globals.css.
To add a new shadcn/ui component to @hms/ui:
cd packages/ui
pnpm dlx shadcn@latest add dialogThen re-export it from src/index.ts.
Adding a New Route
- Create a file in
src/routes/following the naming convention - Export a component as the default export
- Export a
Routeusing TanStack'screateFileRoute:
// src/routes/_auth/$workspace/reports/index.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_auth/$workspace/reports/')({
component: ReportsPage,
})
function ReportsPage() {
return <div>Reports</div>
}TanStack Router will automatically generate the route tree on the next dev server start.
Environment Variables
| Variable | Description |
|---|---|
VITE_API_BASE_URL | Base URL for the hms-core API |
In development: http://localhost:4000/api/v1
In production: https://hms-api.kaisersakhi.com/api/v1 (set in Cloudflare dashboard)