EEgypt7000Travel Live site
egypt7000travel.com · under the hood

How a trip to Egypt gets booked.

A multilingual tour-booking platform: a React front end, an ASP.NET Core API, SQL Server, and hosted card payments. Here is what happens behind each click.

UI languages
11
UI languages
step booking wizard
7
step booking wizard
API routes
170+
API routes
entity sets
40
entity sets
EF Core migrations
36
EF Core migrations
one request, end to endrequest →
Browser
React SPA
Edge
Vercel rewrite
API
ASP.NET Core 10
Database
SQL Server

01A visitor opens a tour package page

Scroll to look inside
01Front end

A React app in eleven languages

React 18, built with Vite. Tailwind holds the site's colour tokens (desert gold, Nile blue, sunset orange). Routing is React Router v6 and API data runs through TanStack Query. Public pages are prerendered as HTML for search engines, then hydrated in the browser.

egypt7000travel.com/package-catalog
React Router v6 · React.lazy40+ lazy pages
  • /
  • /package-catalog
  • /package-details/:id
  • /multi-step-booking/:id
  • /day-trips/:slug/book
  • /trip-cost-estimator
  • /dashboarduser
  • /admin-package-managementadmin
react-vendor.jsui-vendor.js+ one chunk per page
01 / 04

Pages load only when they're needed

Every page is a React.lazy chunk behind a Suspense fallback. Pages that need an account are wrapped in ProtectedRoute: the dashboard, profile and inquiry tracking need a signed-in user, and the 20+ admin screens need the admin role. Old URLs like /packages redirect to their canonical page.

React 18Vite 5React Router 6Tailwind CSS 3
02 / 04

TanStack Query holds the API data

One QueryClient (60-second stale time, one retry). Every content query key includes the language, so English and Arabic results never mix in the cache. A shared axios instance sends cookies on every call. A server error (5xx) shows a toast. A 401 clears the cached user and returns them to sign-in.

@tanstack/react-query 5axioswithCredentials
03 / 04

Prerendered, then hydrated

At build time a script fetches every package. Puppeteer then renders each public route to static HTML and embeds the package data in the page. In the browser that data fills the query cache before hydrateRoot runs, so the first paint is real content and the page doesn't refetch.

puppeteerhydrateRootreact-helmet-async
04 / 04

Eleven languages, one in the bundle

English ships in the JavaScript bundle. The other ten (es, pt, tr, ar, fr, de, it, nl, ru, pl) load from the API, which fills gaps with DeepL and caches the results in SQL Server. Hand-written SEO headings for nine of the languages are locked, so machine translation can't overwrite them. Choosing Arabic sets dir="rtl" on <html> and the layout mirrors.

i18nextreact-i18nextDeepLRTL
02API

ASP.NET Core 10, layered and locked down

A .NET 10 minimal-API backend split into Domain, Application and Infrastructure projects, with EF Core and SQL Server underneath. The routes are documented in a hand-written reference in the repo.

Clean Architecture · .NET 104 projects
Endpoints
Minimal APIs grouped by feature · 170+ routes
MapGroupRequireRateLimitingCacheOutput
Application
MediatR commands & queries, validated in a pipeline
LoginCommandCreateBookingCommandValidationBehavior<,>
Domain
40 entities, no framework dependencies
PackageBookingOrderDayTripTranslationValue
Infrastructure
EF Core 10 on SQL Server + external services
MailKitDeepLGoogle authGetPayInAzure OpenAI
01 / 03

Four projects, dependencies point inward

Endpoints send commands and queries through MediatR. FluentValidation runs as a pipeline step before any handler. Handlers depend only on interfaces. Infrastructure implements them with EF Core, MailKit, DeepL, Google sign-in, GetPayIn and Azure OpenAI (package Q&A). The domain project references no framework at all.

ASP.NET Core 10MediatRFluentValidationEF Core 10
02 / 03

Every request takes the same path

X-Forwarded-For is trusted only from known proxy IPs. In production the API won't start without that list, so nobody can fake their IP to dodge the rate limits. Next come CORS, JWT authentication, role checks, a CSRF guard, output caching and the rate limiter. Any exception comes back as the same JSON error shape.

Program.csForwardedHeadersCSRFOutputCache
03 / 03

A request budget for each route

Eight fixed-window rate limits run per IP, from 600 requests a minute overall down to 5 for login and the contact form. Translation routes get their own limit, because every cache miss spends DeepL credits. The request body size is capped low everywhere so ordinary routes can't be used to exhaust memory. Upload routes raise their own cap.

RateLimiterHybridCacheAPI_ROUTES.md
03Auth flow

Sign-in without exposing tokens to JavaScript

The browser calls the API through a /api proxy on the site's own domain, so the auth cookie is first-party. That's what stops iOS Safari from signing people out.

cookie-based JWTstep 1/4
Browser
React SPA
API
.NET 10
SQL Server
Users
Google
Identity
email + password
5 req/min · validate
find user
BCrypt hash
sign JWT · HS256 · 12 h
Set-Cookie: httpOnly
keep name only
cookie + X-Requested-With
profile + role
401 → sign in again
Google ID token
ID token
validate signature
same httpOnly cookie
01 / 04

Sign in

Login is limited to 5 requests a minute per IP. A MediatR LoginCommand is validated, the user is loaded through EF Core, and the password is checked against its BCrypt hash.

auth_strictLoginCommandBCrypt.Net
02 / 04

The token goes in an httpOnly cookie

The API signs an HS256 JWT that lasts 12 hours. Issuer, audience and expiry are all checked, with zero clock skew allowed. It's sent as an httpOnly, Secure cookie, so JavaScript can't read it. The app caches only display fields (name, email) in a separate cookie. The role is never cached; it always comes from the server.

HttpOnlySecureSameSite12 h
03 / 04

Every request after that

The JWT middleware reads the token from the cookie. Any cookie-authenticated request that changes data must also carry X-Requested-With, which a cross-site form can't add. On load the app asks the API for the profile; the role always comes from the server.

There is no refresh token. When the 12 hours run out, the next call returns 401: the app clears the cached user, signals the session ended, and sends the user to sign in again.

JwtBearerCsrfProtectioninterceptor
04 / 04

Or sign in with Google

Google Identity Services gives the browser an ID token. The API checks it with Google's own library and then sets the same httpOnly cookie, so everything after sign-in works the same way.

Google.Apis.AuthGoogleLoginCommand
04Booking & inquiry

From “this trip” to an inquiry on the agent's desk

A booking starts as an inquiry: the traveller builds the trip and the agency confirms it. The browser runs the wizard, the server checks the price again, and the agency gets an email.

multi-step booking · useReducer1/7
  1. Package Selection1
  2. Traveler Details2
  3. Date Selection3
  4. Room Configuration4
  5. Hotels & Accommodation5
  6. Supplements6
  7. Review & Confirm7
day trips · 6 steps
TypeDateDepartureLanguageGuestsReview
01 / 04

A seven-step wizard

Package, travellers, dates, rooms, hotels & cruises, supplements, review. The whole trip lives in one useReducer, so going back a step never loses anything. Day trips have their own six-step flow.

bookingReducerStepIndicatorday-trips/booking
02 / 04

Priced live in the browser

Single, double and triple rooms are priced per person. Seasonal adjustments, child and orphan discounts, private-service supplements, domestic flights and tax are applied as the traveller clicks, so the total is always visible.

packagePricing.jsReviewConfirmStep
03 / 04

The server re-prices it

Sending the inquiry is a single request. The API recalculates the total from the package data and rejects it if the numbers don't match. It then saves the booking, emails the agency through MailKit, and returns a guest token signed with an HMAC and valid for 14 days. Guests can pay without an account, and nobody can reach a booking by guessing its id.

BookingPriceCalculatorMailKitGuestAccessToken
04 / 04

Then pay now, or wait

The confirmation page offers a deposit or full payment straight away. Signed-in travellers can also follow their inquiry from the dashboard while the agency confirms it.

/payment-returncustomer-inquiry-tracking
05Trip cost estimator

The estimator is four steps of arithmetic

It's a planning tool, not a quote, and it runs entirely in the browser: no API call, no account. The calculator below uses the same constants as the live page.

  1. 1
    Pick a style

    Daily rates for hotel, food, transport and sightseeing: Budget $70, Mid-range $195, Luxury $600 per person.

  2. 2
    Multiply

    × days × travellers, one line per category.

  3. 3
    Add extras

    Per traveller: 4-night Nile cruise ($350 / $700 / $1,800 by style), round-trip Cairo ↔ Luxor flight ($240), e-visa ($25).

  4. 4
    Convert

    The live page shows 30+ currencies using ECB rates from Frankfurter, cached for 6 hours, with a built-in fallback for when it's offline.

try it · same formula as the live pageUSD
  • Accommodation$1,120
  • Food & drink$560
  • Local transport$350
  • Sightseeing & guides$700
  • Nile cruise (4 nights)$1,400
  • Cairo ↔ Luxor flight$480
  • Egypt e-visa$50
Estimated total
$4,660
per person
$2,330
06Payments · GetPayIn

Payments through GetPayIn, verified twice

Card details are entered on GetPayIn's hosted checkout page, never on this site. The API decides how much to charge and signs the request. A booking is only marked paid after a signed webhook arrives from GetPayIn.

order → checkout → webhookstep 1/4
Browser
customer
API
.NET 10
SQL Server
orders
GetPayIn
hosted checkout
create order · plan
load booking
amount set by server
order: pending
HMAC-sign init
init checkout
checkout URL
redirect
card entered on GetPayIn
signed webhook
verify HMAC-SHA256
order PAID · booking updated
back with signature
verify redirect
paid / deposit_paid
01 / 04

The server sets the price

There are three plans: deposit (a percentage the admin sets, 20 by default), full, and balance, which only opens once the deposit is paid. The API works out the amount from the booking. If the browser sends an amount more than a cent off, the request is rejected. If an unpaid order already exists for that booking, it's reused rather than duplicated.

BookingDepositHelperpaymentPlanOrder
02 / 04

A signed hand-off to hosted checkout

The API signs the request that starts the checkout with an HMAC. GetPayIn returns a checkout URL and the browser goes to GetPayIn's hosted page. Card details never reach this server. Payments can be in EGP, EUR or USD.

PaylinkServiceHMACEGP · EUR · USD
03 / 04

Only the webhook marks it paid

GetPayIn calls the API with an HMAC-SHA256 signature of the raw request body. If that header is missing, the API checks a signature field in the body instead, the same fallback GetPayIn's own plugin uses. Only after that check does the order switch to PAID. The booking's paid and due amounts then update, and its status becomes deposit_paid, or paid and confirmed.

webhookX-Paylink-SignatureAmountDue
04 / 04

The redirect is checked too

The customer comes back to /payment-return with a signed query string. The API verifies that signature before the page shows anything, so a tampered URL gets a verification error instead of a receipt.

verify-redirectpayment-return
07Launch

Shipping it: two hosts, one domain

The front end builds and deploys on Vercel. The API and SQL Server run on MonsterASP.NET under IIS. Visitors and search engines see everything at egypt7000travel.com.

deploy.log
front end → Vercel
$npm run build
›prebuild package ids from the API → snap.routes.json
›vite build react-vendor · ui-vendor · one chunk per page
✓postbuild Puppeteer prerendered public + package routes
✓rewrites /api · /uploads · /sitemap.xml → API (same origin)
API → MonsterASP.NET (IIS)
$./publish.ps1
›dotnet publish -c Release → ./publish
›app_offline.htm up → FTP mirror changed files → app_offline down
✓boot check JWT signing key present
✓boot check trusted proxies set (else refuse to start)
✓startup apply EF Core migrations · sync English i18n catalog
egypt7000travel.com → Google
›robots.txt Disallow /admin, /admin-*
›sitemap.xml generated from SQL Server packages
›<head> canonical · Open Graph · JSON-LD
✓Search Console sitemap submitted · prerendered HTML crawlable
01 / 03

Front end on Vercel

Each build lists the current packages, bundles with Vite and prerenders the public pages. Vercel rewrites keep /api, uploaded images and the sitemap on the main domain. The sign-in pages send a Cross-Origin-Opener-Policy header that lets the Google sign-in popup work.

vercel.jsonprerender.mjsCOOP
02 / 03

API and database on MonsterASP.NET

A PowerShell script builds a Release publish and uploads only the changed files over FTP. During the upload an app_offline.htm takes the site offline briefly so IIS can swap the files cleanly. Secrets come from the host's environment variables. The app won't start without a JWT key or the proxy list, and it applies EF Core migrations when it boots.

publish.ps1IISEF Core migrations
03 / 03

Making it findable on Google

robots.txt keeps crawlers out of the admin pages. The API generates sitemap.xml from the packages in SQL Server. Pages set canonical and Open Graph tags, plus JSON-LD structured data where it applies. Because the pages are prerendered, crawlers get the content without running JavaScript. The sitemap is submitted in Google Search Console.

robots.txtsitemap.xmlJSON-LDSearch Console
The stack

One developer, front to back.

Design system, SPA, API, database, payments, translations, deployment and SEO, all live at egypt7000travel.com.

Visit egypt7000travel.com
Front end
React 18Vite 5Tailwind CSS 3React Router 6TanStack Query 5i18nextFramer MotionLeafletRechartsaxios
Back end
ASP.NET Core 10EF Core 10SQL ServerMediatRFluentValidationJWTBCryptHybridCache
Services
GetPayInDeepLGoogle Sign-InMailKitAzure OpenAIFrankfurter (ECB)
Ship
VercelMonsterASP.NET (IIS)Puppeteer prerenderGoogle Search Console