Putting It All Together
Complete System Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JONGYANG SYSTEM โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
EXTERNAL (Internet / Mobile)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Customer's Phone Admin's Browser โ
โ LINE App (notifications) admin.jongyang.xyz โ
โ Banking App (PromptPay QR) bkk@demo.com / Demo1234! โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
โ HTTPS :443 โ HTTPS :443
โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโ
โ CADDY (Docker container: caddy_proxy project) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ *.jongyang.xyz โ nextjs:3000 api.jongyang.xyz โ fastapi:8000โ โ
โ โ TLS termination via Let's Encrypt ACME (HTTP-01 challenge) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโฆโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฆโโโโโโโโโโโโโโโโโโโโโ
โ caddy-net โ caddy-net
โโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโ
โ NEXT.JS :3000 โ โ FASTAPI :8000 โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ proxy.ts middleware โ โ โ โ /api/shops โ โ
โ โ โข subdomain rewrite โ โ โ โ /api/availability โ โ
โ โ โข admin auth guard โ โ โ โ /api/bookings (POST) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโค โ โ โ /api/admin/* (JWT auth) โ โ
โ โ app/shop/[slug] โโโโซโโโซโโโ /webhooks/omise โ โ
โ โ app/admin/(panel) โ โ โ โ /health โ โ
โ โ app/api/admin/proxy/* โโโโซโโโซโโบโ APScheduler background jobs โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโซโโโโโโโโโโโโโโโโโโโ
โ internal network
โโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโ
โ POSTGRESQL :5432 โ
โ shops, resources, services โ
โ base_schedules, bookings โ
โ shop_admins โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
EXTERNAL APIs (called by FastAPI):
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Omise API โ โ LINE APIs โ
โ api.omise.co โ โ access.line.me (OAuth2 Login) โ
โ PromptPay QR โโโโโโ api.line.me (Messaging push) โ
โ charge creation โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ sends webhook โ
โ to our server โ
โโโโโโโโโโโโโโโโโโโโ
End-to-End: Customer Clicks "เธเธญเธเนเธฅเธข"
Following one booking from first click to LINE notification:
STEP 1: Shop page loads
Customer โ HTTPS โ Caddy โ nextjs:3000
proxy.ts: hostname=bkk-sports-club.jongyang.xyz โ rewrite /shop/bkk-sports-club
Next.js server component: fetch /api/shops/bkk-sports-club โ renders shop page
Browser shows: Court A (เธฟ150/hr), Court B (เธฟ150/hr), Court C (เธฟ300/hr)
STEP 2: Customer clicks "เธเธญเธเนเธฅเธข" on Court A
Link href="/shop/bkk-sports-club/book?resource_id=02553923-..."
proxy.ts: path starts with /shop/ โ skip rewrite โ serve book page directly
BookingFlow.tsx (client component) mounts
STEP 3: Multi-step booking wizard (all client-side state)
Step 1: Select duration (1hr = เธฟ150, 1.5hr = เธฟ225, 2hr = เธฟ300)
Step 2: Select date โ calendar renders tomorrow's dates
Step 3: Select time โ React fetches: GET /api/availability/02553923...?date=2026-06-25&duration_min=60
FastAPI: check BaseSchedule (7:00โ22:00) + existing bookings โ returns ["07:00","08:00",...,"21:00"]
15 time slots appear
Step 4: Customer types name + phone
Step 5: Review screen shows all details
STEP 4: Customer clicks "เธขเธทเธเธขเธฑเธ & เธเธณเธฃเธฐเนเธเธดเธ"
React POSTs to FastAPI: POST /api/bookings
Body: {resource_id, booking_date, start_time, duration_min, customer_name, customer_phone}
STEP 5: FastAPI create_booking() runs
1. validate resource exists and is active (DB query)
2. validate shop exists and is active (DB query)
3. resource_type == "asset" โ duration=60, price=60/60*150=เธฟ150
4. check booking_date is not in the past
5. get_available_slots() โ query BaseSchedule + existing Bookings
6. "07:00" in available slots? YES
7. INSERT booking (status="pending_payment", expires_at=now+15min)
8. COMMIT (booking exists in DB)
9. call Omise API: POST api.omise.co/charges โ amount_satang=15000
10. Omise returns: {charge_id: "chrg_test_...", qr_url: "https://..."}
11. UPDATE booking.omise_charge_id, COMMIT
12. return 201 JSON with qr_url
STEP 6: Browser shows QR + countdown
BookingFlow renders QR image (fetched from Omise URL) + 15-minute countdown timer
Customer opens banking app, scans QR, confirms payment of เธฟ150
(timer counts down: 14:57... 14:56...)
STEP 7: Omise sends webhook
Omise POST โ https://api.jongyang.xyz/webhooks/omise
Headers: Omise-Signature, Omise-Signature-Timestamp
FastAPI: verify_webhook_signature() โ HMAC-SHA256 check โ VALID
event.key == "charge.complete" AND charge.status == "successful"
UPDATE booking.status = "confirmed", confirmed_at = now(), COMMIT
STEP 8: LINE notification (if customer connected LINE)
FastAPI: push_text(booking.customer_line_user_id, msg_booking_confirmed(...))
POST api.line.me/v2/bot/message/push โ customer's LINE app shows:
โ
เธขเธทเธเธขเธฑเธเธเธฒเธฃเธเธญเธเนเธฅเนเธง!
เธฃเนเธฒเธ: BKK เธชเธเธญเธฃเนเธเธชเน เธเธฅเธฑเธ
เธงเธฑเธเธเธตเน: 25/06/2026 เนเธงเธฅเธฒ: 07:00
เธซเธกเธฒเธขเนเธฅเธเธเธญเธ: ...427f1e0f
Component Responsibility Map
| File | Responsible for |
|---|---|
| backend/app/main.py | FastAPI app, CORS, router wiring, lifespan |
| backend/app/core/config.py | All config from environment variables |
| backend/app/core/security.py | bcrypt + JWT โ passwords and tokens |
| backend/app/database.py | Async DB engine, session factory, get_db dependency |
| backend/app/models/ | SQLAlchemy ORM table definitions |
| backend/app/schemas/public.py | Pydantic I/O models for public API |
| backend/app/schemas/admin.py | Pydantic I/O models for admin API |
| backend/app/routers/public/ | Public HTTP endpoints (shops, availability, bookings) |
| backend/app/routers/admin/ | Admin HTTP endpoints (auth, resources, services, bookings) |
| backend/app/routers/webhooks/omise.py | Omise webhook handler โ confirms bookings on payment |
| backend/app/services/slots.py | compute_slots() โ pure slot algorithm |
| backend/app/services/availability.py | DB-level availability: fetch schedule+bookings โ call compute_slots |
| backend/app/services/omise.py | create_promptpay_charge() + verify_webhook_signature() |
| backend/app/services/line_notify.py | push_text() + message builder functions |
| backend/app/services/line_login.py | exchange_code() โ LINE OAuth2 |
| backend/app/tasks/scheduler.py | APScheduler: expiry task (5min) + reminder task (30min) |
| frontend/proxy.ts | Next.js middleware: subdomain rewrite + admin auth guard |
| frontend/lib/types.ts | TypeScript interfaces mirroring Pydantic schemas |
| frontend/app/page.tsx | Marketplace homepage โ lists all shops |
| frontend/app/shop/[slug]/page.tsx | Shop detail page โ shows resources and booking buttons |
| frontend/app/shop/[slug]/book/ | 5-step booking wizard |
| frontend/app/admin/(panel)/ | Admin dashboard, bookings, resources, schedules |
| frontend/app/api/admin/proxy/ | Secure proxy โ adds JWT from cookie to backend calls |
| caddy_proxy/Caddyfile | Virtual hosts, TLS, reverse_proxy directives |
| docker-compose.yml | postgres + fastapi + nextjs containers, networks, volumes |
| migrations/ | Alembic schema migrations โ applied with alembic upgrade head |
| seed/dummy_shops.py | Seeds 4 demo shops with resources, services, schedules |
Glossary
| Term | Definition |
|---|---|
| ACME | Automatic Certificate Management Environment โ protocol between Caddy and Let's Encrypt for auto-issuing TLS certs |
| APScheduler | Python library that runs async functions on a schedule (expiry every 5min, reminders every 30min) |
| ASGI | Asynchronous Server Gateway Interface โ the Python async web server standard that FastAPI and uvicorn use |
| async/await | Python keywords for writing async code. async def creates a coroutine; await pauses execution until an I/O operation completes |
| bcrypt | Password hashing algorithm โ salted, deliberately slow, cannot be reversed |
| Caddy | Reverse proxy and web server with automatic HTTPS certificate management |
| CheckConstraint | SQLAlchemy/SQL construct that enforces a condition at the database level (e.g., status IN ('pending_payment', 'confirmed', 'cancelled')) |
| CORS | Cross-Origin Resource Sharing โ browser security mechanism; FastAPI's CORSMiddleware grants permission for jongyang.xyz to call api.jongyang.xyz |
| DeclarativeBase | SQLAlchemy 2.x base class โ all ORM models inherit from it |
| Depends() | FastAPI dependency injection โ declares what a route needs (DB session, current admin); FastAPI creates and cleans up dependencies automatically |
| Docker | Container platform โ packages app + dependencies into an image that runs identically anywhere |
| Event loop | The asyncio scheduler that runs on a single thread and switches between coroutines whenever one awaits I/O |
| FastAPI | Python async web framework โ type hints drive validation, serialization, and auto-generated OpenAPI docs |
| Fixture (pytest) | Reusable setup/teardown function for tests; declared with @pytest_asyncio.fixture and injected by name into test functions |
| Foreign key | A column that references the primary key of another table, enforcing referential integrity |
| HMAC-SHA256 | Hash-based Message Authentication Code โ used for Omise webhook signature verification |
| httpOnly cookie | A cookie that JavaScript cannot read โ prevents XSS attacks from stealing the admin JWT |
| JWT | JSON Web Token โ header.payload.signature, signed with HS256, used for stateless admin authentication |
| LINE Login | OAuth2 identity provider โ lets customers authenticate via their LINE account, giving us their LINE user ID |
| LINE Messaging API | Lets our server push text messages directly to LINE users (booking confirmations, reminders) |
| Mapped[T] | SQLAlchemy 2.x type annotation for ORM columns โ Mapped[str] means a non-null string column; Mapped[str | None] means nullable |
| Migration | Alembic script that describes how to change the DB schema from one version to the next โ applied with alembic upgrade head |
| Multi-tenancy | One running application serving multiple independent customers (shops), with row-level data isolation via shop_id |
| Next.js middleware | proxy.ts โ runs on every request before page rendering; handles subdomain routing and admin auth guard |
| OAuth2 | Industry-standard protocol for "login with provider" flows (LINE Login). Authorization code โ access token โ user ID |
| ORM | Object-Relational Mapper โ SQLAlchemy maps Python classes to DB tables so you work with objects instead of raw SQL |
| pydantic | Python data validation library โ FastAPI uses it for request/response schemas; BaseModel with type annotations |
| pydantic-settings | Extension for loading config from environment variables and .env files; list[str] fields require JSON array format |
| PromptPay | Thailand's national instant bank transfer system โ customers pay by scanning a QR code with their banking app |
| Reverse proxy | Server that sits in front of other servers, routing requests by hostname and terminating TLS |
| SQLAlchemy | Python SQL toolkit and ORM โ used in async mode (asyncpg driver) with PostgreSQL in production, aiosqlite in tests |
| TLS/HTTPS | Transport Layer Security โ encrypts data between browser and server; required for LINE Login, Omise, and browser cookies |
| TypeScript | JavaScript with static types โ catches type errors before running; frontend interfaces mirror Python Pydantic schemas |
| UUID | Universally Unique Identifier โ random 128-bit ID used as primary key; can't be guessed, safe for multi-server |
| uvicorn | ASGI web server that runs the FastAPI app; receives HTTP requests and calls async route handlers |
| Webhook | "Don't call us, we'll call you" โ external service (Omise) POSTs to our URL when an event happens (payment complete) |
๐ Final Quiz โ 20 Questions
1. [Web] A booking returns HTTP 409. What does that mean and what caused it?
2. [Async] Why can a single FastAPI worker handle 100 simultaneous availability requests without 100 threads?
await. While request A waits for a 10ms database query, the loop processes requests B, C, D... When A's query returns, A resumes. Most time in web servers is waiting for I/O, not computing โ so one thread can interleave hundreds of requests efficiently.3. [FastAPI] What does Depends(get_current_admin) in a route signature do? What happens if the JWT is expired?
4. [Database] What is the resource_services table and why does it exist?
5. [Alembic] You add a column to a SQLAlchemy model. What must you do for the production database to gain that column?
alembic revision --autogenerate -m "add column name", review the generated file, then deploy and run alembic upgrade head. Changing the Python model has zero effect on the live database โ Alembic migrations are the mechanism for changing the actual schema.6. [Auth] Why is bcrypt slow on purpose? Doesn't that make login slow?
7. [Payments] What is a webhook? Why does Omise use one instead of us polling?
8. [LINE] After a customer connects LINE, what is stored in the database and how is it used later?
9. [React] What happens when setBookings(data) is called in a React component?
10. [Next.js] What's the difference between a Server Component and a Client Component? Which one can use useState?
11. [TypeScript] What does string | null mean for a field like customer_line_user_id: string | null?
12. [Tailwind] What does hover:bg-amber-200 do and when does it apply?
13. [Docker] What is a multi-stage Docker build and why does the Next.js Dockerfile use it?
14. [Docker Compose] Why does FastAPI use depends_on: postgres: condition: service_healthy?
15. [Caddy] How does Caddy automatically get a TLS certificate for jongyang.xyz without any manual configuration?
16. [Testing] Why do tests use in-memory SQLite instead of mocking the database or using real PostgreSQL?
17. [Architecture] How does JongYang prevent BKK Sports Club's admin from seeing Serenity Spa's bookings?
18. [Architecture] What is the booking state machine? Name all three states and how transitions happen.
19. [Architecture] Why must NEXT_PUBLIC_* env vars be set before docker compose build, not after?
20. [Everything] Trace what happens when an admin at bkk@demo.com logs into admin.jongyang.xyz โ from form submit to seeing the dashboard.
2. Next.js API route converts to form-data: username=bkk@demo.com&password=Demo1234!
3. Next.js POSTs to FastAPI: POST /api/admin/auth/login
4. FastAPI: find ShopAdmin by email โ verify bcrypt โ create JWT {sub: admin_id, shop_id: bkk_shop_id, exp: +24h}
5. FastAPI returns {access_token: "eyJ..."}
6. Next.js sets httpOnly cookie: admin_token = JWT (browser JS can't read this)
7. Next.js returns {ok: true} to browser
8. Browser: useRouter().push('/admin/dashboard')
9. Request to admin.jongyang.xyz/admin/dashboard
10. proxy.ts: pathname starts with /admin โ hasToken = true (cookie exists) โ NextResponse.next()
11. Admin dashboard renders, client component calls /api/admin/proxy/dashboard
12. Proxy route reads cookie server-side, adds Authorization: Bearer eyJ...
13. FastAPI GET /api/admin/dashboard โ get_current_admin decodes JWT โ returns stats for BKK's shop only