โ† Architecture Patterns 18 / 18 ๐Ÿ  Home

Putting It All Together

Module 6 ยท Quality ยท โฑ ~40 min
Final module: The complete system diagram, the full booking trace from button click to LINE notification, a component responsibility map, a glossary, and 20 final questions covering all modules.

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

FileResponsible for
backend/app/main.pyFastAPI app, CORS, router wiring, lifespan
backend/app/core/config.pyAll config from environment variables
backend/app/core/security.pybcrypt + JWT โ€” passwords and tokens
backend/app/database.pyAsync DB engine, session factory, get_db dependency
backend/app/models/SQLAlchemy ORM table definitions
backend/app/schemas/public.pyPydantic I/O models for public API
backend/app/schemas/admin.pyPydantic 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.pyOmise webhook handler โ€” confirms bookings on payment
backend/app/services/slots.pycompute_slots() โ€” pure slot algorithm
backend/app/services/availability.pyDB-level availability: fetch schedule+bookings โ†’ call compute_slots
backend/app/services/omise.pycreate_promptpay_charge() + verify_webhook_signature()
backend/app/services/line_notify.pypush_text() + message builder functions
backend/app/services/line_login.pyexchange_code() โ€” LINE OAuth2
backend/app/tasks/scheduler.pyAPScheduler: expiry task (5min) + reminder task (30min)
frontend/proxy.tsNext.js middleware: subdomain rewrite + admin auth guard
frontend/lib/types.tsTypeScript interfaces mirroring Pydantic schemas
frontend/app/page.tsxMarketplace homepage โ€” lists all shops
frontend/app/shop/[slug]/page.tsxShop 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/CaddyfileVirtual hosts, TLS, reverse_proxy directives
docker-compose.ymlpostgres + fastapi + nextjs containers, networks, volumes
migrations/Alembic schema migrations โ€” applied with alembic upgrade head
seed/dummy_shops.pySeeds 4 demo shops with resources, services, schedules

Glossary

TermDefinition
ACMEAutomatic Certificate Management Environment โ€” protocol between Caddy and Let's Encrypt for auto-issuing TLS certs
APSchedulerPython library that runs async functions on a schedule (expiry every 5min, reminders every 30min)
ASGIAsynchronous Server Gateway Interface โ€” the Python async web server standard that FastAPI and uvicorn use
async/awaitPython keywords for writing async code. async def creates a coroutine; await pauses execution until an I/O operation completes
bcryptPassword hashing algorithm โ€” salted, deliberately slow, cannot be reversed
CaddyReverse proxy and web server with automatic HTTPS certificate management
CheckConstraintSQLAlchemy/SQL construct that enforces a condition at the database level (e.g., status IN ('pending_payment', 'confirmed', 'cancelled'))
CORSCross-Origin Resource Sharing โ€” browser security mechanism; FastAPI's CORSMiddleware grants permission for jongyang.xyz to call api.jongyang.xyz
DeclarativeBaseSQLAlchemy 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
DockerContainer platform โ€” packages app + dependencies into an image that runs identically anywhere
Event loopThe asyncio scheduler that runs on a single thread and switches between coroutines whenever one awaits I/O
FastAPIPython 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 keyA column that references the primary key of another table, enforcing referential integrity
HMAC-SHA256Hash-based Message Authentication Code โ€” used for Omise webhook signature verification
httpOnly cookieA cookie that JavaScript cannot read โ€” prevents XSS attacks from stealing the admin JWT
JWTJSON Web Token โ€” header.payload.signature, signed with HS256, used for stateless admin authentication
LINE LoginOAuth2 identity provider โ€” lets customers authenticate via their LINE account, giving us their LINE user ID
LINE Messaging APILets 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
MigrationAlembic script that describes how to change the DB schema from one version to the next โ€” applied with alembic upgrade head
Multi-tenancyOne running application serving multiple independent customers (shops), with row-level data isolation via shop_id
Next.js middlewareproxy.ts โ€” runs on every request before page rendering; handles subdomain routing and admin auth guard
OAuth2Industry-standard protocol for "login with provider" flows (LINE Login). Authorization code โ†’ access token โ†’ user ID
ORMObject-Relational Mapper โ€” SQLAlchemy maps Python classes to DB tables so you work with objects instead of raw SQL
pydanticPython data validation library โ€” FastAPI uses it for request/response schemas; BaseModel with type annotations
pydantic-settingsExtension for loading config from environment variables and .env files; list[str] fields require JSON array format
PromptPayThailand's national instant bank transfer system โ€” customers pay by scanning a QR code with their banking app
Reverse proxyServer that sits in front of other servers, routing requests by hostname and terminating TLS
SQLAlchemyPython SQL toolkit and ORM โ€” used in async mode (asyncpg driver) with PostgreSQL in production, aiosqlite in tests
TLS/HTTPSTransport Layer Security โ€” encrypts data between browser and server; required for LINE Login, Omise, and browser cookies
TypeScriptJavaScript with static types โ€” catches type errors before running; frontend interfaces mirror Python Pydantic schemas
UUIDUniversally Unique Identifier โ€” random 128-bit ID used as primary key; can't be guessed, safe for multi-server
uvicornASGI 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)
๐ŸŽ‰ You've completed all 18 modules. The final quiz below covers one question from each module. If you can answer all 20 without looking, you understand the full stack.

๐ŸŽ“ Final Quiz โ€” 20 Questions

1. [Web] A booking returns HTTP 409. What does that mean and what caused it?

409 Conflict โ€” the request conflicts with the current state of the resource. It means the requested time slot is already taken (another confirmed or pending_payment booking exists for that resource/date/time). It's not a client input error (400) and not a server error (500) โ€” it's a logical conflict in the business state.

2. [Async] Why can a single FastAPI worker handle 100 simultaneous availability requests without 100 threads?

The asyncio event loop switches between coroutines whenever one hits 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?

FastAPI calls get_current_admin before the route runs. It extracts the Bearer token from the Authorization header, calls decode_access_token() which verifies the signature and checks expiry, then fetches the ShopAdmin from the DB. If the JWT is expired, python-jose raises JWTError, get_current_admin raises HTTPException(401), and the route never runs.

4. [Database] What is the resource_services table and why does it exist?

It's a junction table implementing the Many-to-Many relationship between staff resources and services. A staff member (Khun Nid) can offer many services (Bath, Full Groom). A service (Bath) can be offered by many staff members. SQL can't represent M:M directly โ€” it needs a junction table with two foreign keys: (resource_id, service_id). The composite primary key ensures the same link isn't stored twice.

5. [Alembic] You add a column to a SQLAlchemy model. What must you do for the production database to gain that column?

Create a migration: 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?

Slowness is a security feature. An attacker trying to brute-force a password by testing millions of combinations is slowed down dramatically โ€” if each check takes 100ms, 1 million attempts take 27 hours. A legitimate login doing one bcrypt check takes ~100ms โ€” imperceptible to a human but catastrophic for brute-force attacks. The cost factor can be increased as hardware improves.

7. [Payments] What is a webhook? Why does Omise use one instead of us polling?

A webhook is an HTTP callback โ€” Omise POSTs to our endpoint when a payment completes. Polling would require our server to repeatedly ask "did it complete?" wasting resources and adding latency. Webhooks are instant (milliseconds after payment), efficient, and don't require us to maintain a polling loop. "Don't call us, we'll call you."

8. [LINE] After a customer connects LINE, what is stored in the database and how is it used later?

The LINE user ID (e.g., U4af4980629...) is stored in booking.customer_line_user_id. It's used when sending push messages: the Omise webhook handler calls push_text(booking.customer_line_user_id, msg_booking_confirmed(...)) to send the confirmation. The reminder task also uses it for T-24h and T-2h reminders โ€” only bookings with a non-null user_id get reminders.

9. [React] What happens when setBookings(data) is called in a React component?

React schedules a re-render. The component function is called again with the new value of bookings. React compares the new JSX output to the previous output (reconciliation/diffing) and updates only the DOM nodes that actually changed. The process is synchronous once triggered but doesn't block the browser โ€” React batches updates efficiently.

10. [Next.js] What's the difference between a Server Component and a Client Component? Which one can use useState?

Server components run on the Node.js server, can await data directly, and don't ship JS to the browser. Client components run in the browser, can use hooks and browser APIs, and must be marked 'use client'. Only client components can use useState โ€” useState is a browser-side React hook that doesn't exist in the server environment.

11. [TypeScript] What does string | null mean for a field like customer_line_user_id: string | null?

The field can be either a string (when the customer has connected their LINE account, containing the LINE user ID) or null (when they haven't connected yet). TypeScript enforces that you check which it is before using string-specific methods โ€” preventing the runtime error "Cannot read properties of null".

12. [Tailwind] What does hover:bg-amber-200 do and when does it apply?

It applies background-color: amber-200 (#fde68a) only when the user hovers over the element. The hover: prefix is a state variant โ€” it generates a CSS rule with :hover selector. Without hovering, the class has no effect. Used on the Confirm (Demo) button to darken the background on hover, giving visual feedback.

13. [Docker] What is a multi-stage Docker build and why does the Next.js Dockerfile use it?

A multi-stage build uses multiple FROM instructions. Each stage can copy files from previous stages. The Next.js Dockerfile has three stages: deps (npm install), builder (npm run build), runner (only the compiled output). The final image only contains .next output + node_modules โ€” not source code or build tools. This shrinks the image from ~800MB to ~250MB and removes attack surface.

14. [Docker Compose] Why does FastAPI use depends_on: postgres: condition: service_healthy?

PostgreSQL takes 1-2 seconds to initialize even after its container starts. Without the health check, Docker starts FastAPI immediately, FastAPI tries to connect to PostgreSQL, gets "connection refused", and crashes. The health check (pg_isready) polls PostgreSQL until it's accepting connections, then Docker starts FastAPI. The service_healthy condition waits for 5 consecutive successful health checks.

15. [Caddy] How does Caddy automatically get a TLS certificate for jongyang.xyz without any manual configuration?

ACME HTTP-01 challenge. Caddy contacts Let's Encrypt, which issues a random token and asks Caddy to serve it at http://jongyang.xyz/.well-known/acme-challenge/TOKEN. Let's Encrypt then makes an HTTP request to verify the token. Since only the real server can serve that URL (DNS must point to our server), this proves domain control. Let's Encrypt issues the certificate; Caddy stores it and handles renewals automatically.

16. [Testing] Why do tests use in-memory SQLite instead of mocking the database or using real PostgreSQL?

Not PostgreSQL: requires Docker to be running, is slow to start, and needs cleanup between tests. Not mocks: mocks lie โ€” they return hardcoded values, not real SQL results; a previous project's mocked tests passed while a real migration failed in production. SQLite in-memory: always available, starts in milliseconds, runs real SQL so we catch real SQL errors, and is automatically destroyed after each test leaving no state.

17. [Architecture] How does JongYang prevent BKK Sports Club's admin from seeing Serenity Spa's bookings?

The JWT contains shop_id at login time. get_current_admin() decodes the JWT and returns a ShopAdmin with their shop_id. Every admin query adds .where(Booking.shop_id == admin.shop_id). Even if an admin crafted a malicious request for another shop's data, the WHERE clause would return empty results โ€” the database enforces isolation.

18. [Architecture] What is the booking state machine? Name all three states and how transitions happen.

pending_payment: created when customer submits booking form. confirmed: when Omise webhook fires (charge.complete, status=successful) OR when admin clicks Confirm (Demo). cancelled: when expires_at passes with no payment (APScheduler expiry task runs every 5 minutes) OR explicitly by admin. A booking can never go from confirmed back to pending_payment or cancelled (business logic enforced in the webhook handler with an explicit status check).

19. [Architecture] Why must NEXT_PUBLIC_* env vars be set before docker compose build, not after?

NEXT_PUBLIC_* variables are baked into the JavaScript bundle at build time (when npm run build runs). The values are replaced as string literals in the compiled JS. The running container doesn't read them from the environment at startup โ€” they're already compiled in. Changing them in .env after the image is built has zero effect. You must rebuild with docker compose build nextjs.

20. [Everything] Trace what happens when an admin at bkk@demo.com logs into admin.jongyang.xyz โ€” from form submit to seeing the dashboard.

1. Browser POSTs email+password to Next.js: POST /api/admin/login (JSON body)
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