REST API server for the HealthShadow platform, supporting multi-user healthcare applications with role-based access for students and professionals.
- Backend: Kotlin + Ktor framework
- Database: PostgreSQL with SQLDelight for type-safe queries
- Deployment: Docker Compose with containerized services
- Build Tool: Gradle
This application uses a single-server Docker Compose setup for self-hosting:
Internet
↓
Your Home Server
├── Traefik Reverse Proxy Container
│ ├── Automatic SSL/HTTPS with Let's Encrypt
│ ├── Routes traffic to application containers
│ ├── Service discovery via Docker labels*
│ └── Serves on ports 80/443
*Service Discovery: Traefik automatically finds containers by reading their Docker labels, eliminating the need for manual configuration files
├── Application Container (Ktor)
│ ├── Runs on internal port 8080
│ └── Only accessible through proxy
└── PostgreSQL Container
├── Database with persistent volumes
└── Only accessible to application
Docker Compose Benefits:
- Container Orchestration: Manages multiple services as one application
- Container Networking: Services communicate by name (no IP management)
- Dependency Management: Database starts before application
- Environment Variables: Shared configuration across containers
- Volume Management: Data persistence across container restarts
- One Command Deployment:
docker-compose upstarts entire stack
Reverse Proxy Benefits:
- SSL/HTTPS: Automatic certificate management
- Security: Application never directly exposed to internet
- Port Management: Public ports (80/443) → Internal app port (8080)
- Domain Routing: Routes domain name to application
- Static File Serving: Efficient serving of assets
- Rate Limiting: Built-in DDoS protection
Port Flow Example:
User: https://myapp.com (port 443)
↓
Server: Port 443 → Proxy Container
↓
Proxy: Internal request to app:8080
↓
App: Processes request, responds via port 8080
↓
Proxy: Returns HTTPS response to user
The application uses a role-based user system with relational database design:
users - Base authentication table
id(Primary Key)email(Unique)password_hashuser_type(student, professional, admin)created_at,updated_at,is_active
student - Student-specific data
id(Primary Key)user_id(Foreign Key → users.id, CASCADE DELETE)first_name,last_name,phonestudent_id,major,year_level,gpacreated_at,updated_at
professional - Healthcare professional data
id(Primary Key)user_id(Foreign Key → users.id, CASCADE DELETE)first_name,last_name,phonelicense_number,specialization,years_experienceorganization,title,bio,verifiedcreated_at,updated_at
- Data Integrity: CASCADE DELETE ensures no orphaned records
- Relationship Enforcement: Each student/professional must have a user account
- Query Efficiency: JOIN operations to get complete user profiles
This application uses HikariCP DataSource with SQLDelight 2.0 for database connectivity, following the recommended approach from SQLDelight documentation.
Why HikariCP DataSource?
- Connection Pooling: Efficiently manages database connections
- Performance: HikariCP is one of the fastest connection pools available
- SQLDelight Integration: Native support via
.asJdbcDriver()extension - Production Ready: Handles connection lifecycle, timeouts, and recovery
Configuration:
// DatabaseFactory uses HikariCP with these settings:
maximumPoolSize = 10
isAutoCommit = false
transactionIsolation = "TRANSACTION_REPEATABLE_READ"This approach provides better performance and reliability compared to direct JDBC connections, especially under load.
Traefik is a modern reverse proxy that automatically discovers and routes traffic to your Docker containers. Unlike traditional proxies (nginx, Apache), Traefik requires zero manual configuration - it reads Docker labels to understand how to route traffic.
Automatic Configuration:
- No editing config files when adding/removing services
- Containers declare their own routing rules via labels
- Dynamic updates without proxy restarts
Built-in SSL/HTTPS:
- Automatic Let's Encrypt certificate provisioning
- Certificate renewal handled automatically
- HTTPS redirects configured by default
Docker Integration:
- Reads Docker socket to discover containers
- Understands Docker networks and service names
- Works seamlessly with Docker Compose
Container Labels Define Routing:
app:
labels:
- "traefik.enable=true" # Make this container discoverable
- "traefik.http.routers.hsc-app.rule=Host(`myapp.com`)" # Route myapp.com to this container
- "traefik.http.routers.hsc-app.entrypoints=websecure" # Use HTTPS (port 443)
- "traefik.http.routers.hsc-app.tls.certresolver=letsencrypt" # Get SSL cert from Let's Encrypt
- "traefik.http.services.hsc-app.loadbalancer.server.port=8080" # Container listens on port 8080Traffic Flow:
User: https://myapp.com
↓
Traefik: Reads Host header, finds matching container label
↓
Traefik: Routes to hsc-app container on port 8080
↓
App: Processes request, returns response
↓
Traefik: Returns HTTPS response with auto-managed SSL certificate
The project requires one static configuration file to enable Docker discovery and SSL:
traefik/traefik.yml - Main configuration
- Enables Docker provider for service discovery
- Configures Let's Encrypt for automatic SSL certificates
- Sets up entrypoints (HTTP port 80, HTTPS port 443)
# Start full stack with Traefik
docker-compose up -d
# View Traefik dashboard (shows discovered services)
open http://localhost:8081
# Check Traefik logs (useful for SSL certificate issues)
docker logs hsc-traefik
# Test SSL certificate provisioning
curl -v https://yourdomain.com
# Force SSL certificate renewal (if needed)
docker exec hsc-traefik rm /acme/acme.json
docker restart hsc-traefikRequired for Production:
DOMAIN=yourdomain.com- Your actual domain namePOSTGRES_PASSWORD=secure_password- Database password
Local Development:
- Uses
localhostdefaults for testing - SSL disabled for local development
Traefik stores Let's Encrypt certificates in a Docker volume:
volumes:
traefik-acme: # Persistent storage for SSL certificatesImportant: This volume persists certificates across container restarts. Losing this volume means re-requesting certificates from Let's Encrypt (rate limited).
- JDK 11 or higher
- Docker and Docker Compose (for database)
On a fresh machine, you need to build both the frontend and backend before running the application.
The application serves a React frontend that must be built before the backend can serve it.
# Navigate to the React frontend directory
cd react-web
npm install
npm run build
cd ..What this does:
npm installdownloads packages listed in package.json to node_modules/npm run buildcompiles TypeScript → JavaScript, bundles React components, minifies code, and outputs to react-web/build/
Development vs Production:
- Development:
npm run devstarts a local server at localhost:3002 with hot-reload - Production:
npm run buildcreates optimized files in react-web/build/ for deployment
Once the frontend is built, create the Docker image for the Ktor backend:
docker build -t hsc-http:latest .Why this is needed:
- docker-compose expects a pre-built image named hsc-http:latest
- The Dockerfile copies everything needed from the app into the docker image
- Without this step, you get the "repository does not exist" error
Troubleshooting:
- If
docker-compose upfails with "repository does not exist", you forgot this step - If the frontend doesn't load, rebuild the React app first
# Start PostgreSQL container for development
docker-compose -f docker-compose.dev.yml up -d
# Check container status
docker-compose -f docker-compose.dev.yml ps
# View PostgreSQL logs (useful for troubleshooting)
docker-compose -f docker-compose.dev.yml logs postgres
# Stop PostgreSQL when done
docker-compose -f docker-compose.dev.yml downCommand Explanation:
-f docker-compose.dev.yml= Use this specific file (default isdocker-compose.yml)up -d= Start containers in background (detached mode)ps= Show running containers and their statuslogs postgres= Show PostgreSQL startup messages and errorsdown= Stop and remove all containers
Database Connection:
- Host: localhost
- Port: 5432
- Database: healthshadow_dev
- Username: hsc_user
- Password: hsc_dev_password
# Build and test
./gradlew build
# Run locally (development mode)
./gradlew run
# Application runs on http://localhost:8080Rebuild and Restart Containers:
# Most common - rebuild image and recreate all containers
docker-compose -f docker-compose.dev.yml up -d --build
# Alternative: rebuild image first, then recreate
docker build -t hsc-http . && docker-compose -f docker-compose.dev.yml up -d --force-recreate
# Force recreate without rebuilding image (use existing image)
docker-compose -f docker-compose.dev.yml up -d --force-recreate
# Recreate specific service only
docker-compose -f docker-compose.dev.yml up -d --force-recreate app
# Nuclear option - stop, remove everything, rebuild, restart
docker-compose -f docker-compose.dev.yml down
docker build -t hsc-http .
docker-compose -f docker-compose.dev.yml up -dWhen to Use:
--build- When you've changed application code--force-recreate- When you've changed docker-compose.yml configuration- Both - When you've changed both code and configuration
Direct Access (Bypasses Traefik):
# Test root endpoint
curl http://localhost:8080/
# Get all students
curl http://localhost:8080/students
# Add a student
curl -X POST http://localhost:8080/students \
-H "Content-Type: application/json" \
-d '{"id":"1","firstName":"John","lastName":"Doe","email":"john@example.com","phone":"555-1234"}'Through Traefik (Production-like):
# Test HTTPS access (ignores SSL certificate warnings)
curl -k -H "Host: hsc-http.localhost" https://localhost/
# Test HTTP redirect (should return 301 redirect)
curl -H "Host: hsc-http.localhost" http://localhost/
# Test API endpoints through Traefik
curl -k -H "Host: hsc-http.localhost" https://localhost/studentsServer Requirements:
- Linux server (Ubuntu/Debian recommended)
- Docker and Docker Compose installed
- Domain name pointing to server IP
- Ports 80 and 443 accessible from internet
Deployment Process:
- Clone repository to server
- Configure environment variables
- Run
docker-compose up -d - SSL certificates automatically provisioned
Data Persistence:
- Database data stored in Docker volumes
- Survives container restarts and updates
- Backup strategy: Volume snapshots
Previous State: Heroku-deployed application with basic student management
Current Modernization:
- ✅ Updated dependencies (Kotlin 1.9.25, Ktor 2.3.12, SQLDelight 2.0.2)
- ✅ Migrated from Gradle 7.2 to 8.5
- ✅ Redesigned database schema for multi-user system
- ✅ Fixed localhost binding for browser development
- ✅ Docker containerization for self-hosting
- ✅ Authentication utilities with bcrypt password hashing
- 🔄 In Progress: Web authentication UI (login/logout pages)
- 📋 Planned: SSL setup, monitoring, comprehensive testing
One of the key advantages of this Traefik-based setup is the ability to host multiple services for the HealthShadow platform on the same server using different subdomains. Each service gets its own SSL certificate and container, while sharing the same infrastructure.
How Multi-Service Hosting Works:
The HTTP Host header identifies which service each request is intended for:
User Request: https://api.shadowconnects.com
├── DNS resolves to: 73.43.190.195 (your home IP)
├── Router forwards: Port 443 → Server:443
├── Traefik reads: Host: api.shadowconnects.com
└── Routes to: API container based on Docker labels
Potential Services Architecture:
# Main API (Current)
api.shadowconnects.com → Kotlin/Ktor REST API
# Frontend Application
app.shadowconnects.com → React/Vue.js SPA
# Admin Dashboard
admin.shadowconnects.com → Admin interface
# Student Portal
student.shadowconnects.com → Student-specific features
# Professional Portal
professional.shadowconnects.com → Healthcare professional tools
# Documentation/Help
docs.shadowconnects.com → API documentation, user guides
# File Storage
files.shadowconnects.com → Document/image upload service
# Real-time Features
ws.shadowconnects.com → WebSocket server for chat/notifications
# Analytics Dashboard
analytics.shadowconnects.com → Usage metrics, reportingImplementation Example:
# docker-compose.yml - Multiple services
services:
# Current API service
api:
labels:
- "traefik.http.routers.api.rule=Host(`api.shadowconnects.com`)"
# Frontend application
frontend:
image: nginx:alpine
labels:
- "traefik.http.routers.frontend.rule=Host(`app.shadowconnects.com`)"
# Admin dashboard
admin:
image: admin-dashboard:latest
labels:
- "traefik.http.routers.admin.rule=Host(`admin.shadowconnects.com`)"Benefits of This Architecture:
- Single IP Address: All services use the same public IP (73.43.190.195)
- Automatic SSL: Each subdomain gets its own Let's Encrypt certificate
- Independent Deployment: Services can be updated/restarted independently
- Resource Efficiency: Shared database, shared reverse proxy
- Easy Scaling: Add new services by adding containers + labels
- Professional Appearance: Clean subdomain structure
Future Service Ideas:
- Matching Service: Algorithm to connect students with professionals
- Scheduling System: Appointment booking and calendar management
- Communication Platform: Secure messaging between users
- Content Management: Educational resources and documentation
- Payment Processing: Subscription management for premium features
- Mobile API Gateway: Specialized endpoints for mobile applications
This architecture allows the HealthShadow platform to grow organically - start with the core API, then add specialized services as user needs become clear.
- ✅ Password hashing (bcrypt)
- ✅ Session management utilities
- 🔄 Login/logout web pages
- 📋 Route protection middleware
- 📋 User registration flow
- User registration/authentication endpoints
- Role-based access control
- Professional verification workflow
- Student-professional matching system
- Automated backups
- Monitoring and logging
- CI/CD pipeline
- Health checks
- Code linting and formatting (ktlint)
This project follows industry-standard testing practices with clear separation of concerns:
Unit Tests - Test single components in isolation, no external dependencies
- Pure unit tests: Zero dependencies (e.g., validation logic)
- Unit tests with mocks: Isolated business logic with mocked dependencies
Integration Tests - Test components working together with external dependencies
- Repository tests: Use in-memory H2 database for fast, isolated database testing
- End-to-end tests: Full application flow testing
Current Test Coverage:
- ✅ Authentication layer: Password validation, auth service, user repository
- 📋 Planned: Route handlers, session management, full auth flow
- Ensure tests pass:
./gradlew test - Follow existing code conventions
- Write tests for new functionality (unit tests preferred)
- Update this README for architectural changes
- Commit messages should be descriptive and concise
PORT- Application port (default: 8080)DATABASE_URL- PostgreSQL connection stringJWT_SECRET- Secret key for JWT tokens (when implemented)ANTHROPIC_API_KEY- API key for Claude AI chatbot integration (required for chat feature)
- Local Development:
app/src/main/resources/application.conf - Production: Environment variables override configuration
The application includes an AI-powered chatbot using Claude (Anthropic's AI assistant) to provide users with guidance and answer questions.
- Real-time Streaming Responses: Messages stream in character-by-character for a natural conversation feel
- Floating Chat Widget: Non-intrusive chat button in the bottom-right corner
- Modern UI: Built with React and Tailwind CSS, matches the site's design
-
Get an API Key:
- Sign up at Anthropic Console
- Generate an API key from your account settings
- Free tier available with generous limits
-
Set Environment Variable:
# For local development export ANTHROPIC_API_KEY="your-api-key-here" # Or add to your shell profile (~/.bashrc, ~/.zshrc, etc.) echo 'export ANTHROPIC_API_KEY="your-api-key-here"' >> ~/.bashrc source ~/.bashrc
-
For Docker Deployment:
# In docker-compose.yml services: app: environment: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
Then set the variable on your host:
export ANTHROPIC_API_KEY="your-api-key-here" docker-compose up -d
- Click the chat button in the bottom-right corner
- Type your question and press Enter or click Send
- Responses stream in real-time from Claude
POST /api/chat- Send messages to the chatbot- Request body:
{"message": "your question here"} - Response: Server-Sent Events (SSE) stream
Claude API pricing (as of 2024):
- Claude 3.5 Sonnet: ~$3 per million input tokens, ~$15 per million output tokens
- Typical chat message: 100-500 tokens
- Very affordable for personal/small business use
CORS is a browser security feature that controls which websites can make requests to your server. By default, browsers block requests from one domain to another (e.g., frontend.com trying to call api.backend.com).
Example of CORS in Action:
Your React app at: https://app.shadowconnects.com
Tries to fetch from: https://api.shadowconnects.com/students
Browser checks: "Does api.shadowconnects.com allow requests from app.shadowconnects.com?"
- If YES (CORS configured): Request proceeds
- If NO (CORS not configured): Request blocked, console shows CORS error
Without CORS, any malicious website could make requests to your bank's API using your logged-in session. CORS ensures only trusted origins can access your server.
CORS is Required When:
- Frontend and backend are on different domains/subdomains
- Frontend runs on
localhost:3002and backend onlocalhost:8080 - Mobile apps making API requests
- Third-party services accessing your API
CORS is NOT Required When:
- Frontend and backend served from the same domain (e.g., both at
shadowconnects.com) - Server-to-server communication (no browser involved)
Located in app/src/main/kotlin/com/shadowconnect/plugins/CORS.kt:
fun Application.configureCORS() {
install(CORS) {
allowMethod(HttpMethod.Get) // Allow GET requests
allowMethod(HttpMethod.Post) // Allow POST requests
allowHeader(HttpHeaders.ContentType) // Allow Content-Type header
allowCredentials = true // Allow cookies/sessions
// Allow specific origins
allowHost("localhost:3002", listOf("http", "https"))
allowHost("shadowconnects.com", listOf("https"))
}
}Error: Access to fetch has been blocked by CORS policy
- Cause: Your frontend's domain is not in
allowHost()list - Fix: Add the domain to CORS configuration
Error: The 'Access-Control-Allow-Origin' header contains multiple values
- Cause: Multiple CORS configurations or conflicting settings
- Fix: Ensure only one CORS configuration in your application
Error: Credential is not supported if the CORS header 'Access-Control-Allow-Origin' is '*'
- Cause: Using
anyHost()withallowCredentials = true - Fix: Use specific
allowHost()entries instead ofanyHost()
SEO is the practice of optimizing your website to rank higher in search engine results (Google, Bing, etc.). When someone searches for "clinical shadowing opportunities," you want your site to appear on the first page of results.
Why SEO Matters:
- Discoverability: 93% of online experiences begin with a search engine
- Organic Traffic: Free, sustainable traffic vs. paying for ads
- Credibility: Higher rankings = more trust from users
- Target Audience: Reach students actively searching for shadowing opportunities
How Search Engines Work:
- Crawling: Bots discover your pages by following links
- Indexing: Pages are analyzed and stored in a database
- Ranking: Algorithm determines which pages to show for each search query
Three Pillars of SEO:
- Technical SEO: Site structure, speed, mobile-friendliness
- On-Page SEO: Content, meta tags, keywords, headings
- Off-Page SEO: Backlinks, social signals, domain authority
The Problem with Single-Page Apps (SPAs):
Traditional SPAs serve all content from one URL with anchor links (#about, #features). This is bad for SEO because:
- Google sees only one page to index
- Can't target different keywords per "page"
- Poor internal linking structure
- Difficult to share specific sections
Our Solution: Client-Side Routing with React Router
We use React Router to create distinct URLs for different content:
/ → Homepage (clinical shadowing opportunities)
/how-it-works → Process explanation
/opportunities → Browse opportunities
/about → Mission and team
/for-students → Student-focused content
/for-professionals → Professional-focused content
Each route:
- Has a unique URL Google can index
- Targets specific keywords
- Has custom meta tags and descriptions
- Provides shareable links
1. Multi-Page Routing
- Technology: React Router DOM
- Location:
react-web/src/App.tsx - Benefit: Each URL is a distinct page for search engines
2. Dynamic Meta Tags
- Technology: react-helmet-async
- Location:
react-web/src/components/SEOHead.tsx - What We Include:
- Unique
<title>tags per page - Meta descriptions (150-160 characters)
- Open Graph tags (social media sharing)
- Twitter Card tags
- Canonical URLs (prevent duplicate content)
- Unique
Example from HomePage:
<SEOHead
title="Clinical Shadowing Opportunities"
description="Connect with healthcare professionals and gain valuable clinical experience..."
path="/"
/>3. SPA Fallback Routing
- Location:
app/src/main/kotlin/com/shadowconnect/plugins/Routing.kt - Purpose: Serve
index.htmlfor all non-API routes - How It Works:
// Catch-all route serves React app for client-side routing get("{...}") { call.respondFile(indexFile) }
- Why Needed: Allows users to refresh on
/aboutor directly visit/opportunitieswithout 404 errors
4. Semantic HTML Structure
- Proper heading hierarchy (H1 → H2 → H3)
- Semantic tags (
<header>,<main>,<section>,<footer>) - Descriptive link text (avoid "click here")
5. Mobile Responsiveness
- Technology: Tailwind CSS with responsive breakpoints
- Why It Matters: Google uses mobile-first indexing
✅ Implemented:
- Multi-page routing with React Router
- Unique meta tags per page (title, description, Open Graph)
- Canonical URLs to prevent duplicate content
- Mobile-responsive design
- Fast page loads (Vite bundler)
- Semantic HTML structure
- Internal linking (header, footer, CTAs)
- Backend SPA fallback for proper routing
📋 Planned:
-
sitemap.xml
- XML file listing all pages
- Helps search engines discover content
- Location:
/sitemap.xml
-
robots.txt
- Guides search engine crawlers
- Specifies what to crawl/not crawl
- Location:
/robots.txt
-
Structured Data (Schema.org)
- JSON-LD markup for rich search results
- Types: Organization, BreadcrumbList, FAQPage
- Enables features like knowledge panels
-
Content Strategy
- Blog section at
/blog/for keyword-rich articles - FAQ page with structured data
- Success stories and testimonials
- Blog section at
-
Performance Optimization
- Image optimization and lazy loading
- Code splitting for faster initial loads
- CDN for static assets
-
Analytics & Monitoring
- Google Search Console (track indexing)
- Google Analytics 4 (user behavior)
- Core Web Vitals monitoring
-
Backlink Building
- Partnerships with medical schools
- Directory listings (healthcare education)
- Guest posts on healthcare blogs
When adding new pages or content:
- Create Unique Pages: Add new routes in
App.tsxfor distinct content - Add SEO Tags: Use
<SEOHead>component with unique title/description - Use Keywords Naturally: Include "clinical shadowing," "healthcare," "medical students"
- Optimize Headers: One H1 per page, logical H2/H3 hierarchy
- Write Descriptive Links: Use meaningful anchor text ("Browse Opportunities" not "Click Here")
- Mobile First: Test all pages on mobile devices
- Page Speed: Keep images optimized, avoid heavy libraries
Key Metrics to Track:
- Organic search traffic (Google Analytics)
- Keyword rankings ("clinical shadowing opportunities")
- Click-through rate (Google Search Console)
- Page indexing status
- Core Web Vitals (LCP, FID, CLS)
- Backlink quantity and quality
Tools:
- Google Search Console (free)
- Google Analytics 4 (free)
- Google PageSpeed Insights (free)
- Ahrefs or SEMrush (paid, advanced)
# Connect to development database
docker exec -it hsc-postgres psql -U hsc_user -d healthshadow_dev
# Alternative: Connect from host machine (if psql installed locally)
psql -h localhost -p 5432 -U hsc_user -d healthshadow_devView Tables:
-- List all tables
\dt
-- Show table structure
\d users
\d student
\d professional
-- Show all columns with types
\d+ usersQuery Tables:
-- View all users (shows test logins)
SELECT id, email, user_type, is_active, created_at FROM users;
-- View students with user info
SELECT u.email, u.user_type, s.first_name, s.last_name, s.major
FROM users u
JOIN student s ON u.id = s.user_id;
-- View professionals with user info
SELECT u.email, u.user_type, p.first_name, p.last_name, p.specialization
FROM users u
JOIN professional p ON u.id = p.user_id;Update Entries:
-- Update user password (use actual bcrypt hash)
UPDATE users SET password_hash = '$2a$10$actual.bcrypt.hash.here' WHERE email = 'student@example.com';
-- Activate/deactivate user
UPDATE users SET is_active = false WHERE email = 'student@example.com';
UPDATE users SET is_active = true WHERE email = 'student@example.com';
-- Update student info
UPDATE student SET gpa = 3.85, year_level = 4 WHERE user_id = 1;
-- Verify professional
UPDATE professional SET verified = true WHERE user_id = 2;Exit Database:
-- Exit psql
\qThe database is initialized with these test accounts (passwords need proper bcrypt hashing):
- Student:
student@example.com - Professional:
professional@example.com - Admin:
admin@example.com
passwords are all "password"