Clarity Platform Architecture chevron_right Operations & Platform Services

Operations & Platform Services

Deployment pipelines, scheduled tasks, notification system, settings management, site setup, payment configuration, and business workflows.

cloud_upload Deployment

The Clarity platform is deployed as a Docker Compose stack on Ubuntu 22.04 and 24.04 VMs. The build and release process is orchestrated through Azure DevOps Pipelines, producing Docker images pushed to a private registry and a docker-compose.yml artifact for deployment.

Deployment Flow

rocket_launch

Azure DevOps

Pipeline Trigger

build

Build

Backend + Frontend

cloud_upload

Docker Push

Push to Registry

inventory_2

Artifact

docker-compose.yml

dns

VM Deploy

SSH + docker compose up

Artifact Retrieval & File Transfer

download

Artifact Download

Azure DevOps Build Pipeline publishes the docker-compose.yml as a build artifact. The release pipeline downloads this artifact for deployment.

upload_file

SCP File Transfer

The compose file is transferred to target VMs via SCP (Secure Copy Protocol). The release pipeline authenticates to the VM using SSH keys configured in Azure DevOps.

Token Replacement

Before deployment, the compose file undergoes token replacement to inject environment-specific values. Placeholder tokens in the compose file are replaced with actual values from the release pipeline variables.

# Tokens replaced at deploy time
CONNECTION_STRING: {{CONNECTION_STRING}}   # Database connection string
HOST_NAME:          {{HOST_NAME}}            # Public hostname for the site

SSL & Container Registry

lock

SSL via Let's Encrypt

SSL certificates are automatically provisioned and renewed via Let's Encrypt. The certificate configuration is embedded in the Docker Compose file, requiring no manual certificate management.

deployed_code

Private Container Registry

Docker images are stored in the internal registry at registry.hq.clarityinternal.com:443. Images are tagged with the build number for traceability.

schedule Scheduler

The scheduler provides cron-based task execution for recurring background work. Tasks inherit from TaskBase and are registered with the DI container at startup. Each task specifies its schedule using a standard cron expression.

TaskBase Abstract Class

public class MyScheduledTask : TaskBase
{
    protected override string? Cron => "* * * * *"; // every minute

    public override async Task ExecuteAsync(IPipelineContext context)
    {
        // Task implementation
    }
}

Registration

// In plugin's RegisterServices method
builder.Services.AddScheduledTask<MyScheduledTask>();

Background Work Queue

For ad-hoc background work outside of scheduled tasks, use the background task queue. This is useful for offloading work from request handlers without blocking the response.

// Enqueue work onto the background task queue
scheduler.AddAsyncBackgroundTask(async ctx =>
{
    // Use ctx (IPipelineContext), NOT the outer request context
    await ctx.RunPipeline<SomeWorkPipeline>();
});
warning

Important

Always use the IPipelineContext passed into the background task callback, not the outer request context. The outer context may be disposed by the time the background task executes.

Scheduler Task Execution Flow

schedule

Cron Trigger

Cron Expression

code

TaskBase

Scheduled Task

conversion_path

IPipelineContext

Scoped Context

play_arrow

Execute Work

Run Business Logic

check_circle

Complete

Success / Error

notifications Notification System

The notification system provides a queued, template-driven approach to sending email, SMS, and push notifications. Notifications are created in code, queued to the database, and processed in batches by a scheduled task. Templates use Handlebars syntax and are fully data-driven.

Core Components

topic

NotificationTopic

Categorizes notifications by type (e.g., OrderConfirmation, PasswordReset). Each topic can have multiple templates.

send

NotificationMethod

Delivery channel: Email, SMS, or Push. Each method implements INotificationService.

description

NotificationTemplate

Handlebars-powered template defining the content for a given topic and method. Supports dynamic data and conditional logic.

mail

Notification

A queued notification instance with recipient details, replacement data, and a reference to its template.

info

NotificationStatus

Tracks delivery state: Pending, Sent, Failed, Retrying. Enables monitoring and retry logic for failed deliveries.

Notification Processing Flow

code

NotificationBase

Subclass

conversion_path

QueueNotification

Pipeline

database

DB: Pending

Queued Record

schedule

ProcessBatch

Every Minute

send

INotificationService

Dispatch

email

Email

sms

SMS

notifications

Push

Template Capabilities

data_object

Handlebars Syntax

Full Handlebars support including loops, conditionals, and nested object access for rich template rendering.

edit_note

Data-Driven

Templates are stored in the database and can be modified at runtime without redeployment, enabling business user customization.

stacks

Multiple Templates

Each notification can define multiple templates per topic: customer-facing, back-office, and internal audit copies.

add_alert Creating Notifications

To create a notification, subclass NotificationBase and define a Name property that matches the notification topic. The Replacements dictionary provides the template data, and PopulateReplacementsAsync allows for custom data loading logic.

NotificationBase Subclass Example

public class OrderConfirmationNotification : NotificationBase
{
    public override string Name => "OrderConfirmation";

    public Guid OrderId { get; set; }

    public override async Task PopulateReplacementsAsync(
        IPipelineContext context,
        CancellationToken token)
    {
        var order = await context.GetById<Order>(OrderId, token);

        Replacements["orderNumber"] = order.OrderNumber;
        Replacements["customerName"] = order.CustomerName;
        Replacements["totalAmount"] = order.Total.ToString("C");
        Replacements["order"] = order; // nested object for template access
    }
}

Queuing a Notification

// Create and queue the notification
var notification = new OrderConfirmationNotification
{
    OrderId = order.Id,
    To = order.CustomerEmail
};

await notification.QueueAsync(context, token);

Calling QueueAsync persists the notification to the database with a Pending status. The ProcessNotificationBatchTask scheduled task picks it up on its next run (every minute) and dispatches it through the appropriate INotificationService implementation.

draft Notification Templates

Notification templates are HTML files stored on disk in the plugin's Templates folder. They use Handlebars syntax for dynamic content. File naming and folder structure follow strict conventions to enable automatic registration at startup.

File Naming Convention

# Pattern: {Topic}.{Method}.{Section}.html (case-sensitive)

OrderConfirmation.Email.body.html      # Email body template
OrderConfirmation.Email.subject.html   # Email subject line
OrderConfirmation.Email.from.html      # Sender address
OrderConfirmation.Email.to.html        # Recipient address

Sections available: body, subject, from, to. File names are case-sensitive and must match the notification topic name exactly.

Template Storage

folder PluginRoot/
folder Templates/
folder Backend/ Back-office templates
folder Frontend/ Customer-facing templates
description OrderConfirmation.Email.body.html
description OrderConfirmation.Email.subject.html

On startup, the backend automatically creates database records from the disk-based template files. Existing database records are not overwritten, so runtime modifications to templates in the admin UI persist across deployments.

Handlebars Template Examples

Simple Replacements

<!-- Access top-level replacement values -->
<p>Hello, {{firstName}}!</p>
<p>Your order {{orderNumber}} has been confirmed.</p>

Nested Objects

<!-- Dot notation for nested properties -->
<p>{{contact.street1}}</p>
<p>{{contact.city}}, {{contact.state}} {{contact.zip}}</p>

Arrays and Loops

<!-- Iterate over arrays with #each -->
<table>
  {{#each order.items}}
  <tr>
    <td>{{this.productName}}</td>
    <td>{{this.quantity}}</td>
    <td>{{this.price}}</td>
  </tr>
  {{/each}}
</table>

settings Settings System

The settings system provides a strongly-typed configuration layer for plugins. Settings are stored in the database with a key-value pattern and accessed via the ISettings interface. Settings can be exposed to the frontend through pipeline hooks.

ISettings Interface

public class MyPluginSettings : ISettings
{
    // Key: "MyPluginSettings:EnableFeatureX"
    public bool EnableFeatureX { get; set; } = true;

    // Key: "MyPluginSettings:MaxRetryCount"
    public int MaxRetryCount { get; set; } = 3;

    // Key: "MyPluginSettings:ApiEndpoint"
    public string ApiEndpoint { get; set; } = "";
}

Keys follow the ClassName:PropertyName pattern with a colon separator. Properties are hydrated from the database automatically when the settings object is resolved.

Reading Settings

// Resolve settings from the pipeline context
var settings = context.GetSettings<MyPluginSettings>();

if (settings.EnableFeatureX)
{
    // Feature-gated logic
}

Frontend Exposure via PxConfig

Settings that need to be available in the frontend are exposed through the BuildPxConfigPipeline. Hooks return IPxConfigSection objects that are serialized to JSON and sent to the client.

public class MyPxConfigHook : IHook<BuildPxConfigPipeline>
{
    public async Task ExecuteAsync(BuildPxConfigPipeline pipeline,
        IPipelineContext context, CancellationToken token)
    {
        var settings = context.GetSettings<MyPluginSettings>();

        pipeline.Config.Add(new PxConfigSection("myPlugin")
        {
            ["enableFeatureX"] = settings.EnableFeatureX,
            ["maxRetryCount"] = settings.MaxRetryCount,
        });
    }
}
error

Security Warning

Never expose credentials, API keys, or secrets via IPxConfigSection. PxConfig data is sent to the browser and is visible in the page source. Only include values that are safe for public exposure.

tune Site Setup Wizard

The Site Setup Wizard is an admin-facing configuration flow located at /admin/system/sitesetup. It provides a multi-step wizard for initial platform configuration, connector setup, and ongoing system management.

Wizard Structure

1

Setup Guide

Initial platform configuration. Sets the site name, base URL, and primary admin credentials.

2

Connectors

Added by ConnectCore. Each connector contributes its own setup routes and configuration forms.

3

Settings

Plugin-specific settings. Each plugin can contribute settings panels visible in the wizard.

4

Documentation

Per-connector documentation tabs. Each connector can provide inline help, setup guides, and API reference links.

ConnectCore Integration

The ConnectCore plugin extends the setup wizard with a dedicated "Connectors" step. Each registered connector contributes its own setup routes, documentation, and configuration UI. Settings are persisted via the ConnectorsController.

chevron_right

Per-Connector Tabs

Each connector in the wizard provides three tabs: Setup Guide (step-by-step instructions), Settings (configuration form), and Documentation (reference material and API docs).

hub Connector Configuration

Each connector implements IConnectorSettings to declare its configuration surface. Connectors are discovered through the GetConnectorSetupsPipeline and their settings are stored on a per-site basis.

IConnectorSettings Interface

public class NetSuiteConnectorSettings : IConnectorSettings
{
    public string ConnectorId => "netsuite";

    public string AccountId { get; set; }
    public string ConsumerKey { get; set; }
    public string ConsumerSecret { get; set; }
    public string TokenId { get; set; }
    public string TokenSecret { get; set; }
}

Discovery & Storage

search

Connector Discovery

The GetConnectorSetupsPipeline scans registered plugins for IConnectorSettings implementations and builds the connector list for the setup wizard.

database

Per-Site Settings Storage

Connector settings are stored per-site, allowing multi-tenant deployments where each site connects to a different ERP instance with its own credentials.

Admin UI Integration

The admin UI automatically renders configuration forms based on the IConnectorSettings properties. When a connector is selected in the setup wizard, its settings form is displayed and values are saved through the ConnectorsController REST API.

payments Payment Provider Configuration

Payment configuration is managed through the PaymentSettings class, which controls the payment mode, enabled providers, dashboard routes, and supported payment methods. The GetPaymentProviderByTypePipeline handles multi-provider selection at runtime.

PaymentSettings Properties

toggle_on

Mode

Controls the active payment environment. Options: Testing (sandbox credentials, mock transactions) or Live (production credentials, real transactions).

check_circle

EnabledProviders

List of active payment provider identifiers. Multiple providers can be enabled simultaneously, and the system selects the appropriate one based on payment type and configuration.

Dashboard Routes & Features

account_balance_wallet WalletDashboardRouteEnabled

Enables the wallet/balance dashboard in the customer portal for viewing account balances and stored payment methods.

credit_score CreditsDashboardRouteEnabled

Enables the credits dashboard for customers to view and manage store credits, refund credits, and promotional balances.

credit_card CreditCardEnabled

Enables credit card payments through the configured provider. Supports tokenized card storage for returning customers.

account_balance AchEnabled

Enables ACH (bank transfer) payments. Requires provider-level ACH support and bank account verification configuration.

webhook PaymentMadeWebhookEnabled

Enables webhook notifications when payments are processed, allowing external systems to react to payment events.

undo LineItemLevelRefundsEnabled

Enables granular refund processing at the individual line item level rather than full order refunds only.

Multi-Provider Selection

The GetPaymentProviderByTypePipeline resolves the correct payment provider at runtime based on the payment type and the site's configuration. This allows different payment methods (credit card, ACH) to route to different providers within the same deployment.

account_tree Business Workflows

The platform supports end-to-end business workflows built on composable building blocks. The primary flow is the Order-to-Cash cycle that spans from sales collection through payment processing and ERP synchronization.

Order-to-Cash Flow

shopping_cart

SalesCollection

Orders & Quotes

receipt_long

Invoicing

Invoice Generation

payments

Payments

Payment Processing

target

PaymentTargets

Apply to Invoices

sync

Connector Sync

Push to ERP

Key Pipelines

conversion_path

PayOnAccountPipeline

Processes payments against customer account balances. Applies credits and calculates remaining balances due.

conversion_path

PayInvoicePipeline

Handles direct invoice payment processing. Supports partial payments, multiple payment methods, and split payments across invoices.

conversion_path

CalculateBalanceDuePipeline

Computes the outstanding balance for a customer or invoice, factoring in payments, credits, adjustments, and pending transactions.

Payment Statuses

Pending PartiallyComplete Completed Error Declined Refunded

eCommerce & ERP Integration

storefront

Site Plugin (eCommerce)

The Site plugin provides the customer-facing dashboard and checkout flows. All flows are composed from the platform's building blocks (pipelines, hooks, and entities), not hardcoded.

sync_alt

ERP-Specific Nuances

Each connector handles ERP-specific differences (field mappings, validation rules, sync timing) through its own pipeline hooks, keeping the core workflow agnostic to the target ERP.

account_tree Submodule Operations

The Clarity repository uses Git submodules for Core and each plugin. Understanding the submodule workflow is essential for day-to-day development and coordinating changes across multiple repositories.

Common Submodule Commands

# Initialize and pull all submodules after cloning
git submodule update --init

# Run a command across all submodules
git submodule foreach 'git checkout develop && git pull'

# Check status of all submodules
git submodule foreach 'git status'

# Update submodule references to latest commits
git submodule update --remote

Branch Coordination

When working on a feature that spans multiple submodules, branches must be coordinated to ensure all changes are tested together before merging.

chevron_right

Feature Branch Naming

Use the same branch name across all submodules involved in a feature (e.g., feature/add-new-payment-method) to maintain traceability.

chevron_right

Testing Together

The parent repository's branch should reference the feature branch commits in each submodule so CI builds and tests run against the complete changeset.

Pull Request Workflow

1

Create Submodule PRs

Open PRs in each submodule repository for the changes specific to that submodule.

2

Link in Project PR

In the parent project PR description, include links to all related submodule PRs for cross-reference.

3

Merge Submodules First

Wait for submodule PRs to be reviewed and merged to develop before merging the parent project PR.

4

Update References

After submodule merges, update the parent project's submodule references to point to the develop branch HEAD.

lightbulb

Best Practice

Always wait for submodule PRs to merge to develop before updating the parent project's submodule references. This ensures the parent always references stable, reviewed commits rather than in-flight feature branch code.

help

Frequently Asked Questions