Architecture Overview
Big picture
Section titled “Big picture”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)Two deployment targets
Section titled “Two deployment targets”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 point | worker.js via wrangler dev | worker.js |
| Database | Cloudflare D1 (local emulator, .wrangler/state/v3/d1/) | Cloudflare D1 (Cloudflare-hosted) |
| File storage | Local disk (/uploads/) | Cloudflare R2 |
| Node APIs | Restricted subset (Workers runtime) | Restricted subset (Workers runtime) |
| Secrets | wrangler.toml [vars] section | Cloudflare 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.
Request lifecycle
Section titled “Request lifecycle”1. Browser → POST /api/tickets2. 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 R24. Response → BrowserRole model
Section titled “Role model”There are three roles, stored in the users.role column:
| Role | Access |
|---|---|
USER | Public site, booking, own dashboard, KYC submission |
AGENT | Dashboard (scoped by agent type), ticket upload |
ADMIN | Full 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.
Technology choices
Section titled “Technology choices”| Layer | Choice | Why |
|---|---|---|
| Backend framework | Express | Simple, well-understood, compatible with serverless-http for Workers |
| Frontend framework | React 18 + Vite | Fast build, good TypeScript support |
| UI components | shadcn/ui (Radix) | Accessible, unstyled primitives, easy to customize |
| Styling | Tailwind CSS v4 | Utility-first, no runtime overhead |
| Auth | JWT (access + refresh) | Stateless, works in both environments without session store |
| ORM / query builder | None — raw SQL | Keeps 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 TABLEguards scattered across route files have no rollback capability. If the project grows, consider Drizzle ORM with Drizzle Kit for versioned, rollback-capable D1 migrations.