App architecture
The dashboard is a Next.js App Router application. Routes live under
src/app, UI under src/components, data hooks under src/hooks, and shared
logic under src/lib.
Route tree
There are four top-level route groups:
| Group | Purpose |
|---|---|
/ , /overview | Landing / entry redirects |
/login, /signup, /forgot-password, /reset-password, /verify-email, /auth/oauth/*, /invite/* | Authentication, OAuth linking, and invite acceptance |
/check-in/t/[ticketCode] | Public ticket check-in (its own minimal layout) |
/dashboard/* | The authenticated admin surface — everything else |
/dashboard/layout.tsx wraps the admin surface with the sidebar chrome on
desktop and the MobileShell on small
viewports. A /dashboard/[...slug] catch-all handles dynamic/module routes that
aren’t statically declared.
The server-side proxy
The browser never calls the Box API directly. Every backend request goes through a single catch-all route handler:
/api/proxy/[...path] → ${BACKEND_API_URL}/<path>BACKEND_API_URL (falling back to NEXT_PUBLIC_API_URL, default
http://localhost:4000) is server-side only and is never shipped to the
client bundle. The handler applies four protections before forwarding:
- Path allowlist. Only requests whose first path segment is on the allowlist
are forwarded; anything else is rejected. Allowed prefixes include
tenants,accounts,billing,products,orders,bookings,customers,members,invoices,finance,loyalty,discounts,content,blog,campaigns,automations,media,analytics,onboarding,domains,shipping,locations, and more. - Cookie allowlist. Only auth cookies are forwarded upstream —
pb_auth_token,pb_csrf_token,pb_refresh_token. All other cookies are stripped. - Rate limiting. A simple per-IP limiter (100 requests / minute by default)
guards the proxy. Client IP is read from
x-forwarded-for/x-real-ip. - Method coverage.
GET,POST,PUT,PATCHandDELETEare all proxied through the same hardened path.
The in-memory rate limiter is per-instance and intended for development. In production behind multiple instances, back it with a shared store (e.g. Redis), as the source notes.
Authentication
Auth is cookie-based. After sign-in the Box API sets HTTP-only cookies
(pb_auth_token, pb_refresh_token, pb_csrf_token); because they are
HTTP-only, the access token cannot be read from client JavaScript. The proxy
forwards those cookies upstream on every call, and the auth/OAuth/invite pages
under /auth/* and /invite/* handle the sign-in, OAuth-link and
invite-acceptance flows.
Data layer
The dashboard uses TanStack Query (@tanstack/react-query) for all
server-state. Components call typed hooks in src/hooks that issue requests
through the proxy; queries handle caching, refetching and optimistic updates.
Desktop and mobile bodies for the same route share the same hooks — there is
no separate mobile data path.
Desktop and mobile share one URL tree
There is no separate /m/* route tree. The mobile experience is delivered as
“chrome + body” on the same /dashboard/* URLs:
- Chrome — on small viewports the layout mounts
MobileShell(top bar, bottom tab bar, FAB) instead of the desktop sidebar. - Body — each route renders a mobile body and a desktop body, switched by
CSS (
md:hidden/hidden md:block) so deep links, auth, and SEO keep working for both:
export default function Page() {
return (
<>
<div className="md:hidden"><MobileOrdersList /></div>
<div className="hidden md:block"><DesktopOrdersList /></div>
</>
)
}Both bodies reuse the same data hooks; only presentation differs. This keeps a single URL per screen for push/email links, shared URLs and middleware.
Notable libraries
| Concern | Library |
|---|---|
| Server state | @tanstack/react-query |
| Rich text / blog editor | @tiptap/* |
| Calendar / scheduling | @fullcalendar/* |
| Drag-and-drop | @dnd-kit/* |
| Charts | recharts |
| Image editing | react-easy-crop, react-filerobot-image-editor, konva |
| QR / check-in | html5-qrcode, qrcode.react |
| Validation | zod |
| Telemetry | @sentry/nextjs, posthog-js |