Skip to content

Architecture Overview

Shanvi Travels is a travel agency management platform with two user-facing surfaces: a public website (home, flight search, booking) and a protected dashboard (admin/agent operations).

Browser
├── frontend (React + Vite)
│ ├── Public pages (home, search, submissions, VFS tracking)
│ └── Protected dashboard (admin, agent, user views)
└── backend (Express)
├── REST API (/api/*)
├── Static file serving (/uploads/* — local dev only)
└── DB: Cloudflare D1 (both local and production)
File storage: local disk (dev) ↔ Cloudflare R2 (production)

The same Express codebase runs in two environments. Database is D1 in both — the only difference is file storage and how secrets are provided.

Local (wrangler dev)Cloudflare Workers (production)
Entry pointworker.js via wrangler devworker.js
DatabaseCloudflare D1 (local emulator, .wrangler/state/v3/d1/)Cloudflare D1 (Cloudflare-hosted)
File storageLocal disk (/uploads/)Cloudflare R2
Node APIsRestricted subset (Workers runtime)Restricted subset (Workers runtime)
Secretswrangler.toml [vars] sectionCloudflare Secrets (wrangler secret put)

wrangler dev injects D1_DB into globalThis from the local emulator, so the code path is identical to production. There is no MySQL dependency.

config/db.js still contains a MySQL pool fallback for when D1_DB is absent, but this code is effectively dead now. It can be removed in a cleanup pass. See the Database Abstraction doc for details.

1. Browser → POST /api/tickets
2. Express middleware stack:
a. helmet (security headers)
b. cors (origin allowlist)
c. express.json (body parsing)
d. DB init middleware (runs migrations on first request)
3. Route handler (routes/tickets.js)
a. authenticateToken (verify JWT, attach req.user)
b. Business logic
c. db.run() / db.get() / db.all() → MySQL or D1
d. saveFile() → local disk or R2
4. Response → Browser

There are three roles, stored in the users.role column:

RoleAccess
USERPublic site, booking, own dashboard, KYC submission
AGENTDashboard (scoped by agent type), ticket upload
ADMINFull access — user management, all submissions, all tickets

Agents have a sub-type (agentType) that determines which systems and permissions they have. Agent types are fully configurable from the Admin dashboard. See Agent Types & Permissions.

LayerChoiceWhy
Backend frameworkExpressSimple, well-understood, compatible with serverless-http for Workers
Frontend frameworkReact 18 + ViteFast build, good TypeScript support
UI componentsshadcn/ui (Radix)Accessible, unstyled primitives, easy to customize
StylingTailwind CSS v4Utility-first, no runtime overhead
AuthJWT (access + refresh)Stateless, works in both environments without session store
ORM / query builderNone — raw SQLKeeps the dual-DB abstraction simple; ORMs add complexity when targeting two different SQL dialects

A note on the ORM decision: Raw SQL is fine for this scale. Since the project now uses D1 (SQLite dialect) in both environments, the cross-dialect hazard is gone. The remaining concern is schema migration management — ALTER TABLE guards scattered across route files have no rollback capability. If the project grows, consider Drizzle ORM with Drizzle Kit for versioned, rollback-capable D1 migrations.