Self-Service Signup & B2B2C Onboarding
Automated multi-tenant onboarding saga with verification OTPs, regional currency resolving, and Stripe checkout integration.
SCRIPE features a comprehensive B2B2C self-service tenant onboarding engine orchestrated via a robust two-phase saga. It coordinates database transactions, subscription setup, billing connections, and provides automatic compensation rollbacks if payments are abandoned.
Onboarding Flow
Phase 1: Identity & Provisioning Transaction
Phase 1 executes in a single database transaction. It generates the tenant workspace, hooks up the default subdomain, provisions settings, scaffolds the default security roles, and creates the tenant owner user account.
// Provisioning entities dynamically within RegisterTenantSelfServiceCommandHandler
await _unitOfWork.ExecuteInTransactionAsync(async (cancellationToken) =>
{
// 1. Create Tenant
var tenant = new Tenant
{
Id = tenantId,
Name = request.WorkspaceName.Trim(),
Code = tenantCode,
IsActive = true,
HierarchyLevel = 0,
HierarchyPath = "/"
};
await _tenantRepository.AddAsync(tenant, cancellationToken);
// 2. Provision TenantDomain (subdomain.platform.com)
var autoDomain = new TenantDomain
{
Id = Guid.NewGuid(),
TenantId = tenantId,
Domain = $"\{subdomain}.\{platformDomain}",
Type = "auto",
IsPrimary = true,
IsVerified = true,
VerifiedAt = DateTime.UtcNow
};
tenant.Domains.Add(autoDomain);
// 3. Eagerly provision TenantSettings (prevents null reference issues)
var tenantSettings = new TenantSettings { Id = Guid.NewGuid(), TenantId = tenantId };
tenant.Settings = tenantSettings;
// 4. Create Roles
var superAdminRole = new Role
{
Id = superAdminRoleId,
Code = $"\{tenantCode}_SUPER_ADMIN",
NameEn = $"\{request.WorkspaceName} Super Admin",
TenantId = tenantId,
Priority = 1,
IsSystem = true,
IsTenantSuperAdmin = true,
IsPermissionLocked = false // UNLOCKED for subsequent feature assignment
};
await _roleRepository.AddAsync(superAdminRole, cancellationToken);
// 5. Create pre-activated Owner user
var admin = new Admin
{
Id = adminId,
Username = $"\{tenantCode}_\{resolvedUsername}",
PasswordHash = _passwordHasher.Hash(request.Password),
Email = email,
TenantId = tenantId,
IsActive = true,
IsAccountActivated = true // Pre-verified via OTP
};
admin.AdminRoles.Add(new AdminRole { RoleId = superAdminRoleId, TenantId = tenantId });
await _adminRepository.AddAsync(admin, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
});Subdomain & Identity Verification
To maintain security and prevent routing conflicts, the onboarding system applies strict format rules and checks subdomain availability against a reserved word blacklist.
| Constraint | Rule / Pattern | Security Rationale |
|---|---|---|
| Subdomain Format | lowercase alphanumeric, hyphens allowed, length 3-63 chars | Standard DNS routing compliance |
| Reserved Subdomains | api, admin, portal, studio, dev, system, auth, scripe, blog | Prevents phishing and routing conflicts |
| Database Uniqueness | Case-insensitive check against Tenant.Code (subdomain in uppercase) | Ensures absolute isolation and distinct workspaces |
Email Verification Tickets
Before a saga can start, the prospect's email must be verified. The system issues a cryptographic HMAC-SHA256 signed ticket with a 15-minute expiration time.
// HMAC-SHA256 Token Signature Validation
public static bool ValidateVerificationToken(string token, string email, string secret)
{
if (string.IsNullOrEmpty(token) || !token.Contains('.')) return false;
var parts = token.Split('.');
if (parts.Length != 3) return false;
var payload = parts[0]; // base64url(email + ":" + expires)
var signature = parts[1]; // base64url(HMAC-SHA256)
// Verify token expiry (TTL 15 minutes)
var decodedPayload = Encoding.UTF8.GetString(Base64UrlDecode(payload));
var payloadParts = decodedPayload.Split(':');
if (payloadParts.Length != 2 || payloadParts[0] != email) return false;
if (!long.TryParse(payloadParts[1], out var expiryTicks) || DateTime.UtcNow.Ticks > expiryTicks)
{
return false; // Expired
}
// Verify cryptographic signature integrity
var expectedSig = GenerateHmacSignature(payload, secret);
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(signature),
Encoding.UTF8.GetBytes(expectedSig)
);
}Phase 2: Entitlements & Billing Handoff
Phase 2 links the newly created tenant to the Entitlements module. If the chosen edition is paid, the system generates a Stripe checkout session and redirects the user.