This document describes how to deploy TGM Manager as a single-tenant, on-premise application.

Table of Contents


Overview

TGM Manager supports two deployment models:

SaaS (Multi-Tenant) On-Premise (Single-Tenant)
Databases One per client, routed dynamically Single database
Multi-tenancy Enabled (multitenancy.database.enabled=true) Disabled
License Managed per-client by platform admin Installed locally, enforced on every request
Infrastructure Hosted by EN Solutions Customer-managed
Profile prod onprem

The on-premise mode disables database-level multi-tenancy while keeping all other features intact: license enforcement, sandbox environments (schema-based), cron jobs, file storage, and the full API surface.


Architecture

┌─────────────────────────────────────────────────────────────────┐
│                    On-Premise Deployment                         │
│                                                                  │
│  ┌─────────────┐  ┌──────────┐  ┌──────────┐  ┌─────────────┐ │
│  │ TGM Manager │  │PostgreSQL│  │  Redis   │  │  RabbitMQ   │ │
│  │  (app:1337) │  │  (pg15)  │  │  (cache) │  │  (events)   │ │
│  └──────┬──────┘  └────┬─────┘  └────┬─────┘  └──────┬──────┘ │
│         │              │             │                │         │
│         └──────────────┴─────────────┴────────────────┘         │
│                              │                                   │
│                       ┌──────┴──────┐                            │
│                       │    MinIO    │                            │
│                       │  (storage)  │                            │
│                       └─────────────┘                            │
└─────────────────────────────────────────────────────────────────┘

Services included:

Service Image Purpose
tgm-manager Custom (Dockerfile) Application server
postgres pgvector/pgvector:pg15 Primary database with vector search support
redis redis:7-alpine Caching (license payload, sessions, data)
rabbitmq rabbitmq:3.12-management-alpine Event processing and async jobs
minio minio/minio:latest File and document storage

Not included by default (can be added): - InfluxDB (time-series metrics) - OCR service (document text extraction)


Prerequisites

  • Docker Engine 20.10+ and Docker Compose v2
  • At least 4 GB RAM available for containers
  • A license key and public key provided by EN Solutions
  • Network access between all containers (handled by Docker networking)

Quick Start

Step 1: Create the environment file

cp .env.onprem.template .env.onprem

Step 2: Generate security keys

Each deployment must have unique cryptographic keys:

# Database password
echo "DATABASE_PASSWORD=$(openssl rand -base64 32)" >> .env.onprem

# JWT and encryption keys
echo "JWT_SECRET=$(openssl rand -hex 32)" >> .env.onprem
echo "API_TOKEN_SALT=$(openssl rand -hex 32)" >> .env.onprem
echo "TRANSFER_TOKEN_SALT=$(openssl rand -hex 32)" >> .env.onprem
echo "ENCRYPTION_KEY=$(openssl rand -hex 16)" >> .env.onprem

# RabbitMQ password
echo "RABBIT_PASS=$(openssl rand -base64 24)" >> .env.onprem

# MinIO credentials
echo "MINIO_ACCESS_KEY=$(openssl rand -base64 20)" >> .env.onprem
echo "MINIO_SECRET_KEY=$(openssl rand -base64 32)" >> .env.onprem

Step 3: Add the license public key

EN Solutions will provide an RSA public key with your license. Add it to .env.onprem:

LICENSE_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
...your public key from EN Solutions...
-----END PUBLIC KEY-----"

Step 4: Start the services

docker compose -f docker-compose.onprem.yml --env-file .env.onprem up -d

Step 5: Verify the deployment

# Check all containers are running
docker compose -f docker-compose.onprem.yml ps

# Check application health
curl http://localhost:1337/actuator/health

# Check license status (should show isValid: false until license installed)
curl http://localhost:1337/custom/license-status

Step 6: Install your license

curl -X PUT http://localhost:1337/custom/xywezelicense \
  -H "Content-Type: application/json" \
  -d '{"licenseKey": "YOUR_SIGNED_LICENSE_KEY_FROM_ENSOLUTIONS"}'

Expected response:

{
  "success": true,
  "data": {
    "success": true,
    "message": "License installed successfully",
    "licenseId": "abc-123-def",
    "companyName": "Your Company",
    "type": "enterprise",
    "expiresAt": "2027-12-31T00:00:00Z"
  }
}

The application is now ready to use.


Configuration Reference

Required Environment Variables

Variable Description Generation
DATABASE_PASSWORD PostgreSQL password openssl rand -base64 32
JWT_SECRET JWT signing secret openssl rand -hex 32
API_TOKEN_SALT API token salt openssl rand -hex 32
TRANSFER_TOKEN_SALT Transfer token salt openssl rand -hex 32
ENCRYPTION_KEY Data encryption key openssl rand -hex 16
LICENSE_PUBLIC_KEY RSA public key (PEM format) Provided by EN Solutions

Optional Environment Variables

Variable Default Description
PORT 1337 Application port
DATABASE_NAME tgm_manager Database name
DATABASE_USERNAME postgres Database user
DATABASE_EXTERNAL_PORT 5432 PostgreSQL port on host
REDIS_PASSWORD (none) Redis password
RABBIT_USER guest RabbitMQ username
RABBIT_PASS guest RabbitMQ password
MINIO_ACCESS_KEY minioadmin MinIO access key
MINIO_SECRET_KEY minioadmin MinIO secret key
MINIO_BUCKET tgm-uploads MinIO bucket name
EMAIL_ENABLED false Enable email notifications
CRON_ENABLED true Enable scheduled jobs
LOG_FILE logs/tgm-manager.log Log file path

Profile Settings (application-onprem.yml)

The onprem profile automatically sets:

multitenancy.database.enabled: false    # Single database, no routing
app.license.enabled: true               # License enforcement active
spring.jpa.hibernate.ddl-auto: validate # Schema managed by Flyway only
spring.flyway.clean-disabled: true      # Prevent accidental data loss

License Setup

How License Enforcement Works

In on-prem mode, every API request (except public endpoints) passes through the LicenseEnforcementFilter:

Request → JWT Authentication → License Validation → Controller
                                      │
                                      ├─ No license installed → 403 NO_LICENSE
                                      ├─ License expired      → 403 LICENSE_EXPIRED
                                      ├─ Signature invalid    → 403 INVALID_SIGNATURE
                                      └─ License valid        → Request proceeds

Public endpoints (no license required): - /auth/** — Authentication - /custom/license-info — Check license details - /custom/license-status — Check license validity - /custom/xywezelicense — Install/update license - /actuator/health — Health check - /swagger-ui/** — API documentation

Checking License Status

# License validity and limits
curl http://localhost:1337/custom/license-status
{
  "success": true,
  "data": {
    "isValid": true,
    "message": "License is active",
    "licenseEnabled": true,
    "expiresAt": "2027-12-31T00:00:00",
    "users": {
      "current": 12,
      "max": 50,
      "remaining": 38,
      "limitReached": false
    },
    "units": {
      "current": 45,
      "max": 100,
      "remaining": 55,
      "limitReached": false
    }
  }
}
# Full license details
curl http://localhost:1337/custom/license-info
{
  "success": true,
  "data": {
    "status": "active",
    "type": "enterprise",
    "companyName": "Your Company",
    "maxUsers": 50,
    "maxUnits": 100,
    "features": ["ai", "iot", "export", "webhooks"],
    "expiresAt": "2027-12-31T00:00:00",
    "daysRemaining": 695
  }
}

Updating a License

When EN Solutions provides a renewed license key:

curl -X PUT http://localhost:1337/custom/xywezelicense \
  -H "Content-Type: application/json" \
  -d '{"licenseKey": "NEW_LICENSE_KEY"}'

No restart required — the license cache is cleared automatically.


Request Lifecycle

Understanding how requests flow through the system in on-prem mode:

HTTP Request
    │
    ▼
┌─ TenantFilter ─────────────────────────────────────────────────┐
│  Always active. Resolves schema for sandbox support.            │
│  Default: "public". Can be overridden with X-Tenant-ID header. │
└────────────────────────────────────┬────────────────────────────┘
                                     │
    ┌────────────────────────────────┘
    │   ClientFilter is NOT loaded (multitenancy disabled).
    │   ClientContext stays on "master" — single database used.
    ▼
┌─ ApiTokenAuthenticationFilter ─────────────────────────────────┐
│  If token starts with "tgm_" → API token auth.                 │
│  Otherwise → skip to JWT filter.                               │
└────────────────────────────────────┬────────────────────────────┘
                                     ▼
┌─ JwtAuthenticationFilter ──────────────────────────────────────┐
│  Validates Bearer JWT token.                                    │
│  Sets SecurityContext with authenticated user.                  │
└────────────────────────────────────┬────────────────────────────┘
                                     ▼
┌─ LicenseEnforcementFilter ─────────────────────────────────────┐
│  Validates license on every protected request.                  │
│  Payload cached for 5 minutes (Redis).                         │
│  If invalid → 403 with errorType JSON.                         │
└────────────────────────────────────┬────────────────────────────┘
                                     ▼
┌─ Spring Security ──────────────────────────────────────────────┐
│  Checks: anyRequest().authenticated()                          │
└────────────────────────────────────┬────────────────────────────┘
                                     ▼
              Controller → Service → Repository → PostgreSQL

Key Differences from Multi-Tenant Mode

Component On-Prem Multi-Tenant
ClientFilter Not loaded Routes requests to client databases
ClientContext Always "master" Set per-request from subdomain/header
DataSource Single HikariCP pool (20 connections) Routing DataSource with per-client pools
MultiTenantDataSourceConfig Not loaded Creates per-client connection pools
TenantFilter (sandboxes) Active — sandbox schemas still work Active
Company filtering None (single tenant) None (isolation is at database level)

Sandbox Support

Even in on-prem mode, schema-based sandboxes are available. You can create sandbox environments for testing within the same database:

# Requests to a sandbox schema
curl -H "X-Tenant-ID: sandbox_dev" \
     -H "Authorization: Bearer $JWT" \
     http://localhost:1337/api/units

Optional Services

Enabling InfluxDB (Time-Series Metrics)

Add to .env.onprem:

ENABLE_INFLUXDB=true
INFLUXDB_TOKEN=your-influxdb-token
INFLUXDB_ORG=your-org
INFLUXDB_BUCKET=tgm-metrics

Then add an InfluxDB container to your deployment or point to an external instance.

Enabling Email Notifications

Add to .env.onprem:

EMAIL_ENABLED=true
SENDGRID_API_KEY=SG.your-api-key
EMAIL_FROM=noreply@yourcompany.com
EMAIL_REPLY_TO=support@yourcompany.com

Enabling OCR (Document Text Extraction)

Add to .env.onprem:

OCR_ENABLED=true
OCR_SERVICE_URL=http://ocr-service:8000

Then add the OCR container to your deployment:

ocr-service:
  image: blazordevlab/paddleocrapi:latest
  container_name: tgm-ocr-service
  ports:
    - "8000:8000"
  networks:
    - tgm-network

Logging

The on-prem profile writes logs to a file with automatic rotation:

Setting Value
Log file logs/tgm-manager.log (configurable via LOG_FILE)
Max file size 50 MB
History 30 rotated files
Total cap 1 GB
App log level INFO
Root log level WARN

Logs are also available via Docker:

# Live logs
docker logs -f tgm-manager-server

# Last 100 lines
docker logs --tail 100 tgm-manager-server

The logs-data Docker volume persists log files across container restarts.


Backups

Database

# Backup
docker exec tgm-postgres pg_dump -U postgres tgm_manager > backup_$(date +%Y%m%d).sql

# Restore
cat backup_20260201.sql | docker exec -i tgm-postgres psql -U postgres tgm_manager

File Storage (MinIO)

# Using MinIO client
docker run --rm --network tgm-network \
  minio/mc alias set local http://minio:9000 $MINIO_ACCESS_KEY $MINIO_SECRET_KEY
docker run --rm --network tgm-network \
  minio/mc mirror local/tgm-uploads /backup/minio/

Full Backup Script

#!/bin/bash
BACKUP_DIR="/backups/tgm/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"

# Database
docker exec tgm-postgres pg_dump -U postgres tgm_manager | gzip > "$BACKUP_DIR/db.sql.gz"

# Volumes
docker run --rm -v tgm-manager-server_minio-data:/data -v "$BACKUP_DIR":/backup \
  alpine tar czf /backup/minio-data.tar.gz /data

echo "Backup complete: $BACKUP_DIR"

Upgrading

Standard Upgrade

# Pull latest image / rebuild
docker compose -f docker-compose.onprem.yml --env-file .env.onprem build

# Restart with new image (Flyway runs migrations automatically)
docker compose -f docker-compose.onprem.yml --env-file .env.onprem up -d

Flyway migrations are applied automatically on startup. The onprem profile uses ddl-auto: validate to ensure the schema matches the entity model after migrations run.

Before Upgrading

  1. Back up the database (see Backups)
  2. Check the release notes for breaking changes
  3. Verify the new version is compatible with your license

Troubleshooting

Application won't start

# Check container logs
docker logs tgm-manager-server

# Check if database is ready
docker exec tgm-postgres pg_isready -U postgres

"No valid license found" on all API calls

The license has not been installed yet. Install it:

curl -X PUT http://localhost:1337/custom/xywezelicense \
  -H "Content-Type: application/json" \
  -d '{"licenseKey": "YOUR_LICENSE_KEY"}'

"License signature verification failed"

  • The LICENSE_PUBLIC_KEY in .env.onprem does not match the key used to sign the license
  • Verify with EN Solutions that you have the correct public key for your license

"License has expired"

Contact EN Solutions for a renewed license key. Install the new key — no restart required:

curl -X PUT http://localhost:1337/custom/xywezelicense \
  -H "Content-Type: application/json" \
  -d '{"licenseKey": "NEW_LICENSE_KEY"}'

Database connection refused

# Check PostgreSQL container health
docker inspect --format='{{.State.Health.Status}}' tgm-postgres

# Verify the password matches
docker exec tgm-postgres psql -U postgres -c "SELECT 1"

Port conflicts

If default ports are in use, change them in .env.onprem:

DATABASE_EXTERNAL_PORT=5433
REDIS_EXTERNAL_PORT=6380
RABBIT_EXTERNAL_PORT=5673
MINIO_API_PORT=9002
MINIO_CONSOLE_PORT=9003
PORT=8080

Health check

curl http://localhost:1337/actuator/health | python3 -m json.tool

Expected output shows UP status for db, redis, rabbit, and diskSpace components.