Open Closed

Help understanding how Saas, Payments, and Tenancy #9614


User avatar
0
cstobler created

I am hoping to get a little knowledge to piece together some of these modules into a cohesive unit. I have all of the modules installed, and I followed the documentation to add a Stripe webhook, product and price, and I then used that to configure my StripeOptions. I also setup an Plan, GatewayPlan, and Edition, so theoretically, once I have a page set up to allow customers to subscribe to an edition, it should work. However, I have a few questions that don't seem to be elucidated in the documentation:

  1. In this example code in the documentation: var paymentRequest = await SubscriptionAppService.CreateSubscriptionAsync(editionId, CurrentTenant.GetId());, it seems to imply that the tenant already needs to be a tenant and be signed in to be able to subscribe to an edition. This seems confusing to me. I need my users to be able to sign up for an edition at the same time they sign up to be a tenant (it doesn't make sense to have a tenant without an edition assigned to them). Would I just do this all at the same time?

  2. Related to 1, I don't have a use for the "host" section of the app, and really only need the app accessible via the admin and tenants. Is there a way to "disable" the host (non-tenant) functionality and just force the user to login or register before being redirected to the homepage? Unless host does something else I am unaware of, but I don't want users to be able to access anything without being logged in.

I am stuck on these at the moment, so any advice on how to structure this would be greatly appreciated.

Charlie


11 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    1. Tenant Signup and Edition Subscription Flow

    The default ABP SaaS + Payment integration expects that a tenant already exists and is authenticated before subscribing to an edition. The code:

    var paymentRequest = await SubscriptionAppService.CreateSubscriptionAsync(editionId, CurrentTenant.GetId());
    

    assumes CurrentTenant.GetId() is available, meaning the user is already associated with a tenant.

    If you want users to sign up and select an edition in a single flow (i.e., create a tenant and subscribe to an edition at the same time), you need to:

    • Present a registration form that collects both tenant info and edition selection.
    • First, create the tenant programmatically using the SaaS module's tenant creation APIs.
    • After the tenant is created, immediately call CreateSubscriptionAsync with the new tenant's ID and the selected edition.
    • Redirect the user to the payment gateway as usual.

    This approach is not provided out-of-the-box, but it is a common customization. You will need to orchestrate tenant creation and subscription in your own controller or service.

    1. Disabling Host (Non-Tenant) Access

    By default, the "host" user is the super admin who manages tenants, editions, and other global settings. If you do not want to expose any host-level functionality to end users:

    • Restrict or remove host-specific UI/menu items.
    • Ensure all controllers/pages require authentication and a tenant context.
    • You can enforce tenant context by checking CurrentTenant.Id and redirecting to login/registration if not present.
    • Optionally, you can override the default route to redirect unauthenticated or host users to the login or registration page.

    This way, only tenant users (not host) can access the application, and all users must be logged in.

    Sources:

    • https://abp.io/docs/latest/modules/saas
    • https://abp.io/docs/latest/modules/payment

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

  • User Avatar
    0
    enisn created
    Support Team .NET Developer

    According to your questions:

    1. This example (var paymentRequest = await SubscriptionAppService.CreateSubscriptionAsync(editionId, CurrentTenant.GetId());) shows how to create a payment link. So you can create a tenant at the code-behind and pass that parameter to this method without actually chaning tenant yet and redirect browser to that payment link immediately. ABP provides you an infrastructure you can build your own way to implement it if you need custom solutions.

    2. Host termionology stands for the manager of the system. If you need to build an application only accessed by tenants, you can use tenant domain resolvers and generate subdomain for your each tenant, so they can acess the application with their own special links: https://abp.io/docs/latest/framework/architecture/multi-tenancy#domain-subdomain-tenant-resolver

    ABP provides you a structure but it doesn't force you to use it. The modules and framework is modular and highly customizable according to need. If you want to achieve a specific customization, please tell me to guide you. Do you need an example code how to create a tenant with a form and create payment for that tenant at the same time?

  • User Avatar
    0
    cstobler created

    Sorry for the delay; just getting to this now.

    I understand conceptually what you mean in the first answer, but if you wouldn't mind providing example code as you offered to give me a basic understanding of the implementation of creating a tenant and payment at the same time, that would be much appreciated. I am unsure if it utilizes the existing register page or if it uses a custom page so getting an example would really help.

    Regarding the second question: I was intending on using tenant domain resolvers and generating a subdomain for each tenant, so that will work with my original plan. However, I guess I was under the assumption that most people would probably just go to my root domain, which would redirect them to a login page, and then from there would redirect them to their tenant dashboard with their custom tenant subdomain. I guess I really just need to redirect the user when they are not logged in to the login/register pages, which I guess I could do when mapping endpoints in the Configure method in Program.cs?

    I am new to some of this backend stuff, so I appreciate your patience if I am asking stupid questions!

    Charlie

  • User Avatar
    0
    cstobler created

    Hi, bumping this to see if I can get some help.

  • User Avatar
    0
    cstobler created

    I'm not sure why I haven't received a response yet; it's been 10 days since I last received a reply. Either way, perhaps I can receive some help in terms of if the direction I am going is correct, and how to create a tenant programmatically (since I wasn't able to find it in the documentation).

    Here is my page model for my registration page that will handle creating the tenant and subscribing the edition to that tenant:

    [AllowAnonymous]
    public class RegisterModel : ArmadaPageModel
    {
        private readonly ITenantAppService _tenantAppService;
        private readonly IMultiTenancyAppService _multiTenancyAppService;
        private readonly IEditionAppService _editionAppService;
        private readonly ISubscriptionAppService _subscriptionAppService;
        private readonly ICurrentTenant _currentTenant;
    
        public RegisterModel(
            ITenantAppService tenantAppService,
            IMultiTenancyAppService multiTenancyAppService,
            IEditionAppService editionAppService,
            ISubscriptionAppService subscriptionAppService,
            ICurrentTenant currentTenant)
        {
            _tenantAppService = tenantAppService;
            _multiTenancyAppService = multiTenancyAppService;
            _editionAppService = editionAppService;
            _subscriptionAppService = subscriptionAppService;
            _currentTenant = currentTenant;
        }
    
        [BindProperty]
        public RegisterTenantViewModel Input { get; set; } = new();
    
        public async Task OnGetAsync()
        {
            List<EditionDto> editions = await _multiTenancyAppService.GetEditionsAsync();
            List<SelectListItem> editionSelectItems = editions.Select(e => new SelectListItem
            {
                Text = e.DisplayName,
                Value = e.Id.ToString()
            }).ToList();
    
            Input.AvailableEditions = editionSelectItems;
        }
    
        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                await OnGetAsync(); // reload editions
                return Page();
            }
    
            // Confirm we are NOT in tenant context
            if (_currentTenant.Id != null)
            {
                throw new Exception("Cannot register a tenant while already in a tenant context.");
                //return Forbid(); // Registration should only be done as host
            }
    
            //var tenantDto = await _tenantAppService.CreateAsync(new SaasTenantCreateDto
            //{
            //    Name = Input.TenantName,
            //    AdminEmailAddress = Input.Email,
            //    AdminPassword = Input.Password,
            //    EditionId = Input.EditionId
            //});
    
            var tenantDto = await _multiTenancyAppService.CreateTenantAsync(new SaasTenantCreateDto
            {
                Name = Input.TenantName,
                AdminEmailAddress = Input.Email,
                AdminPassword = Input.Password,
                EditionId = Input.EditionId
            });
    
            // 2. Manually switch to new tenant context
            using (_currentTenant.Change(tenantDto.Id))
            {
                // 3. Create subscription with edition ID
                var subscription = await _subscriptionAppService.CreateSubscriptionAsync(
                    Input.EditionId,
                    tenantDto.Id
                );
    
                // 4. Redirect to Stripe or confirmation
                //return Redirect(subscription.PaymentUrl);
            }
    
            return Page();
        }
    }
    

    I also created an appservice called MultiTenancyAppService which helps me populate the editions dropdown, as well as attempt to create a tenant (since calling tenantAppService.CreateAsync() was throwing an authorization error):

    [AllowAnonymous]
    public class MultiTenancyAppService : ArmadaIOAppService, IMultiTenancyAppService
    {
        private readonly IRepository<Edition, Guid> _editionRepository;
        private readonly ITenantAppService _tenantAppService;
        private readonly IRepository<Tenant, Guid> _tenantRepository;
    
        public MultiTenancyAppService(IRepository<Edition, Guid> editionRepository, ITenantAppService tenantAppService, IRepository<Tenant, Guid> tenantRepository)
        {
            _editionRepository = editionRepository;
            _tenantAppService = tenantAppService;
            _tenantRepository = tenantRepository;
        }
    
        public async Task<List<EditionDto>> GetEditionsAsync()
        {
            List<Edition> editions = await _editionRepository.GetListAsync();
            List<EditionDto> dtos = ObjectMapper.Map<List<Edition>, List<EditionDto>>(editions);
            return dtos;
        }
    
        public async Task<SaasTenantDto> CreateTenantAsync(SaasTenantCreateDto input)
        {
            if (string.IsNullOrWhiteSpace(input.Name) || string.IsNullOrWhiteSpace(input.AdminEmailAddress) || string.IsNullOrWhiteSpace(input.AdminPassword))
            {
                throw new UserFriendlyException("Please fill all required fields before submission");
            }
            
            return await _tenantAppService.CreateAsync(input);
        }
    }
    

    It is still throwing an authorization error when trying to create a tenant, even though I have the [AllowAnonymous] decorator on the page model and the app service. I am under the assumption that by default, a register page is creating a tenant when in host mode, since a tenant isn't logged in, so this seems like unintuitive functionality.

    Please advise me on if this is the correct direction for a custom registration page, as well how to go about creating the tenant. If there is any documentation I missed, please link it. I have looked through much of it, but it is possible I missed something that would be relevant here.

    Thanks in advance for your help, Charlie

  • User Avatar
    0
    enisn created
    Support Team .NET Developer

    Overview

    Prepared a guide that explains how the ABP SaaS and Payment modules work together to enable tenant subscriptions, based on analysis of the source code and official documentation.

    How Tenant Subscription Works

    Current Architecture

    The tenant subscription process in ABP follows this workflow:

    1. Tenant Must Exist First: The SubscriptionAppService.CreateSubscriptionAsync(editionId, tenantId) method requires both parameters, meaning a tenant must already be created before subscribing to an edition.

    2. Payment Request Creation: When creating a subscription, the system:

      • Creates a payment request with PaymentType.Subscription
      • Associates the tenant ID and edition ID in ExtraProperties
      • Links the payment to the edition's plan ID
    3. Event-Driven Updates: When payment is successful, the Payment module publishes a SubscriptionCreatedEto event, which the SaaS module handles to:

      • Update the tenant's EditionId
      • Set the EditionEndDateUtc based on the subscription period

    Key Components

    // From SubscriptionAppService.cs
    public virtual async Task<PaymentRequestWithDetailsDto> CreateSubscriptionAsync(Guid editionId, Guid tenantId)
    {
        var edition = await EditionManager.GetEditionForSubscriptionAsync(editionId);
        
        var paymentRequest = await PaymentRequestAppService.CreateAsync(new PaymentRequestCreateDto
        {
            Products = new List<PaymentRequestProductCreateDto>
            {
                new PaymentRequestProductCreateDto
                {
                    PlanId = edition.PlanId,
                    Name = edition.DisplayName,
                    Code = $"{tenantId}_{edition.PlanId}",
                    Count = 1,
                    PaymentType = PaymentType.Subscription,
                }
            },
            ExtraProperties =
            {
                { EditionConsts.EditionIdParameterName, editionId },
                { TenantConsts.TenantIdParameterName, tenantId },
            }
        });
    
        // Additional tenant setup...
    }
    

    Answers to Your Questions

    1. Tenant Registration + Edition Subscription

    Current State: You are correct that the current documentation assumes a tenant already exists. However, you can implement a combined registration flow.

    Solution: Create a custom service that handles both tenant creation and subscription initiation:

    public class TenantRegistrationService
    {
        public async Task<TenantRegistrationResult> RegisterTenantWithSubscription(
            string tenantName, 
            string adminEmail, 
            string adminPassword, 
            Guid editionId)
        {
            // 1. Create tenant first
            var tenant = await _tenantAppService.CreateAsync(new SaasTenantCreateDto
            {
                Name = tenantName,
                AdminEmailAddress = adminEmail,
                AdminPassword = adminPassword,
                EditionId = editionId, // Pre-assign edition
                ActivationState = TenantActivationState.Passive // Keep inactive until payment
            });
    
            // 2. Create subscription payment request
            var paymentRequest = await _subscriptionAppService.CreateSubscriptionAsync(
                editionId, 
                tenant.Id);
    
            return new TenantRegistrationResult
            {
                TenantId = tenant.Id,
                PaymentRequestId = paymentRequest.Id,
                PaymentUrl = $"/Payment/GatewaySelection?paymentRequestId={paymentRequest.Id}"
            };
        }
    }
    

    Key Points:

    • Create the tenant with ActivationState.Passive initially
    • Pre-assign the EditionId during tenant creation
    • Only activate the tenant after successful payment (handle this in the SubscriptionCreatedHandler)

    2. Disabling Host Functionality

    Current Architecture: ABP's multi-tenancy system distinguishes between:

    • Host: Administrative context (no tenant ID)
    • Tenant: Specific tenant context (with tenant ID)

    Solutions for Tenant-Only Access:

    Option A: Custom Authorization Policy

    public class RequireTenantAuthorizationHandler : AuthorizationHandler<RequireTenantRequirement>
    {
        protected override Task HandleRequirementAsync(
            AuthorizationHandlerContext context,
            RequireTenantRequirement requirement)
        {
            var currentTenant = context.User.FindFirst(AbpClaimTypes.TenantId);
            
            if (currentTenant != null && !string.IsNullOrEmpty(currentTenant.Value))
            {
                context.Succeed(requirement);
            }
            else
            {
                context.Fail();
            }
    
            return Task.CompletedTask;
        }
    }
    

    Option B: Custom Middleware

    public class TenantRequiredMiddleware
    {
        public async Task InvokeAsync(HttpContext context, ICurrentTenant currentTenant)
        {
            // Skip for authentication/registration pages
            if (IsAuthenticationPage(context.Request.Path))
            {
                await _next(context);
                return;
            }
    
            if (!currentTenant.IsAvailable)
            {
                context.Response.Redirect("/Account/TenantSelection");
                return;
            }
    
            await _next(context);
        }
    }
    

    Option C: Override Home Page Logic

    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            if (!CurrentTenant.IsAvailable)
            {
                return RedirectToPage("/Account/TenantRegistration");
            }
    
            // Normal tenant home page logic
            return View();
        }
    }
    

    3. Recommended Implementation Strategy

    Based on the source code analysis, here's the recommended approach:

    Phase 1: Custom Tenant Registration Flow

    1. Create a public tenant registration page
    2. Implement combined tenant creation + subscription initiation
    3. Handle the payment flow completion to activate tenants

    Phase 2: Tenant-Only Access Control

    1. Implement custom authorization to require tenant context
    2. Create a tenant selection/registration landing page
    3. Redirect non-tenant users to registration

    Phase 3: Event Handlers

    // Handle successful subscription to activate tenant
    public class SubscriptionActivationHandler : IDistributedEventHandler<SubscriptionCreatedEto>
    {
        public async Task HandleEventAsync(SubscriptionCreatedEto eventData)
        {
            var tenantId = Guid.Parse(eventData.ExtraProperties[TenantConsts.TenantIdParameterName]?.ToString());
            var tenant = await _tenantRepository.GetAsync(tenantId);
            
            // Activate tenant after successful payment
            tenant.SetActivationState(TenantActivationState.Active);
            await _tenantRepository.UpdateAsync(tenant);
        }
    }
    

    Key Architectural Insights

    Multi-Tenancy Flow

    1. Host Context: Used for administrative functions (creating tenants, managing editions)
    2. Tenant Context: Used for tenant-specific operations
    3. Current Design: Assumes host admins create tenants manually

    Payment Integration

    1. Payment Request: Links to tenant and edition via ExtraProperties
    2. Webhook Events: Update tenant status when subscription changes
    3. Subscription Lifecycle: Managed through distributed events

    Missing Pieces for Self-Service

    The current ABP SaaS module is designed for admin-managed tenants. For self-service tenant registration, you need to:

    1. Create public APIs (without admin authorization)
    2. Implement custom tenant registration workflows
    3. Handle payment completion to activate tenants
    4. Add tenant selection/registration UI for non-authenticated users

    Conclusion

    While ABP doesn't provide out-of-the-box self-service tenant registration, the architecture supports building this functionality. The key is to create custom services that combine tenant creation with payment initiation, and implement proper access controls to enforce tenant-only access.

  • User Avatar
    0
    cstobler created

    Thank you for the detailed write-up. This will be very helpful as a reference as I am working on this.

    My main question currently has to do with the last message I sent. I am getting a "Exception of type 'Volo.Abp.Authorization.AbpAuthorizationException' was thrown" error on the "await _tenantAppService.CreateAsync(input);" line. The code I provided looks functionally similar to yours, so I am unsure as to why I am receiving this authorization exception in my app service.

    I'm assuming part of this is because the page itself is accessible to non-tenant users, but that is necessary figuring the only people registering will be those who are not yet tenants.

    Please advise me on how to resolve this. Let me know if you want a copy of the project files for review.

    Thanks, Charlie

  • User Avatar
    0
    cstobler created

    I need this bumped up in priority. It's been nearly 3 weeks since I opened this ticket, and I still haven't received enough help to resolve this. I made some progress but I cannot get past the authorization error I encountered in my last message and this delay is slowing down development.

    Charlie

  • User Avatar
    0
    cstobler created

    Asking again to be prioritized or reassigned.

  • User Avatar
    0
    enisn created
    Support Team .NET Developer

    This post answers how to combine tenant signup with edition subscription, how to restrict access to tenant-only, and how to fix the AbpAuthorizationException you hit when creating tenants from a public page.

    1) Can users sign up as a tenant and subscribe to an edition in one flow?

    Yes. Create the tenant first, then immediately create the subscription for the new tenantId, and redirect to the payment gateway. Keep the tenant Passive until payment succeeds, then activate it on the subscription-created event.

    Why: CreateSubscriptionAsync(editionId, tenantId) expects a valid tenant context. The provided host-side implementation already packages the correct payment request with extra properties for EditionId and TenantId.

    • Subscription creation implementation (in repo): src/Volo.Saas.Host.Application/Volo/Saas/Subscription/SubscriptionAppService.cs
    • Payment request model types: abp/payment/src/Volo.Payment.Domain/Volo/Payment/...
    • Extra property keys: EditionConsts.EditionIdParameterName and TenantConsts.TenantIdParameterName

    Payment events update the tenant after checkout:

    • SubscriptionCreatedHandler sets tenant.EditionId and tenant.EditionEndDateUtc.
    • SubscriptionUpdatedHandler refreshes tenant.EditionEndDateUtc and can update EditionId if provided.

    Docs: SaaS, Payment

    2) How to “disable” host (non-tenant) access and force login/registration?

    You don’t need to remove host; just restrict what end users see:

    • Require authentication for app pages and hide host UI/menu.
    • Enforce tenant context with a small middleware (redirect when CurrentTenant.IsAvailable is false) or a policy.
    • Consider domain/subdomain tenant resolver for per-tenant URLs (see ABP docs on multi-tenancy resolvers).

    Docs: Multi-tenancy resolvers

    Fix: AbpAuthorizationException when creating a tenant from a public page

    TenantAppService.CreateAsync is host-only and protected by SaasHostPermissions.Tenants.*, so calling it from a public page throws. Instead:

    1. Create your own public endpoint ([AllowAnonymous]) that uses the domain layer to create the tenant: ITenantManager + ITenantRepository.
    2. Publish TenantCreatedEto (to seed admin user etc.).
    3. Call CreateSubscriptionAsync(editionId, tenantId) to start payment.
    4. Activate the tenant on SubscriptionCreatedEto if desired.

    Code pointers in the source code:

    • Host-only app service that throws when called without host permissions: src/Volo.Saas.Host.Application/Volo/Saas/Host/TenantAppService.cs
    • Domain creation API (safe to use from your public endpoint): src/Volo.Saas.Domain/Volo/Saas/Tenants/TenantManager.cs
    • Subscription creation that embeds tenant/edition IDs: src/Volo.Saas.Host.Application/Volo/Saas/Subscription/SubscriptionAppService.cs
    • Payment → SaaS handlers setting edition and period:
      • src/Volo.Saas.Domain/Volo/Payment/Subscription/SubscriptionCreatedHandler.cs
      • src/Volo.Saas.Domain/Volo/Payment/Subscription/SubscriptionUpdatedHandler.cs

    Recommended flow (concise)

    • Public page collects tenant info + editionId.
    • Create tenant via domain (ITenantManager), set Passive.
    • Publish TenantCreatedEto with admin email/password.
    • Call CreateSubscriptionAsync(editionId, tenant.Id) and redirect to payment.
    • On SubscriptionCreatedEto: set tenant.ActivationState = Active (custom handler), while built-in handlers set edition + end date.
  • User Avatar
    0
    enisn created
    Support Team .NET Developer

    What ABP provides out of the box

    • SaaS tenant management app service (ITenantAppService) is host‑only and protected by SaasHostPermissions.Tenants.*.
    • Edition → Plan linkage and subscription payment creation via a host app service (SubscriptionAppService.CreateSubscriptionAsync(Guid editionId, Guid tenantId)), which creates a PaymentRequest with required extra properties:
      • EditionConsts.EditionIdParameterName
      • TenantConsts.TenantIdParameterName
    • Payment events update the tenant’s edition and period:
      • SubscriptionCreatedHandler sets tenant.EditionId and tenant.EditionEndDateUtc.
      • SubscriptionUpdatedHandler refreshes tenant.EditionEndDateUtc and optionally tenant.EditionId.

    Relevant code in this repository:

    • SaaS tenant creation (host‑only app service): src/Volo.Saas.Host.Application/Volo/Saas/Host/TenantAppService.cs
    • Public subscription creation API (host context): src/Volo.Saas.Host.Application/Volo/Saas/Subscription/SubscriptionAppService.cs
    • Payment request and plan types: abp/payment/src/Volo.Payment.Domain/Volo/Payment/...
    • Payment → SaaS event handlers: src/Volo.Saas.Domain/Volo/Payment/Subscription/SubscriptionCreatedHandler.cs, SubscriptionUpdatedHandler.cs

    Problem: Self‑service signup causes AbpAuthorizationException

    If you call TenantAppService.CreateAsync from a public page, you’ll get AbpAuthorizationException because it is decorated with [Authorize(SaasHostPermissions.Tenants.Default)] and intended for host administrators.

    Solution overview

    Create a dedicated, public endpoint that:

    1. Creates the tenant via domain layer (ITenantManager + ITenantRepository) and publishes TenantCreatedEto (to seed the admin user, etc.),
    2. Starts the subscription by calling SubscriptionAppService.CreateSubscriptionAsync(editionId, tenantId),
    3. Keeps tenant inactive (or passive) until payment succeeds, and activates it on payment event if desired.

    This avoids host‑only permissions while keeping the standard cross‑module behaviors (user seeding and subscription updates) intact.

    Minimal implementation

    1. Create a public application service or controller for registration
    using System;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authorization;
    using Volo.Abp;
    using Volo.Abp.EventBus.Distributed;
    using Volo.Saas.Tenants;
    using Volo.Saas.Host.Dtos; // for consistency if you reuse DTOs
    using Volo.Saas.Host.Application.Volo.Saas.Subscription; // ISubscriptionAppService
    
    [AllowAnonymous]
    public class PublicTenantRegistrationAppService : MyAppAppService
    {
        private readonly ITenantManager _tenantManager;
        private readonly ITenantRepository _tenantRepository;
        private readonly IDistributedEventBus _eventBus;
        private readonly ISubscriptionAppService _subscriptionAppService;
    
        public PublicTenantRegistrationAppService(
            ITenantManager tenantManager,
            ITenantRepository tenantRepository,
            IDistributedEventBus eventBus,
            ISubscriptionAppService subscriptionAppService)
        {
            _tenantManager = tenantManager;
            _tenantRepository = tenantRepository;
            _eventBus = eventBus;
            _subscriptionAppService = subscriptionAppService;
        }
    
        public async Task<StartSubscriptionResultDto> RegisterAndSubscribeAsync(RegisterTenantInput input)
        {
            // 1) Create tenant via domain layer (no host permission needed)
            var tenant = await _tenantManager.CreateAsync(input.TenantName, editionId: input.EditionId);
    
            tenant.SetActivationState(TenantActivationState.Passive); // keep passive until payment succeeds
            await _tenantRepository.InsertAsync(tenant, autoSave: true);
    
            // 2) Publish TenantCreatedEto to seed admin user (same as TenantAppService does)
            await _eventBus.PublishAsync(new TenantCreatedEto
            {
                Id = tenant.Id,
                Name = tenant.Name,
                Properties =
                {
                    {"AdminEmail", input.AdminEmail},
                    {"AdminPassword", input.AdminPassword}
                }
            });
    
            // 3) Start subscription (creates PaymentRequest with TenantId/EditionId extra props)
            var paymentRequest = await _subscriptionAppService.CreateSubscriptionAsync(input.EditionId, tenant.Id);
    
            return new StartSubscriptionResultDto
            {
                TenantId = tenant.Id,
                PaymentRequestId = paymentRequest.Id,
                // redirect URL depends on your payment gateway UI
            };
        }
    }
    
    public class RegisterTenantInput
    {
        public string TenantName { get; set; }
        public Guid EditionId { get; set; }
        public string AdminEmail { get; set; }
        public string AdminPassword { get; set; }
    }
    
    public class StartSubscriptionResultDto
    {
        public Guid TenantId { get; set; }
        public Guid PaymentRequestId { get; set; }
    }
    
    1. Handle activation after payment

    SubscriptionCreatedHandler and SubscriptionUpdatedHandler update edition and end date. If you also want to flip activation state on initial payment, add a small handler:

    using System;
    using System.Threading.Tasks;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.EventBus.Distributed;
    using Volo.Payment.Subscription;
    using Volo.Saas.Tenants;
    
    public class ActivateTenantOnPaidHandler : IDistributedEventHandler<SubscriptionCreatedEto>, ITransientDependency
    {
        private readonly ITenantRepository _tenantRepository;
    
        public ActivateTenantOnPaidHandler(ITenantRepository tenantRepository)
        {
            _tenantRepository = tenantRepository;
        }
    
        public async Task HandleEventAsync(SubscriptionCreatedEto eventData)
        {
            var tenantId = Guid.Parse(eventData.ExtraProperties[TenantConsts.TenantIdParameterName]?.ToString());
            var tenant = await _tenantRepository.FindAsync(tenantId);
            if (tenant == null) return;
    
            tenant.SetActivationState(TenantActivationState.Active);
            await _tenantRepository.UpdateAsync(tenant);
        }
    }
    
    1. Tenant‑only access options
    • Require authentication everywhere by default and redirect to login/registration for anonymous users.
    • Enforce tenant context via a lightweight middleware or policy. Example middleware:
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Http;
    using Volo.Abp.MultiTenancy;
    
    public class TenantRequiredMiddleware
    {
        private readonly RequestDelegate _next;
        public TenantRequiredMiddleware(RequestDelegate next) => _next = next;
    
        public async Task InvokeAsync(HttpContext context, ICurrentTenant currentTenant)
        {
            var path = context.Request.Path.Value ?? string.Empty;
            if (path.StartsWith("/Account", StringComparison.OrdinalIgnoreCase) ||
                path.StartsWith("/Public", StringComparison.OrdinalIgnoreCase))
            {
                await _next(context);
                return;
            }
    
            if (!currentTenant.IsAvailable)
            {
                context.Response.Redirect("/Account/Login");
                return;
            }
    
            await _next(context);
        }
    }
    

    Also consider the domain/subdomain tenant resolver to route tenants to their own subdomains.

    Notes and gotchas

    • TenantAppService.CreateAsync is host‑only. Use domain + repository + event bus in a public endpoint to avoid authorization errors.
    • SubscriptionAppService.CreateSubscriptionAsync must be invoked in host context with a valid tenantId. It automatically emits a PaymentRequest with PaymentType.Subscription and a product PlanId.
    • Payment events already set EditionId and subscription periods. If you need custom onboarding (e.g., activation), add your own distributed event handler.
    • Keep your registration endpoint [AllowAnonymous] but aggressively validate inputs and throttle to prevent abuse.

    References

    • SaaS module docs: https://abp.io/docs/latest/modules/saas
    • Payment module docs: https://abp.io/docs/latest/modules/payment
    • Multi‑tenancy tenant resolvers (domain/subdomain): https://abp.io/docs/latest/framework/architecture/multi-tenancy#domain-subdomain-tenant-resolver
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.0.0-preview. Updated on September 01, 2025, 08:37