Skip to content
Pratima Awa
← Selected work

Case study

Onta Trips: one codebase for travellers and travel agencies

A production travel platform where consumers search and book flights, and agencies manage bookings, staff and clients from a dashboard, both served by a single Next.js application.

Visit Onta Trips ↗

Role
Lead Frontend Developer
Timeframe
Dec 2025 – Present
Product
B2C booking site + B2B agency dashboard
Stack
Next.js 16, React 19, TypeScript, Tailwind CSS v4

My part

I lead the frontend on Onta Trips, an independent engagement alongside my role at Gurzu. I built the B2C and B2B agency booking experience from a single shared Next.js codebase with multi-tenant configuration, using App Router route groups, JWT-based role access and feature-level permission gating.

Within that architecture I own the flight booking workflow end to end: search, fare selection, reservation and ticketing.

One app, two portals

Consumers and agencies use the same Next.js App Router codebase. B2C users book for themselves. B2B accounts have sub-roles (admin, agent and client) with different levels of access to the agency dashboard.

Sharing one codebase means shared types, services and UI, with the tenant type deciding which shell, navigation and permissions apply.

Search and booking flows

Flight search and booking are form-heavy: passenger details, itineraries and validation rules that change with the trip. Forms are built with React Hook Form and Zod schemas, so validation is declarative and typed end to end. Cross-step state lives in Zustand stores.

Authentication without exposed tokens

The JWT is stored in an HttpOnly cookie and is never kept in localStorage or anywhere else client-side JavaScript can read. Cookies are set and cleared on the server, and the client asks a session endpoint for its auth state.

The browser sends requests with an HttpOnly cookie to a Next.js proxy route, which reads the cookie and forwards the request to the backend with a Bearer token.Browserno token in JS/api/proxy/[...path]Next.js route handlerBackend APIexpects Bearer tokenCookie: auth_tokenAuthorization: Bearer …
The proxy is the only path to the backend. The token lives in an HttpOnly cookie, so client-side JavaScript can never read it.

Every API call goes through a Next.js route handler that reads the cookie and adds the Bearer header before forwarding to the backend. Middleware only does coarse redirects; access checks happen in the app shell.

Data fetching and caching

Server data is managed with TanStack Query. A shared Axios instance handles the auth header and converts the backend’s snake_case responses to camelCase, so components only ever see one naming convention.

I resolved server-state issues involving request deduplication, cache invalidation and refetch correctness across the shared multi-tenant codebase.

Centralised permissions

B2B page access is defined in a single page-access map and checked in the dashboard shell, not with guards scattered across pages. Adding a route means adding one entry. In-page controls use a usePermissions() hook to show or hide actions based on full or view-only access.

Uploads, realtime and errors

File uploads use presigned URLs and go straight to Cloudflare R2, bypassing the backend. Notifications arrive in real time over ActionCable, and errors are reported to Sentry.

Conventions and code review

The codebase follows a strict layering: types, services, hooks, components, pages. Components never call services directly. Husky and lint-staged run ESLint, Prettier and the TypeScript compiler on every commit, with zero lint warnings allowed, and shared components are documented in Storybook.