Skip to content

Deploying TMI Server

Eric Fitzgerald edited this page Apr 8, 2026 · 4 revisions

Deploying TMI Server

This guide covers all supported methods for deploying the TMI API server, from managed platforms to bare-metal installations.

Overview

The TMI server is a Go-based HTTP server that provides:

  • RESTful API with OpenAPI 3.0 specification
  • WebSocket support for real-time collaborative editing
  • OAuth and SAML authentication with JWT tokens
  • Role-based access control
  • Multi-database support via DATABASE_URL (PostgreSQL, MySQL, SQLServer, SQLite, Oracle)
  • Redis integration for caching and session management

Deployment Methods

Method Best For Section
Heroku Fastest path to production, managed infrastructure Heroku Deployment
Docker Compose Containerized, portable, small production or development Docker Compose
Traditional Server Full control on VPS or bare metal Traditional Server
Kubernetes Cloud-native, horizontally scalable Kubernetes

Building the Server

Prerequisites

  • Go 1.26.1 or later (verified in go.mod)
  • Git
  • Make

Development Build

# Clone the repository
git clone https://github.com/ericfitz/tmi.git
cd tmi

# Build the server
make build-server

# The binary is written to ./bin/tmiserver

Production Build

The production build injects version metadata from the .version file and the git commit hash via linker flags. Use the Makefile target for correct versioning:

# Build with version metadata (recommended)
make build-server

# Manual build with optimizations (no version metadata)
CGO_ENABLED=0 go build -ldflags="-w -s" -trimpath -o tmiserver ./cmd/server

# Cross-compile for different platforms
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -trimpath -o tmiserver-linux-amd64 ./cmd/server
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-w -s" -trimpath -o tmiserver-windows-amd64.exe ./cmd/server

Note: The build target is ./cmd/server (a package path), not cmd/server/main.go. Setting CGO_ENABLED=0 produces a static binary but excludes Oracle database support.

Docker Build

TMI uses Chainguard container images for enhanced security with a minimal attack surface.

The Dockerfile.server in the repository uses a multi-stage build with version injection from the .version file:

# Dockerfile.server (Chainguard-based secure build)
# Builder stage: Chainguard Go image
FROM cgr.dev/chainguard/go:latest AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . .
# Static binary build with version metadata from .version file
# (Excludes Oracle support which requires CGO)
RUN MAJOR=$(cat .version | grep '"major"' | grep -o '[0-9]*') && \
    MINOR=$(cat .version | grep '"minor"' | grep -o '[0-9]*') && \
    PATCH=$(cat .version | grep '"patch"' | grep -o '[0-9]*') && \
    PRERELEASE=$(cat .version | grep '"prerelease"' | sed 's/.*: *"\(.*\)".*/\1/') && \
    CGO_ENABLED=0 GOOS=linux go build \
    -ldflags "-s -w -X github.com/ericfitz/tmi/api.VersionMajor=${MAJOR} \
              -X github.com/ericfitz/tmi/api.VersionMinor=${MINOR} \
              -X github.com/ericfitz/tmi/api.VersionPatch=${PATCH} \
              -X github.com/ericfitz/tmi/api.VersionPreRelease=${PRERELEASE} \
              -X github.com/ericfitz/tmi/api.GitCommit=$(git rev-parse --short HEAD 2>/dev/null || echo 'development') \
              -X github.com/ericfitz/tmi/api.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
    -trimpath -buildmode=exe -o tmiserver ./cmd/server

# Runtime stage: Chainguard static image (minimal attack surface)
FROM cgr.dev/chainguard/static:latest
COPY --from=builder /app/tmiserver /tmiserver
# No migration files needed -- schema is managed by GORM AutoMigrate at startup
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/tmiserver"]
# Build individual containers (faster for iterative development)
make build-container-tmi     # TMI server only
make build-container-db      # PostgreSQL only
make build-container-redis   # Redis only

# Build all containers
make build-containers

# Run locally
docker run -p 8080:8080 \
  -e TMI_DATABASE_URL="postgres://tmi_user:password@host.docker.internal:5432/tmi?sslmode=disable" \
  -e TMI_REDIS_HOST=host.docker.internal \
  -e TMI_JWT_SECRET=your-secret \
  tmi/tmi-server:latest

Note: Container builds use CGO_ENABLED=0 for static binaries, which excludes Oracle database support. Supported databases are PostgreSQL, MySQL, SQL Server, and SQLite. For Oracle support, use the dedicated Oracle container target (make build-container-oracle).

Heroku Deployment

Heroku provides the fastest path to production with managed PostgreSQL and Redis.

Quick Deployment

# Create Heroku app
heroku create my-tmi-server

# Provision addons
heroku addons:create heroku-postgresql:essential-0
heroku addons:create heroku-redis:mini

# Deploy
git push heroku main

Automated Configuration

TMI includes a setup script to automate Heroku configuration:

# Interactive configuration
make setup-heroku

# Or run directly
uv run scripts/setup-heroku-env.py

The script automatically:

  • Detects PostgreSQL and Redis credentials
  • Generates secure JWT secret
  • Configures OAuth callback URLs
  • Sets up WebSocket CORS origins
  • Prompts for OAuth provider credentials

Manual Configuration

If you prefer manual setup:

# Get database credentials (returns a postgres:// URL)
heroku pg:credentials:url DATABASE --app my-tmi-server

# Set environment variables
# TMI uses DATABASE_URL format (12-factor app pattern)
heroku config:set \
  TMI_DATABASE_URL="postgres://<user>:<password>@<host>:5432/<database>?sslmode=require" \
  TMI_JWT_SECRET=$(openssl rand -base64 32) \
  TMI_REDIS_HOST=<redis-host> \
  TMI_OAUTH_CALLBACK_URL="https://my-tmi-server.herokuapp.com/oauth2/callback" \
  --app my-tmi-server

# Configure OAuth (example with Google)
# OAuth providers use the OAUTH_PROVIDERS_ prefix (not TMI_ prefixed)
heroku config:set \
  OAUTH_PROVIDERS_GOOGLE_ENABLED=true \
  OAUTH_PROVIDERS_GOOGLE_CLIENT_ID=your-client-id \
  OAUTH_PROVIDERS_GOOGLE_CLIENT_SECRET=your-client-secret \
  OAUTH_PROVIDERS_GOOGLE_NAME=Google \
  OAUTH_PROVIDERS_GOOGLE_AUTHORIZATION_URL="https://accounts.google.com/o/oauth2/auth" \
  OAUTH_PROVIDERS_GOOGLE_TOKEN_URL="https://oauth2.googleapis.com/token" \
  OAUTH_PROVIDERS_GOOGLE_USERINFO_URL="https://www.googleapis.com/oauth2/v3/userinfo" \
  OAUTH_PROVIDERS_GOOGLE_SCOPES="openid,profile,email" \
  --app my-tmi-server

Note: OAuth providers are dynamically discovered from environment variables matching the pattern OAUTH_PROVIDERS_<PROVIDER>_ENABLED=true. The provider name is derived from the environment variable prefix (e.g., OAUTH_PROVIDERS_GOOGLE_* creates a provider with key google).

WebSocket and CORS Configuration for Heroku

Critical: Heroku requires specific configuration for WebSocket support.

  1. Port Binding -- The Procfile binds to Heroku's dynamically assigned port:
web: SERVER_PORT=$PORT bin/server

The server reads PORT from the environment when TMI_SERVER_PORT is not set (Heroku compatibility).

  1. CORS Origins -- Configure allowed origins for both REST API and WebSocket connections:
# Primary CORS configuration (controls API and general cross-origin access)
heroku config:set TMI_CORS_ALLOWED_ORIGINS="https://your-frontend.com,https://www.your-frontend.com" --app my-tmi-server

# WebSocket-specific allowed origins (checked during WebSocket upgrade)
heroku config:set WEBSOCKET_ALLOWED_ORIGINS="https://your-frontend.com,https://www.your-frontend.com" --app my-tmi-server
  1. Session Affinity (optional, for multi-dyno deployments):
heroku features:enable http-session-affinity --app my-tmi-server

Session affinity uses HTTP cookies to route reconnection attempts to the same dyno.

Scaling on Heroku

# Vertical scaling (dyno size)
heroku ps:resize web=standard-1x --app my-tmi-server

# Horizontal scaling (more dynos)
heroku ps:scale web=2 --app my-tmi-server

Monitoring Heroku Deployment

# View logs
heroku logs --tail --app my-tmi-server

# Check dyno status
heroku ps --app my-tmi-server

# Database info
heroku pg:info --app my-tmi-server

# Redis info
heroku redis:info --app my-tmi-server

Heroku Build Details

TMI uses the following files for Heroku deployment:

  • Procfile -- web: SERVER_PORT=$PORT bin/server. Heroku's Go buildpack names the binary server from the last path component of GO_INSTALL_PACKAGE_SPEC.
  • app.json -- Defines addons (PostgreSQL essential-0, Redis mini), environment variables, GO_INSTALL_PACKAGE_SPEC (github.com/ericfitz/tmi/cmd/server), and GOVERSION.

Database migrations run automatically on startup via GORM AutoMigrate. No manual migration step is needed.

Heroku Troubleshooting

Application Error (H10) -- Server failed to bind to $PORT:

# Verify Procfile uses SERVER_PORT=$PORT
cat Procfile

WebSocket CORS Error -- Add your frontend origin to the allowed origins:

heroku config:set TMI_CORS_ALLOWED_ORIGINS="https://your-frontend.com" --app my-tmi-server
heroku config:set WEBSOCKET_ALLOWED_ORIGINS="https://your-frontend.com" --app my-tmi-server

For detailed Heroku deployment documentation including cost estimation, security considerations, and CI/CD integration, see the Heroku Deployment Guide.

Docker Compose Deployment

Docker Compose is best suited for development and small production deployments.

Docker Compose Configuration

Create docker-compose.yml:

version: "3.8"

services:
  tmi:
    image: tmi-server:latest
    ports:
      - "8080:8080"
    environment:
      - TMI_DATABASE_URL=postgres://tmi_user:${POSTGRES_PASSWORD}@postgres:5432/tmi?sslmode=disable
      - TMI_REDIS_HOST=redis
      - TMI_REDIS_PORT=6379
      - TMI_REDIS_PASSWORD=${REDIS_PASSWORD}
      - TMI_JWT_SECRET=${JWT_SECRET}
      - TMI_OAUTH_CALLBACK_URL=https://tmi.example.com/oauth2/callback
      - TMI_CORS_ALLOWED_ORIGINS=https://tmi.example.com
      - TMI_LOG_LEVEL=info
      - TMI_LOG_IS_DEV=false
      # OAuth providers use non-TMI-prefixed env vars with dynamic discovery
      - OAUTH_PROVIDERS_GOOGLE_ENABLED=true
      - OAUTH_PROVIDERS_GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
      - OAUTH_PROVIDERS_GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
      - OAUTH_PROVIDERS_GOOGLE_NAME=Google
      - OAUTH_PROVIDERS_GOOGLE_AUTHORIZATION_URL=https://accounts.google.com/o/oauth2/auth
      - OAUTH_PROVIDERS_GOOGLE_TOKEN_URL=https://oauth2.googleapis.com/token
      - OAUTH_PROVIDERS_GOOGLE_USERINFO_URL=https://www.googleapis.com/oauth2/v3/userinfo
      - OAUTH_PROVIDERS_GOOGLE_SCOPES=openid,profile,email
    depends_on:
      - postgres
      - redis
    restart: unless-stopped

  postgres:
    image: postgres:15
    environment:
      - POSTGRES_USER=tmi_user
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=tmi
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    restart: unless-stopped
    ports:
      - "6379:6379"

volumes:
  postgres_data:
  redis_data:

Environment Variables (.env file)

POSTGRES_PASSWORD=secure_db_password
REDIS_PASSWORD=secure_redis_password
JWT_SECRET=your-very-secure-jwt-secret-key-minimum-32-characters
GOOGLE_CLIENT_ID=your-google-oauth-client-id
GOOGLE_CLIENT_SECRET=your-google-oauth-client-secret

Important: The .env file variables above are used for Docker Compose variable substitution (the ${...} references in the compose file). The TMI server itself reads TMI_-prefixed environment variables, which are set explicitly in the compose file's environment section.

Deployment Commands

# Start all services
docker-compose up -d

# View logs
docker-compose logs -f tmi

# Stop services
docker-compose down

# Update and restart
docker-compose pull
docker-compose up -d

Traditional Server Deployment

Use this method when deploying to a VPS, bare-metal server, or existing infrastructure where you manage the operating system directly.

System Setup

# Create dedicated user
sudo useradd -r -s /bin/false tmi

# Create directories
sudo mkdir -p /opt/tmi /etc/tmi /var/log/tmi
sudo chown tmi:tmi /var/log/tmi

Install Dependencies

PostgreSQL:

# Ubuntu/Debian
sudo apt update
sudo apt install postgresql postgresql-contrib

# CentOS/RHEL
sudo yum install postgresql-server postgresql-contrib
sudo postgresql-setup initdb

Redis:

# Ubuntu/Debian
sudo apt install redis-server

# CentOS/RHEL
sudo yum install epel-release
sudo yum install redis

Deploy TMI Server

# Copy binary and config
sudo cp tmiserver /opt/tmi/
sudo cp config-production.yml /etc/tmi/

# Set permissions
sudo chown -R tmi:tmi /opt/tmi
sudo chmod +x /opt/tmi/tmiserver

Note: No migration files need to be copied. TMI uses GORM AutoMigrate to manage the database schema automatically at startup.

Systemd Service

Create /etc/systemd/system/tmi.service:

[Unit]
Description=TMI Threat Modeling Server
After=network.target postgresql.service redis.service

[Service]
Type=simple
User=tmi
Group=tmi
WorkingDirectory=/opt/tmi
ExecStart=/opt/tmi/tmiserver --config=/etc/tmi/config-production.yml
Restart=always
RestartSec=5

# Environment variables (use TMI_ prefix)
Environment=TMI_DATABASE_URL=postgres://tmi_user:your-db-password@localhost:5432/tmi?sslmode=disable
Environment=TMI_JWT_SECRET=your-secure-jwt-secret
Environment=TMI_REDIS_HOST=localhost
Environment=TMI_OAUTH_CALLBACK_URL=https://api.tmi.example.com/oauth2/callback
Environment=TMI_CORS_ALLOWED_ORIGINS=https://tmi.example.com
# OAuth providers (use OAUTH_PROVIDERS_ prefix, not TMI_)
Environment=OAUTH_PROVIDERS_GOOGLE_ENABLED=true
Environment=OAUTH_PROVIDERS_GOOGLE_CLIENT_ID=your-google-client-id
Environment=OAUTH_PROVIDERS_GOOGLE_CLIENT_SECRET=your-google-secret

# Security settings
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/log/tmi
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Start Service

# Reload systemd
sudo systemctl daemon-reload

# Enable and start
sudo systemctl enable tmi
sudo systemctl start tmi

# Check status
sudo systemctl status tmi

# View logs
sudo journalctl -u tmi -f

Nginx Reverse Proxy

Create /etc/nginx/sites-available/tmi:

server {
    listen 443 ssl http2;
    server_name api.tmi.example.com;

    ssl_certificate /etc/ssl/certs/tmi.crt;
    ssl_private_key /etc/ssl/private/tmi.key;

    # 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://127.0.0.1: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";

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

# HTTP to HTTPS redirect
server {
    listen 80;
    server_name api.tmi.example.com;
    return 301 https://$server_name$request_uri;
}

Enable the site and restart Nginx:

sudo ln -s /etc/nginx/sites-available/tmi /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

Kubernetes Deployment

Use Kubernetes for large-scale, cloud-native deployments that require horizontal scaling and automated orchestration.

Namespace

# namespace.yml
apiVersion: v1
kind: Namespace
metadata:
  name: tmi

ConfigMap

# configmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: tmi-config
  namespace: tmi
data:
  TMI_REDIS_HOST: "redis"
  TMI_REDIS_PORT: "6379"
  TMI_OAUTH_CALLBACK_URL: "https://tmi.example.com/oauth2/callback"
  TMI_CORS_ALLOWED_ORIGINS: "https://tmi.example.com"
  TMI_LOG_LEVEL: "info"
  TMI_LOG_IS_DEV: "false"

Secrets

# secrets.yml
apiVersion: v1
kind: Secret
metadata:
  name: tmi-secrets
  namespace: tmi
type: Opaque
data:
  TMI_DATABASE_URL: <base64-encoded-database-url>
  TMI_JWT_SECRET: <base64-encoded-jwt-secret>
  TMI_REDIS_PASSWORD: <base64-encoded-redis-password>
  OAUTH_PROVIDERS_GOOGLE_ENABLED: <base64-encoded: dHJ1ZQ==>
  OAUTH_PROVIDERS_GOOGLE_CLIENT_ID: <base64-encoded-client-id>
  OAUTH_PROVIDERS_GOOGLE_CLIENT_SECRET: <base64-encoded-client-secret>

Note: The TMI_DATABASE_URL value should be a full connection string, e.g., postgres://user:pass@postgres:5432/tmi?sslmode=require.

Deployment

# deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tmi-server
  namespace: tmi
spec:
  replicas: 3
  selector:
    matchLabels:
      app: tmi-server
  template:
    metadata:
      labels:
        app: tmi-server
    spec:
      containers:
        - name: tmi-server
          image: tmi-server:latest
          ports:
            - containerPort: 8080
          envFrom:
            - configMapRef:
                name: tmi-config
            - secretRef:
                name: tmi-secrets
          livenessProbe:
            httpGet:
              path: /
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
          resources:
            requests:
              memory: "256Mi"
              cpu: "100m"
            limits:
              memory: "512Mi"
              cpu: "500m"

Service

# service.yml
apiVersion: v1
kind: Service
metadata:
  name: tmi-service
  namespace: tmi
spec:
  selector:
    app: tmi-server
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP

Ingress

# ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tmi-ingress
  namespace: tmi
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    nginx.ingress.kubernetes.io/websocket-services: "tmi-service"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.tmi.example.com
      secretName: tmi-tls
  rules:
    - host: api.tmi.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: tmi-service
                port:
                  number: 80

Deploy to Kubernetes

# Apply all manifests
kubectl apply -f namespace.yml
kubectl apply -f configmap.yml
kubectl apply -f secrets.yml
kubectl apply -f deployment.yml
kubectl apply -f service.yml
kubectl apply -f ingress.yml

# Check deployment
kubectl get pods -n tmi
kubectl logs -f deployment/tmi-server -n tmi

# Scale deployment
kubectl scale deployment/tmi-server --replicas=5 -n tmi

Configuration

Environment Variables

All configuration can be set via TMI_-prefixed environment variables. OAuth provider configuration uses a separate dynamic discovery pattern (see below).

Core Configuration

Variable Required Default Description
TMI_SERVER_PORT No 8080 HTTP server port (also reads PORT for Heroku compat)
TMI_SERVER_INTERFACE No 0.0.0.0 Interface to bind
TMI_SERVER_BASE_URL No (auto) Public base URL for callbacks
TMI_SERVER_TLS_ENABLED No false Enable HTTPS
TMI_SERVER_TLS_CERT_FILE No - TLS certificate path
TMI_SERVER_TLS_KEY_FILE No - TLS key path
TMI_DATABASE_URL Yes - Database connection URL (e.g., postgres://user:pass@host:5432/db?sslmode=require)
TMI_REDIS_URL No - Redis connection URL (takes precedence over individual fields)
TMI_REDIS_HOST Yes* localhost Redis host (*required if TMI_REDIS_URL not set)
TMI_REDIS_PORT No 6379 Redis port
TMI_REDIS_PASSWORD No - Redis password
TMI_REDIS_DB No 0 Redis database number
TMI_JWT_SECRET Yes - JWT signing secret
TMI_JWT_EXPIRATION_SECONDS No 3600 JWT token expiration (seconds)
TMI_JWT_SIGNING_METHOD No HS256 JWT signing method
TMI_OAUTH_CALLBACK_URL Yes - OAuth callback URL
TMI_CORS_ALLOWED_ORIGINS No - Comma-separated allowed CORS origins
TMI_LOG_LEVEL No info Log level (debug, info, warn, error)
TMI_LOG_IS_DEV No true Development mode logging
TMI_LOG_DIR No logs Directory for log files
TMI_OPERATOR_NAME No - Operator/maintainer name
TMI_OPERATOR_CONTACT No - Operator contact info
TMI_SECRETS_PROVIDER No env Secret provider (env, aws, oci)
WEBSOCKET_ALLOWED_ORIGINS No - WebSocket-specific allowed origins

Connection Pool Settings

Variable Required Default Description
TMI_DB_MAX_OPEN_CONNS No 10 Maximum open database connections
TMI_DB_MAX_IDLE_CONNS No 2 Maximum idle connections
TMI_DB_CONN_MAX_LIFETIME No 240 Max connection lifetime (seconds)
TMI_DB_CONN_MAX_IDLE_TIME No 30 Max idle time (seconds)

OAuth Provider Configuration (Dynamic Discovery)

OAuth providers are configured via environment variables with the pattern OAUTH_PROVIDERS_<PROVIDER>_*. The server dynamically discovers providers by scanning for OAUTH_PROVIDERS_<PROVIDER>_ENABLED=true.

Variable Pattern Description
OAUTH_PROVIDERS_<PROVIDER>_ENABLED Set to true to enable the provider
OAUTH_PROVIDERS_<PROVIDER>_CLIENT_ID OAuth client ID
OAUTH_PROVIDERS_<PROVIDER>_CLIENT_SECRET OAuth client secret
OAUTH_PROVIDERS_<PROVIDER>_NAME Display name
OAUTH_PROVIDERS_<PROVIDER>_AUTHORIZATION_URL OAuth authorization endpoint
OAUTH_PROVIDERS_<PROVIDER>_TOKEN_URL OAuth token endpoint
OAUTH_PROVIDERS_<PROVIDER>_USERINFO_URL User info endpoint
OAUTH_PROVIDERS_<PROVIDER>_SCOPES Comma-separated scopes
OAUTH_PROVIDERS_<PROVIDER>_ICON Icon identifier

Where <PROVIDER> is an uppercase identifier like GOOGLE, GITHUB, or MICROSOFT.

Administrator Configuration (Environment Variables)

For Heroku or single-administrator deployments:

Variable Description
TMI_ADMIN_PROVIDER OAuth/SAML provider ID (e.g., google)
TMI_ADMIN_PROVIDER_ID Provider's unique user ID
TMI_ADMIN_EMAIL Admin email (optional)
TMI_ADMIN_SUBJECT_TYPE user or group (default: user)

Configuration File

You can also use a YAML configuration file instead of (or alongside) environment variables. The configuration structure uses a database.url field containing a connection string URL rather than individual host/port/user/password fields:

# config-production.yml
server:
  port: "8080"
  interface: "0.0.0.0"
  tls_enabled: true
  tls_cert_file: "/etc/tmi/certs/server.crt"
  tls_key_file: "/etc/tmi/certs/server.key"
  tls_subject_name: "tmi.yourdomain.com"
  http_to_https_redirect: true

database:
  url: ""  # Set via TMI_DATABASE_URL environment variable; never store passwords in config files
  connection_pool:
    max_open_conns: 25
    max_idle_conns: 25
    conn_max_lifetime: 300
    conn_max_idle_time: 300
  redis:
    host: "redis.example.com"
    port: "6379"
    password: ""  # Set via TMI_REDIS_PASSWORD
    db: 0

auth:
  jwt:
    secret: ""  # Set via TMI_JWT_SECRET
    expiration_seconds: 3600
    signing_method: "HS256"
  oauth:
    callback_url: "https://tmi.example.com/oauth2/callback"
    providers:
      google:
        id: "google"
        name: "Google"
        enabled: true
        icon: "fa-brands fa-google"
        client_id: ""  # Set via env var
        client_secret: ""  # Set via env var
        authorization_url: "https://accounts.google.com/o/oauth2/auth"
        token_url: "https://oauth2.googleapis.com/token"
        userinfo:
          - url: "https://www.googleapis.com/oauth2/v3/userinfo"
            claims: {}
        issuer: "https://accounts.google.com"
        scopes:
          - openid
          - profile
          - email

websocket:
  inactivity_timeout_seconds: 600

logging:
  level: "warn"
  is_dev: false
  log_dir: "/var/log/tmi"
  max_age_days: 30
  max_size_mb: 500

operator:
  name: "TMI Production Instance"
  contact: "admin@yourdomain.com"

A full example configuration is available at config-example.yml in the repository root.

To start the server with a config file:

./tmiserver --config=/etc/tmi/config-production.yml

You can also generate a configuration template:

./tmiserver --generate-config

Health Checks

TMI provides health check endpoints that do not require authentication:

# Root endpoint (returns API info with health status)
curl http://localhost:8080/

# API server info (alias for root endpoint)
curl http://localhost:8080/api/server-info

# OAuth providers listing
curl http://localhost:8080/oauth2/providers

Expected response from / (root endpoint):

{
  "status": {
    "code": "OK",
    "time": "2025-12-01T12:00:00Z"
  },
  "service": {
    "build": "1.4.0-rc.0+abc1234",
    "name": "TMI"
  },
  "api": {
    "specification": "https://github.com/ericfitz/tmi/blob/main/api-schema/tmi-openapi.json",
    "version": "1.4.0"
  }
}

The status.code field reflects database and Redis health: OK, DEGRADED, or UNKNOWN. When degraded, a health object is included with component-level details.

Note: There is no /version endpoint. The root endpoint (/) serves as the health check and provides version information. When accessed from a browser (Accept: text/html), it returns an HTML page; API clients receive JSON.

Troubleshooting

Server Won't Start

Check the configuration and logs:

# Start the server with a config file to see errors in the console
./tmiserver --config=config-production.yml

# Check logs if running under systemd
journalctl -u tmi -n 50

Common issues:

  • Missing TMI_JWT_SECRET
  • Missing or invalid TMI_DATABASE_URL
  • Redis not reachable (TMI_REDIS_HOST)
  • Invalid OAuth credentials

WebSocket Connections Fail

Check CORS configuration:

# Verify allowed origins
echo $TMI_CORS_ALLOWED_ORIGINS
echo $WEBSOCKET_ALLOWED_ORIGINS

# WebSocket connections use ticket-based auth (not Bearer tokens).
# First, obtain a ticket via GET /ws/ticket with a valid JWT,
# then connect with the ticket as a query parameter.
wscat -c "ws://localhost:8080/ws/notifications?ticket=YOUR_TICKET"

Heroku-specific: Verify that SERVER_PORT=$PORT is set in the Procfile.

Database Connection Issues

# Test the PostgreSQL connection using your DATABASE_URL
psql "$TMI_DATABASE_URL" -c "SELECT 1"

# Migrations run automatically at startup via GORM AutoMigrate.
# Check the server logs for migration errors:
journalctl -u tmi -n 100 | grep -i "migrate\|schema"

Performance Tuning

Connection Pooling

Connection pool settings are configured via environment variables or the YAML config:

# Environment variables
TMI_DB_MAX_OPEN_CONNS=25      # Default: 10
TMI_DB_MAX_IDLE_CONNS=10      # Default: 2
TMI_DB_CONN_MAX_LIFETIME=300  # Default: 240 (seconds)
TMI_DB_CONN_MAX_IDLE_TIME=60  # Default: 30 (seconds)
# In config YAML
database:
  connection_pool:
    max_open_conns: 25
    max_idle_conns: 10
    conn_max_lifetime: 300
    conn_max_idle_time: 60

Redis Optimization

# Monitor Redis performance
redis-cli --latency-history

# Check memory usage
redis-cli info memory

Server Tuning

Adjust timeouts in configuration (or via TMI_SERVER_READ_TIMEOUT, TMI_SERVER_WRITE_TIMEOUT, TMI_SERVER_IDLE_TIMEOUT):

server:
  read_timeout: 10s    # Default: 5s
  write_timeout: 30s   # Default: 10s
  idle_timeout: 2m0s   # Default: 60s

Security Considerations

  1. Always use HTTPS in production -- Set TMI_SERVER_TLS_ENABLED=true or terminate TLS at a reverse proxy.
  2. Generate strong JWT secrets -- Use 32+ cryptographically random characters: openssl rand -base64 32.
  3. Use SSL for database connections -- Include ?sslmode=require in your TMI_DATABASE_URL.
  4. Restrict database and Redis network access -- Apply firewall rules to limit exposure.
  5. Keep OAuth secrets secure -- Store them in environment variables or an external secrets provider such as AWS Secrets Manager or OCI Vault.
  6. Enable rate limiting -- The server includes user-based rate limiting; also configure rate limiting at the load balancer.
  7. Configure CORS origins -- Set TMI_CORS_ALLOWED_ORIGINS explicitly. Do not leave it empty in production.
  8. Apply regular security updates -- Keep Go, PostgreSQL, Redis, and the operating system up to date.

Next Steps

Related Pages

For comprehensive deployment documentation including troubleshooting, performance tuning, and monitoring, see the Deployment Guide in the repository.

Clone this wiki locally