This document describes how to deploy TGM Manager as a single-tenant, on-premise application.
Table of Contents¶
- Overview
- Architecture
- Prerequisites
- Quick Start
- Configuration Reference
- License Setup
- Request Lifecycle
- Optional Services
- Logging
- Backups
- Upgrading
- Troubleshooting
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¶
- Back up the database (see Backups)
- Check the release notes for breaking changes
- 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_KEYin.env.onpremdoes 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.