-
Notifications
You must be signed in to change notification settings - Fork 2
Component Integration
This guide covers integrating all TMI components to create a complete working system.
A fully operational TMI deployment consists of:
- TMI Web Application (Frontend)
- TMI Server (API Backend)
- PostgreSQL (Database)
- Redis (Cache)
- OAuth Providers (Authentication)
- Load Balancer/Reverse Proxy (Optional, recommended for production)
┌─────────────────┐
│ User Browser │
└────────┬────────┘
│ HTTPS
↓
┌─────────────────┐
│ Load Balancer │ (Optional)
│ / Reverse │
│ Proxy │
└────┬──────┬─────┘
│ │
│ └─────────────┐
│ HTTPS │ HTTPS
↓ ↓
┌──────────────┐ ┌────────────────┐
│ TMI Web App │ │ TMI Server │
│ (Static) │ │ (API/WebSoc...│
└──────────────┘ └───┬────────┬───┘
│ │
│ TCP │ TCP
↓ ↓
┌──────────┐ ┌──────────┐
│PostgreSQL│ │ Redis │
└──────────┘ └──────────┘
↓
┌──────────────┐
│OAuth Provider│
│(Google/GitHub│
│ /Microsoft) │
└──────────────┘
Before integration, ensure you have:
- TMI server deployed and running
- TMI web application built and deployed
- PostgreSQL configured with migrations run
- Redis configured and running
- OAuth providers configured
- DNS records configured (for production)
- SSL/TLS certificates (for production)
The web application needs to know where to find the API server.
Edit src/environments/environment.prod.ts:
export const environment: Environment = {
production: true,
logLevel: 'ERROR',
apiUrl: 'https://api.tmi.example.com/v1', // Your TMI API server URL
authTokenExpiryMinutes: 60,
operatorName: 'TMI Operator',
operatorContact: 'support@example.com',
operatorJurisdiction: '',
securityConfig: {
enableHSTS: true,
hstsMaxAge: 31536000,
hstsIncludeSubDomains: true,
hstsPreload: false,
frameOptions: 'DENY',
referrerPolicy: 'strict-origin-when-cross-origin',
permissionsPolicy: 'camera=(), microphone=(), geolocation=()',
},
};The apiUrl can also be overridden at runtime via the TMI_API_URL environment variable when running the containerized web application.
Important: The apiUrl must be accessible from users' browsers. Options:
-
Separate subdomain (recommended):
- Web app:
https://tmi.example.com - API:
https://api.tmi.example.com
- Web app:
-
Same domain, different path:
- Web app:
https://tmi.example.com - API:
https://tmi.example.com/api - Requires reverse proxy to route
/apito TMI server
- Web app:
-
Development:
- Web app:
http://localhost:4200 - API:
http://localhost:8080
- Web app:
If web app and API are on different domains, configure CORS in TMI server.
Environment Variable (for general HTTP CORS):
TMI_CORS_ALLOWED_ORIGINS="https://tmi.example.com,https://www.tmi.example.com"Environment Variable (for WebSocket origin checking, separate from HTTP CORS):
WEBSOCKET_ALLOWED_ORIGINS="https://tmi.example.com,https://www.tmi.example.com"Configuration File (config-production.yml):
server:
cors:
# Env: TMI_CORS_ALLOWED_ORIGINS (comma-separated)
allowed_origins:
- "https://tmi.example.com"
- "https://www.tmi.example.com"Note: TMI_CORS_ALLOWED_ORIGINS controls HTTP CORS headers for REST API requests. WEBSOCKET_ALLOWED_ORIGINS is a separate setting that controls which origins may establish WebSocket connections. In production, both should typically be set to the same value(s).
OAuth requires coordination between web app, TMI server, and OAuth providers.
Configure redirect URIs in OAuth provider dashboards:
Web Application Callback: https://tmi.example.com/oauth2/callback
This is where users land after authenticating with the provider.
Configure the OAuth callback URL that TMI server uses:
Configuration File (config-production.yml):
auth:
oauth:
callback_url: "https://api.tmi.example.com/oauth2/callback"
providers:
google:
enabled: true
client_id: "" # Set via env var OAUTH_PROVIDERS_GOOGLE_CLIENT_ID
client_secret: "" # Set via env var OAUTH_PROVIDERS_GOOGLE_CLIENT_SECRET
github:
enabled: true
client_id: "" # Set via env var OAUTH_PROVIDERS_GITHUB_CLIENT_ID
client_secret: "" # Set via env var OAUTH_PROVIDERS_GITHUB_CLIENT_SECRETImportant: OAuth client IDs and secrets should be set via environment variables (e.g., OAUTH_PROVIDERS_GOOGLE_CLIENT_ID), not committed in configuration files. The web app handles the user-facing callback, then sends the authorization code to the TMI server's token exchange endpoint.
The complete OAuth flow:
- User clicks login → Web app redirects to OAuth provider
- User authenticates → Provider redirects to web app callback
- Web app receives code → Sends code to TMI server
- TMI server exchanges code → Gets user info from provider
- TMI server issues JWT → Returns tokens to web app
- Web app stores tokens → Uses for subsequent API requests
Ensure TMI server can connect to PostgreSQL and Redis.
Test connection from TMI server:
# From TMI server host
psql -h postgres-host -U tmi_user -d tmi -c "SELECT version();"Configure TMI server (connection URL, required):
TMI_DATABASE_URL="postgres://tmi_user:secure_password@postgres.example.com:5432/tmi?sslmode=require"The TMI_DATABASE_URL environment variable is required and uses the standard connection URL format. The URL scheme determines the database type automatically. Supported schemes include postgres://, mysql://, sqlserver://, sqlite:///, and oracle://.
Verify migrations:
# Check database schema
make check-database
# Should show all tables presentTest connection from TMI server:
# From TMI server host
redis-cli -h redis-host -p 6379 -a redis_password ping
# Expected: PONGConfigure TMI server (connection URL):
TMI_REDIS_URL="redis://redis_password@redis.example.com:6379/0"Alternatively, configure individual fields in config-production.yml:
database:
redis:
host: "redis.example.com"
port: "6379"
password: "redis_password"
db: 0If TMI_REDIS_URL is set, it takes precedence over individual fields.
Ensure TMI server can reach databases:
# Test PostgreSQL connectivity
telnet postgres.example.com 5432
# Test Redis connectivity
telnet redis.example.com 6379Firewall rules:
- PostgreSQL: Allow only from TMI server IPs
- Redis: Allow only from TMI server IPs
- Do not expose databases to internet
A reverse proxy provides SSL termination, load balancing, and routing.
Create /etc/nginx/sites-available/tmi:
# Upstream servers
upstream tmi_api {
server localhost:8080;
# Add more servers for load balancing
# server 10.0.0.11:8080;
# server 10.0.0.12:8080;
keepalive 32;
}
# Web application
server {
listen 443 ssl http2;
server_name tmi.example.com;
ssl_certificate /etc/letsencrypt/live/tmi.example.com/fullchain.pem;
ssl_private_key /etc/letsencrypt/live/tmi.example.com/privkey.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Web application (static files)
root /var/www/tmi-ux;
index index.html;
# Enable gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
# Static assets with long cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Angular routing
location / {
try_files $uri $uri/ /index.html;
}
}
# API server
server {
listen 443 ssl http2;
server_name api.tmi.example.com;
ssl_certificate /etc/letsencrypt/live/api.tmi.example.com/fullchain.pem;
ssl_private_key /etc/letsencrypt/live/api.tmi.example.com/privkey.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req zone=api burst=20 nodelay;
location / {
proxy_pass http://tmi_api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts for WebSocket
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
}
# HTTP to HTTPS redirect
server {
listen 80;
server_name tmi.example.com api.tmi.example.com;
return 301 https://$server_name$request_uri;
}Enable configuration:
sudo ln -s /etc/nginx/sites-available/tmi /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxFor same-domain deployment:
server {
listen 443 ssl http2;
server_name tmi.example.com;
ssl_certificate /etc/letsencrypt/live/tmi.example.com/fullchain.pem;
ssl_private_key /etc/letsencrypt/live/tmi.example.com/privkey.pem;
# API routes
location /api/ {
rewrite ^/api/(.*) /$1 break;
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Web application
root /var/www/tmi-ux;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}Update web app configuration:
apiUrl: 'https://tmi.example.com/api'# Install Certbot
sudo apt install certbot python3-certbot-nginx
# Get certificate for web app
sudo certbot --nginx -d tmi.example.com -d www.tmi.example.com
# Get certificate for API
sudo certbot --nginx -d api.tmi.example.com
# Test renewal
sudo certbot renew --dry-runCertbot automatically:
- Obtains certificates
- Configures nginx
- Sets up auto-renewal
If using custom certificates:
# Copy certificates
sudo cp tmi.example.com.crt /etc/ssl/certs/
sudo cp tmi.example.com.key /etc/ssl/private/
sudo chmod 600 /etc/ssl/private/tmi.example.com.keyConfigure in nginx (shown in Step 4).
Configure DNS records for your domains:
tmi.example.com A 203.0.113.10
api.tmi.example.com A 203.0.113.10
Or use separate IPs:
tmi.example.com A 203.0.113.10 (web app server)
api.tmi.example.com A 203.0.113.11 (API server)
tmi.example.com A 203.0.113.10
www.tmi.example.com CNAME tmi.example.com
api.tmi.example.com CNAME tmi.example.com
# Check DNS resolution
dig tmi.example.com
dig api.tmi.example.com
# Or with nslookup
nslookup tmi.example.com
nslookup api.tmi.example.com# Health/info check (root endpoint)
curl https://api.tmi.example.com/
# Expected response (JSON when called without browser Accept header):
{
"status": {
"code": "OK",
"time": "2025-11-12T12:00:00Z"
},
"service": {
"build": "1.4.0-abc1234 (production)",
"name": "TMI"
},
"api": {
"specification": "https://github.com/ericfitz/tmi/blob/main/api-schema/tmi-openapi.json",
"version": "1.4.0"
}
}
# Check OAuth providers
curl https://api.tmi.example.com/oauth2/providers
# Expected response:
{
"providers": [
{"id": "google", "name": "Google", "icon": "fa-brands fa-google"},
{"id": "github", "name": "GitHub", "icon": "fa-brands fa-github"}
]
}Note: The root endpoint (/) serves as both the health check and API info endpoint. When accessed from a browser (with Accept: text/html), it returns an HTML page. When the system is degraded, the response includes a health object with database and Redis status details.
# Check if web app loads
curl -I https://tmi.example.com
# Should return 200 OK
# Check static assets
curl -I https://tmi.example.com/main.js
# Should return 200 OK with long cache headers-
Open web app in browser:
https://tmi.example.com - Click login button
- Select OAuth provider (Google/GitHub/Microsoft)
- Complete authentication
- Verify redirect to application
- Check browser console for errors
- Verify JWT token received
After logging in:
// In browser console
fetch('https://api.tmi.example.com/threat_models', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('tmi_access_token')
}
})
.then(r => r.json())
.then(console.log)Should return threat models (empty array if none created yet).
TMI uses ticket-based authentication for WebSocket connections. The flow is:
- Obtain a ticket via an authenticated REST call:
// In browser console (after login)
const sessionId = crypto.randomUUID();
const response = await fetch(
`https://api.tmi.example.com/ws/ticket?session_id=${sessionId}`,
{ headers: { 'Authorization': 'Bearer ' + localStorage.getItem('tmi_access_token') } }
);
const { ticket } = await response.json();- Connect to WebSocket using the ticket:
const ws = new WebSocket(
`wss://api.tmi.example.com/ws/diagrams/${diagramId}?ticket=${ticket}&session_id=${sessionId}`
);
ws.onopen = () => console.log('WebSocket connected');
ws.onerror = (error) => console.error('WebSocket error:', error);
ws.onclose = () => console.log('WebSocket closed');Note: Tickets are single-use and expire after 30 seconds. The GET /ws/ticket endpoint requires a valid JWT (Bearer token or session cookie). The WebSocket connection itself authenticates via the ?ticket= query parameter, not via headers.
Symptom: Browser console shows CORS errors when calling API.
Solution:
- Verify
TMI_CORS_ALLOWED_ORIGINSincludes web app domain - Check API server CORS configuration
- Ensure preflight requests are handled
Test CORS:
curl -H "Origin: https://tmi.example.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Authorization" \
-X OPTIONS \
https://api.tmi.example.com/threat_modelsSymptom: After OAuth login, app redirects back to provider repeatedly.
Causes:
- Redirect URI mismatch in OAuth provider configuration
- State parameter validation failing
- Token exchange failing
Debug:
- Check browser network tab for failed requests
- Verify OAuth redirect URI matches exactly
- Check TMI server logs for token exchange errors
- Verify OAuth client credentials are correct
Symptom: Real-time collaboration doesn't work.
Solutions:
- Check reverse proxy WebSocket configuration
- Verify
UpgradeandConnectionheaders are forwarded - Check WebSocket allowed origins
- Increase proxy timeouts for long-lived connections
Test WebSocket:
# First obtain a ticket (requires a valid JWT)
TICKET=$(curl -s -H "Authorization: Bearer YOUR_TOKEN" \
"https://api.tmi.example.com/ws/ticket?session_id=test-session-id" | jq -r '.ticket')
# Then connect using the ticket (requires wscat)
npm install -g wscat
wscat -c "wss://api.tmi.example.com/ws/diagrams/test-id?ticket=${TICKET}&session_id=test-session-id"Note: The ticket expires after 30 seconds and is single-use, so obtain it immediately before connecting.
Symptom: API requests fail with database timeout errors.
Solutions:
- Check database server is running
- Verify firewall allows TMI server to database
- Check connection pool settings
- Verify database credentials
Test database connection:
# From TMI server host
psql -h postgres-host -U tmi_user -d tmi -c "SELECT 1;"Symptom: Web app loads but shows blank page, missing CSS/JS.
Solutions:
- Check nginx static file configuration
- Verify file permissions on /var/www/tmi-ux
- Check browser console for 404 errors
- Verify nginx is serving correct directory
Debug:
# Check files exist
ls -la /var/www/tmi-ux/
# Check nginx error log
sudo tail -f /var/log/nginx/error.log
# Test static file
curl -I https://tmi.example.com/main.jsAfter integration, verify security:
- All traffic uses HTTPS
- HTTP redirects to HTTPS
- Security headers configured (CSP, X-Frame-Options, etc.)
- Database not accessible from internet
- Redis not accessible from internet
- OAuth secrets in environment variables, not config files
- JWT secret is strong and unique
- Rate limiting configured
- CORS properly restricted
- Firewall rules limiting access
- SSL certificates valid and auto-renewing
Configure nginx:
upstream tmi_api {
server localhost:8080;
keepalive 32; # Keep connections alive
}
location / {
proxy_http_version 1.1;
proxy_set_header Connection ""; # Clear connection header
}Configure caching headers:
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Don't cache API responses
location /api/ {
add_header Cache-Control "no-store, no-cache, must-revalidate";
}Enable compression in nginx:
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml+rss;Set up monitoring for integrated system:
Create /usr/local/bin/check-tmi-health.sh:
#!/bin/bash
# Check web app
if ! curl -f -s https://tmi.example.com > /dev/null; then
echo "ERROR: Web app not responding"
exit 1
fi
# Check API (root endpoint serves as health check)
if ! curl -f -s https://api.tmi.example.com/ > /dev/null; then
echo "ERROR: API not responding"
exit 1
fi
# Check database
if ! psql -h postgres-host -U tmi_user -d tmi -c "SELECT 1;" > /dev/null 2>&1; then
echo "ERROR: Database not responding"
exit 1
fi
# Check Redis
if ! redis-cli -h redis-host -a password ping > /dev/null 2>&1; then
echo "ERROR: Redis not responding"
exit 1
fi
echo "OK: All components healthy"
exit 0Consider deploying:
- Prometheus: Metrics collection
- Grafana: Dashboards and visualization
- Loki: Log aggregation
- Alertmanager: Alert routing
Before going live:
- All components tested individually
- Integration tests pass
- OAuth flow works end-to-end
- WebSocket connections successful
- Database migrations complete
- Redis cache functioning
- DNS records configured and propagated
- SSL certificates installed and valid
- Reverse proxy configured and tested
- Firewall rules in place
- Monitoring and alerting active
- Backup procedures tested
- Rollback plan documented
- Can access web app via domain
- Can login with OAuth providers
- Can create threat model
- Can edit diagrams
- Real-time collaboration works
- No errors in browser console
- No errors in server logs
- Post-Deployment - Final verification and testing
- Monitoring-and-Health - Set up ongoing monitoring
- Security-Best-Practices - Additional hardening
- Using TMI for Threat Modeling
- Accessing TMI
- Authentication
- Creating Your First Threat Model
- Understanding the User Interface
- Working with Data Flow Diagrams
- Managing Threats
- Collaborative Threat Modeling
- Using Notes and Documentation
- Timmy AI Assistant
- Metadata and Extensions
- Planning Your Deployment
- Terraform Deployment (AWS, OCI, GCP, Azure)
- Deploying TMI Server
- OCI Container Deployment
- Certificate Automation
- Deploying TMI Web Application
- Setting Up Authentication
- Database Setup
- Component Integration
- Post-Deployment
- Branding and Customization
- Monitoring and Health
- Cloud Logging
- Database Operations
- Security Operations
- Performance and Scaling
- Maintenance Tasks
- Getting Started with Development
- Architecture and Design
- API Integration
- Testing
- Contributing
- Extending TMI
- Dependency Upgrade Plans
- DFD Graphing Library Reference
- Migration Instructions