Skip to content

Tenant Administration

Day-to-day management of tenants in Mantis.

Tenant administration covers ongoing management tasks:

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/tenants"

Response:

{
"data": [
{
"id": "019b937d-4862-84ae-9c2a-6ac230cc4c7e",
"name": "Acme Corp",
"description": "Enterprise customer",
"contact_email": "ops@acme.com",
"created_at": "2024-01-15T10:30:00Z"
},
{
"id": "019b937d-4862-84ae-9c2a-6ac230cc4c80",
"name": "Globex Inc",
"description": "SMB customer",
"contact_email": "admin@globex.com",
"created_at": "2024-01-20T14:00:00Z"
}
],
"meta": {
"total": 2,
"page": 1,
"limit": 20
}
}
Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/tenants/$TENANT_ID"

Response includes relationships:

{
"id": "019b937d-4862-84ae-9c2a-6ac230cc4c7e",
"name": "Acme Corp",
"description": "Enterprise customer",
"contact_email": "ops@acme.com",
"logo_url": "https://acme.com/logo.png",
"tags": [
{ "id": "tag_region", "key": "region", "value": "us-east" },
{ "id": "tag_tier", "key": "tier", "value": "enterprise" }
],
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-02-01T09:15:00Z"
}
Terminal window
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corporation",
"description": "Updated enterprise customer description",
"contact_email": "newops@acme.com"
}' \
"https://api.mantis.local/api/v1/tenants/$TENANT_ID"
FieldUpdate Behavior
nameMust remain unique
descriptionSet to null to clear
contact_emailSet to null to clear
logo_urlSet to null to clear
  1. Navigate to TenancyTenants
  2. Click on the tenant name
  3. Click Edit
  4. Update fields
  5. Click Save Changes
Terminal window
# List solutions
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/tenants/$TENANT_ID/solutions"
# List targets
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/tenants/$TENANT_ID/targets"
# List environments
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/tenants/$TENANT_ID/environments"
Terminal window
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"solution_id": "sol_newfeature",
"environment_id": "env_staging"
}' \
"https://api.mantis.local/api/v1/tenants/$TENANT_ID/solutions"
Terminal window
curl -X DELETE \
-H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/tenants/$TENANT_ID/solutions/$SOLUTION_ID/environments/$ENV_ID"

Document tenant entitlements:

TenantSolutionProductionStagingDevelopment
Acme CorpWebApp
Acme CorpAPI
Globex IncWebApp

Tenant variables live in variable sets, scoped with scope_tenant_id. There is no /tenants/{id}/variables endpoint.

Terminal window
# List variables in a set
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/variable-sets/$VARIABLE_SET_ID/variables"
# Update one
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "https://api-v2.acme.com"}' \
"https://api.mantis.local/api/v1/variable-sets/$VARIABLE_SET_ID/variables/$VARIABLE_ID"
# Delete one
curl -X DELETE \
-H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/variable-sets/$VARIABLE_SET_ID/variables/$VARIABLE_ID"

Rotating a secret is an update with is_encrypted: true; reads return "<ENCRYPTED>" instead of the value, so the plaintext never leaves the database.

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/tenants/$TENANT_ID/deployments"

Response:

{
"data": [
{
"id": "019b937d-4862-8de0-1111-222233334444",
"source_type": "solution",
"solution_name": "Solution 019b937d-4862-8de0-1111-222233334444",
"state": "success",
"progress": 1.0,
"tenant_name": "Acme Corp",
"environment_name": "Production",
"created_at": "2024-02-01T10:00:00Z",
"completed_at": "2024-02-01T10:05:32Z",
"duration_ms": 332000
}
],
"meta": {
"total": 150,
"page": 1,
"limit": 20
}
}
Terminal window
# Pagination
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/tenants/$TENANT_ID/deployments?page=2&limit=50"

State-based filtering is not currently supported on this endpoint. Use the SQL monitoring queries below for status-filtered analysis.

Track tenant health with deployment metrics:

SELECT
DATE_TRUNC('day', created_at) AS day,
COUNT(*) AS total_deployments,
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS successful,
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
AVG(EXTRACT(EPOCH FROM (completed_at - started_at))) AS avg_duration_secs
FROM deployment_history
WHERE tenant_id = $tenant_id
AND created_at >= NOW() - INTERVAL '30 days'
GROUP BY DATE_TRUNC('day', created_at)
ORDER BY day DESC;
Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/admin/users"

System admins see all users; a tenant-scoped caller sees users in their tenant plus tenant-less (global) users (tenant_id IS NULL), resolved by effective_tenant_scope. There is an admin-only tenant_id query parameter to narrow the listing; it is ignored for tenant-scoped callers.

Terminal window
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "bob",
"email": "bob@acme.com",
"password": "secure_password",
"tenant_id": "019b937d-4862-84ae-9c2a-6ac230cc4c7e",
"role_ids": ["role_viewer"]
}' \
"https://api.mantis.local/api/v1/admin/users"
Terminal window
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "ten_new_tenant"
}' \
"https://api.mantis.local/api/v1/admin/users/$USER_ID"

Disable rather than delete for audit retention:

Terminal window
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "inactive"}' \
"https://api.mantis.local/api/v1/admin/users/$USER_ID"

Audit logs are read from /api/v1/audit/logs. A tenant-scoped caller sees their tenant’s entries plus global/system entries (tenant_id IS NULL); an admin with the admin role and no tenant sees all tenants. An admin-only tenant_id query parameter narrows the listing (it is ignored for tenant-scoped callers).

Terminal window
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/audit/logs"
Terminal window
# Authentication events
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/audit/logs?category=authentication"
# Deployment events
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/audit/logs?category=deployment"
# Security events
curl -H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/audit/logs?severity=security"

audit_log_entries enforces row-level security, so the scope must be declared or the export silently produces a file containing only system rows. Use \copy from psql — server-side COPY TO <file> needs superuser or pg_write_server_files.

SET mantis.audit_tenant_id = '<tenant-uuid>';
\copy (
SELECT
occurred_at,
event_type,
event_category,
severity,
actor_id,
action,
outcome,
metadata
FROM audit_log_entries
WHERE tenant_id = '<tenant-uuid>'
AND occurred_at >= '2024-01-01'
AND occurred_at < '2024-02-01'
ORDER BY occurred_at
) TO 'audit_export.csv' WITH CSV HEADER
□ Create tenant
□ Configure description and contact
□ Attach required solutions to environments
□ Assign targets
□ Set up variables (including encrypted secrets)
□ Add tags for organization
□ Create initial users
□ Configure SSO (if applicable)
□ Document tenant configuration
□ Notify tenant of access

When a tenant needs to be temporarily disabled:

Terminal window
# Remove all solution entitlements
for SOL_ENV in $(get_tenant_solutions); do
curl -X DELETE "$API_URL/tenants/$TENANT_ID/solutions/$SOL_ENV"
done
# Disable all tenant users (valid statuses: active, inactive, locked, pending)
for USER_ID in $(get_tenant_users); do
curl -X PUT -d '{"status": "inactive"}' \
"https://api.mantis.local/api/v1/admin/users/$USER_ID"
done
Terminal window
# Re-enable users
for USER_ID in $(get_tenant_users); do
curl -X PUT -d '{"status": "active"}' \
"https://api.mantis.local/api/v1/admin/users/$USER_ID"
done
# Restore entitlements
curl -X POST -d '{"solution_id": "sol_...", "environment_id": "env_..."}' \
"$API_URL/tenants/$TENANT_ID/solutions"

Before deletion:

  1. Export data - Audit logs, deployment history
  2. Notify stakeholders - Confirm deletion is authorized
  3. Disable users first - Prevent new activity
  4. Document decision - For compliance
ResourceOn Tenant Delete
Solution-Environment linksCascade deleted
Target assignmentsCascade deleted
Tenant tagsCascade deleted

Deletion is blocked (409) while any ON DELETE RESTRICT child rows remain. Those tables are: variable_sets, scoped_variables, deployment_schedules, deployment_freezes, promotion_lifecycles, api_keys, environment_states, environment_state_history, variable_snapshots, storages_local, storages_git, storages_git_auth, storages_s3, and notification_events. Remove or reassign these before deleting the tenant. | Users | Not deleted - orphaned | | Audit logs | Preserved | | Deployment history | Preserved |

Terminal window
curl -X DELETE \
-H "Authorization: Bearer $TOKEN" \
"https://api.mantis.local/api/v1/tenants/$TENANT_ID"

No manual user cleanup is needed. The users.tenant_id foreign key is ON DELETE SET NULL, so PostgreSQL automatically clears it when a tenant is deleted — there are no orphaned users to find afterward. (If you need the list of affected users, query them BEFORE deleting the tenant.)

Monitor key metrics per tenant:

MetricAlert ThresholdAction
Failed deployments>3 in 1 hourInvestigate failures
Inactive users>30 daysReview user access
Variable changesUnexpectedReview audit logs
Target disconnections>1 targetCheck target health

Failed Deployments by Tenant:

SELECT
t.name AS tenant,
COUNT(*) AS failed_count
FROM deployment_history dh
JOIN tenants t ON t.id = dh.tenant_id
WHERE dh.status = 'failed'
AND dh.created_at >= NOW() - INTERVAL '24 hours'
GROUP BY t.name
ORDER BY failed_count DESC;

Active Users per Tenant:

SELECT
t.name AS tenant,
COUNT(*) AS active_users,
MAX(u.last_login_at) AS latest_login
FROM users u
JOIN tenants t ON t.id = u.tenant_id
WHERE u.status = 'active'
GROUP BY t.name;
#!/bin/bash
# Update variable across all tenants
VARIABLE_KEY="api_version"
NEW_VALUE="v2"
for TENANT_ID in $(curl -s "$API_URL/tenants" | jq -r '.data[].id'); do
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"key\": \"$VARIABLE_KEY\",
\"value\": \"$NEW_VALUE\",
\"scope_tenant_id\": \"$TENANT_ID\",
\"is_encrypted\": false
}" \
"$API_URL/variable-sets/$VARIABLE_SET_ID/variables"
echo "Updated $TENANT_ID"
done
#!/bin/bash
# Grant new solution to all enterprise tenants
SOLUTION_ID="sol_new_feature"
ENVIRONMENT_ID="env_production"
# Get tenants with the enterprise tag. NOTE: the LIST response rows carry no
# `tags` field (tags appear only on the per-tenant detail response), so filter
# tags client-side by fetching each tenant's detail, or filter on a field the
# list actually returns.
ENTERPRISE_TENANTS=$(curl -s "$API_URL/tenants" | jq -r '.data[].id' | while read id; do
curl -s "$API_URL/tenants/$id" | jq -r 'select(.tags[]?.value == "enterprise") | .id'
done)
for TENANT_ID in $ENTERPRISE_TENANTS; do
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"solution_id\": \"$SOLUTION_ID\",
\"environment_id\": \"$ENVIRONMENT_ID\"
}" \
"$API_URL/tenants/$TENANT_ID/solutions"
echo "Granted to $TENANT_ID"
done
  1. Check solution entitlement:

    Terminal window
    curl "$API_URL/tenants/$TENANT_ID/solutions"
  2. Check target assignment:

    Terminal window
    curl "$API_URL/tenants/$TENANT_ID/targets"
  3. Verify variables:

    Terminal window
    curl "$API_URL/tenants/$TENANT_ID/variables/resolved?solution_id=$SOL&environment_id=$ENV"
  1. Verify user’s tenant_id:

    SELECT tenant_id FROM users WHERE id = $user_id;
  2. Check user status:

    SELECT status FROM users WHERE id = $user_id;
  3. Verify tenant exists:

    SELECT id, name FROM tenants WHERE id = $tenant_id;