Creating Tenants
Tenant creation is a two-phase process: first the tenant record is created in PENDING status, then asynchronous provisioning begins. The TenantController.createTenant() endpoint validates input, persists the tenant, and triggers the provisioning pipeline.
Create Tenant Request
The CreateTenantRequest DTO defines the required and optional fields:
public class CreateTenantRequest {
@NotBlank @Size(min = 2, max = 100)
private String name;
@Pattern(regexp = "^[a-z0-9][a-z0-9-]*[a-z0-9]$")
@Size(min = 3, max = 63)
private String slug;
@Size(max = 500)
private String description;
@NotBlank @Email
private String adminEmail;
@Size(max = 50)
private String adminFirstName;
@Size(max = 50)
private String adminLastName;
private TenantTier tier = TenantTier.FREE; // Default
@NotBlank
private String region;
private String externalOrgId; // Optional external integration
}Validation Rules
| Field | Rule |
|---|---|
name | Required, 2-100 characters |
slug | Lowercase alphanumeric with hyphens, 3-63 characters, must start and end with alphanumeric |
adminEmail | Required, valid email format, must not already be registered |
region | Required, valid cloud region identifier |
slug uniqueness | No existing non-deleted tenant with the same slug |
adminEmail uniqueness | No existing non-deleted tenant with the same admin email |
Creation Flow
POST /api/v1/tenants
|
v
TenantValidationService.validateCreateRequest(request)
|
v
Check duplicate slug (tenantRepository.existsBySlugAndDeletedFalse)
|
v
Check duplicate admin email (tenantRepository.findByAdminEmailAndDeletedFalse)
|
v
TenantMapper.toEntity(request) --> Tenant entity (status = PENDING)
|
v
applyTierDefaults(tenant) --> Set resource limits from tier
|
v
tenantRepository.save(tenant) --> Persist to database
|
v
provisioningService.startProvisioning(tenantId) --> Async provisioning
|
v
Return HTTP 201 with TenantResponseTier Defaults
When a tenant is created, the service applies default resource limits based on the selected tier. These defaults are only applied if the field is not explicitly set in the request:
| Tier | Max Users | Max Pipelines | Max Queries/Day | Storage (GB) | Retention (Days) |
|---|---|---|---|---|---|
| FREE / STARTER | 5 | 10 | 1,000 | 10 | 30 |
| PROFESSIONAL | 50 | 100 | 10,000 | 500 | 365 |
| ENTERPRISE | -1 (unlimited) | -1 | -1 | -1 | -1 |
Source: TenantService.applyTierDefaults() and the getTierDefault* methods.
Example: Create a Tenant
curl -X POST http://localhost:8082/api/v1/tenants \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "Acme Corporation",
"slug": "acme-corp",
"description": "Enterprise data analytics platform",
"adminEmail": "admin@acme.com",
"adminFirstName": "Jane",
"adminLastName": "Smith",
"tier": "PROFESSIONAL",
"region": "eastus"
}'Response (201 Created):
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Acme Corporation",
"slug": "acme-corp",
"description": "Enterprise data analytics platform",
"tier": "PROFESSIONAL",
"status": "PENDING",
"region": "eastus",
"cloudProvider": "azure",
"environment": "dev",
"adminEmail": "admin@acme.com",
"maxUsers": 50,
"maxPipelines": 100,
"maxQueriesPerDay": 10000,
"storageLimitGb": 500,
"dataRetentionDays": 365,
"deploymentType": "SHARED",
"isTrial": true,
"createdAt": "2026-02-12T10:30:00Z"
}Error Responses
| Status | Error | Cause |
|---|---|---|
| 400 | Validation error | Missing required field or invalid format |
| 409 | DuplicateResourceException | Slug or admin email already registered |
{
"error": "DuplicateResource",
"message": "Tenant with slug 'acme-corp' already exists",
"field": "slug",
"value": "acme-corp"
}Self-Registration
The RegistrationController provides a public self-registration flow at POST /api/v1/tenants/register that does not require authentication:
@PostMapping("/register")
@Operation(summary = "Self-register a new tenant")
public ResponseEntity<RegistrationResponse> register(
@Valid @RequestBody SelfRegistrationRequest request) {
RegistrationResponse response = registrationService.register(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}This flow includes email verification via POST /api/v1/tenants/verify-email and GET /api/v1/tenants/verify-email?token=.... Verification emails can be resent via POST /api/v1/tenants/resend-verification?email=....
Available Plans
The GET /api/v1/tenants/plans endpoint returns all available subscription plans with pricing and feature lists. This is a public endpoint for display on signup pages:
| Tier | Price/Month | Key Features |
|---|---|---|
| FREE | $0 | 3 services, 5 users, 100 queries/day, community support |
| PROFESSIONAL | $499 | 10 services, 25 users, 10K queries/day, email support, BI dashboards |
| ENTERPRISE | Contact sales | Unlimited everything, 24/7 SLA, AI features, SSO/SAML, dedicated infra |
Source Files
| File | Path |
|---|---|
| Controller | control-plane/tenant-service/src/main/java/com/matih/tenant/controller/TenantController.java |
| Registration | control-plane/tenant-service/src/main/java/com/matih/tenant/controller/RegistrationController.java |
| Service | control-plane/tenant-service/src/main/java/com/matih/tenant/service/TenantService.java |
| Request DTO | control-plane/tenant-service/src/main/java/com/matih/tenant/dto/request/CreateTenantRequest.java |
| Entity | control-plane/tenant-service/src/main/java/com/matih/tenant/entity/Tenant.java |