This document provides a high-level overview of the Teachly educational material marketplace architecture. It covers the overall system design, main use cases, and request flows.
For detailed implementation guidelines, see:
- Frontend AGENTS.md - React + Vite specifics
- Backend AGENTS.md - NestJS + TypeORM specifics
Teachly is a full-stack marketplace platform where teachers can upload, browse, and purchase educational materials (PDFs). The system follows a microservices-inspired architecture with clear separation of concerns.
Core Actors:
- Teachers/Refendars - Upload, browse, and buy teaching materials
- Payment Provider (Stripe) - Handles secure payment processing
Key Domain Concepts:
- Materials - PDF teaching resources with metadata, previews, and thumbnails
- Authors - Users who upload materials
- Consumers - Users who purchase materials
- Carts - Shopping cart functionality for purchasing multiple materials
The system consists of the following containers:
-
Web Frontend (fe/) - React SPA served by nginx
- Entry point:
https://app.teachly.store - See: fe/AGENTS.md
- Entry point:
-
Web API (be/) - NestJS REST API
- Entry point:
https://api.teachly.store - See: be/AGENTS.md
- Entry point:
-
Identity Provider (keycloak/) - Keycloak for authentication/authorization
- Entry point:
https://auth.teachly.store
- Entry point:
-
Backend Database - PostgreSQL for application data
- Stores: users, materials, carts, transactions
-
Keycloak Database - PostgreSQL for identity data
- Stores: user credentials, realms, clients
-
File Storage - Docker volume for PDFs and generated images
- Path:
/app/assets(in backend container)
- Path:
SSL Termination:
- Nginx reverse proxy handles SSL termination
- Certbot manages Let's Encrypt certificates
- All traffic is HTTPS-only
Network Flow:
User → Nginx Reverse Proxy (SSL) → Frontend (port 80)
→ Backend API (port 3000)
→ Keycloak (port 8080)
- User accesses
https://app.teachly.store - Frontend loads and fetches materials from
/api/materials - Backend queries database and returns material metadata
- Frontend displays materials with preview images
- Authenticated user clicks "Upload"
- Frontend redirects to Keycloak if not logged in
- User fills form with title, description, price, and PDF file
- Frontend POSTs to
/api/materialswith multipart/form-data - Backend:
- Validates JWT token with Keycloak
- Saves PDF to file storage
- Generates preview images (PNG thumbnails)
- Stores metadata in database
- Returns success response
- User adds materials to cart
- Frontend maintains cart state in Zustand store
- User proceeds to checkout
- Frontend POSTs to
/api/stripe/create-checkout-session - Backend:
- Creates Stripe Checkout session
- Stores pending transaction
- Returns checkout URL
- Frontend redirects to Stripe
- After payment, Stripe webhook calls
/api/stripe/webhook - Backend:
- Verifies webhook signature
- Grants access to purchased materials
- Updates transaction status
- Authenticated user accesses purchased materials
- Frontend requests download from
/api/materials/:id/download - Backend:
- Verifies user has purchased the material
- Streams PDF from file storage
- Frontend triggers browser download
1. User clicks "Sign In"
2. Frontend redirects to: https://auth.teachly.store/realms/teachly/protocol/openid-connect/auth
3. Keycloak authenticates user and redirects back to frontend with tokens
4. Frontend stores access token in memory (via react-oidc-context)
5. Subsequent API calls include Authorization header: "Bearer {access_token}"
6. Backend validates token by calling Keycloak's userinfo endpoint
Browser → Frontend (nginx)
│
├─ Static assets (JS, CSS, images) served directly by nginx
│
└─ API calls to /api/*
│
└─ Nginx proxies to backend:3000
│
└─ Backend processes request
├─ Validates JWT (calls Keycloak if needed)
├─ Queries PostgreSQL database (TypeORM)
├─ Accesses file storage (if needed)
└─ Returns JSON response
The application supports three deployment configurations with different API routing strategies:
1. Development (Vite Dev Server):
// vite.config.ts
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
}
}
}2. Docker Compose (Nginx Container):
Frontend Container Nginx (fe/nginx.conf):
location ~ /api/(?<section>.+) {
proxy_pass http://172.17.0.1:3000/$section$is_args$args;
}Reverse Proxy Nginx (deploy/reverse-proxy/nginx.conf):
location ~ /api/(?<section>.+) {
proxy_pass $scheme://172.17.0.1:3000/$section$is_args$args;
}3. Kubernetes (Ingress Controller) - Current Production:
The Kubernetes ingress handles path-based routing at the cluster level:
# teachme-frontend-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- host: app.teachly.store
http:
paths:
# API requests → backend service
- path: /api(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: teachme-backend-service
port:
number: 80
# All other requests → frontend service
- path: /
pathType: Prefix
backend:
service:
name: teachme-frontend-service
port:
number: 80Routing Examples:
| Request | Path Received by Backend |
|---|---|
app.teachly.store/api/materials |
/materials |
app.teachly.store/api/cart/buy |
/cart/buy |
app.teachly.store/api/auth/login |
/auth/login |
Benefits of Kubernetes Ingress Approach:
- ✅ No code changes required in frontend or backend
- ✅ Single SSL certificate for frontend domain
- ✅ No CORS issues (same origin)
- ✅ Path-based routing at ingress controller level
- ✅ Independent scaling of frontend and backend services
teachme/
├── fe/ # Frontend React application
│ ├── src/
│ │ ├── components/ # React components
│ │ ├── services/ # API service layer
│ │ ├── DTOs/ # TypeScript types
│ │ └── ...
│ ├── nginx.conf # Production nginx config (API proxy)
│ └── AGENTS.md # Frontend-specific guidelines
│
├── be/ # Backend NestJS application
│ ├── src/
│ │ ├── materials/ # Materials module (upload, download)
│ │ ├── users/ # Users module
│ │ ├── cart/ # Shopping cart module
│ │ ├── stripe/ # Payment integration
│ │ └── ...
│ └── AGENTS.md # Backend-specific guidelines
│
├── keycloak/ # Keycloak configuration
│ └── start-keycloak.sh
│
├── deploy/ # Docker Compose deployments
│ ├── docker-compose.yml
│ └── reverse-proxy/
│ └── nginx.conf # SSL reverse proxy config
│
└── AGENTS.md # This file
- Frontend: React + TypeScript + Vite + Tailwind CSS + Zustand
- Backend: NestJS + TypeScript + TypeORM + PostgreSQL
- Authentication: Keycloak (OpenID Connect)
- Payments: Stripe
- File Processing: Ghostscript, GraphicsMagick, Poppler
- Containerization: Docker + Docker Compose
- Cloud: DigitalOcean
- Frontend:
http://localhost:5173 - Backend API:
http://localhost:3000 - Keycloak:
http://localhost:8080
The frontend uses Vite's dev server proxy for API calls during development. See fe/vite.config.ts for proxy configuration.
PDFs and generated thumbnails are stored in:
- Local: Docker volume mounted at
/app/assets - Production: PersistentVolumeClaim
teachme-assets-pvc
TypeORM synchronize is enabled in development. For production, migrations should be managed manually.
The application is deployed using ArgoCD with GitOps workflow:
- Push changes to main branch
- GitHub Actions builds and pushes Docker images
- Updates image tags in
app-of-appsrepository - ArgoCD automatically syncs Kubernetes manifests
See Kubernetes manifests in ../app-of-apps/teachme/ directory.