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

Self-Service Tenant Onboarding Saga
POST /api/v1/auth/signup/self-service
Verify Signed OTP Verification Token
Validate Subdomain Uniqueness & Format
Provision Tenant, Auto-Domain & Eager Settings
Scaffold Tenant Super Admin & Default Roles
Create Owner Admin User (Pre-Activated)
Resolve Checkout Mode from EditionId
Checkout Mode: Free?
Yes (Free)
Generate JWT & Prime Permission Cache
Checkout Mode: Trial / Paid?
Yes (Trial/Paid)
Create SignupSession (AwaitingPayment)
Dispatch AssignEditionSelfService (Stripe Connect)
Stripe Session Created?
Yes
Attach Session to SignupSession & Return URL
Abandon Signup Session (Compensate Phase 1)

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.

C#
// 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.

ConstraintRule / PatternSecurity Rationale
Subdomain Formatlowercase alphanumeric, hyphens allowed, length 3-63 charsStandard DNS routing compliance
Reserved Subdomainsapi, admin, portal, studio, dev, system, auth, scripe, blogPrevents phishing and routing conflicts
Database UniquenessCase-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.

C#
// 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.