Operations & Platform Services
Deployment pipelines, scheduled tasks, notification system, settings management, site setup, payment configuration, and business workflows.
On this page
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
Azure DevOps
Pipeline Trigger
Build
Backend + Frontend
Docker Push
Push to Registry
Artifact
docker-compose.yml
VM Deploy
SSH + docker compose up
Artifact Retrieval & File Transfer
Artifact Download
Azure DevOps Build Pipeline publishes the docker-compose.yml as a build artifact. The release pipeline downloads this artifact for deployment.
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
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.
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>(); });
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
Cron Trigger
Cron Expression
TaskBase
Scheduled Task
IPipelineContext
Scoped Context
Execute Work
Run Business Logic
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
NotificationTopic
Categorizes notifications by type (e.g., OrderConfirmation, PasswordReset). Each topic can have multiple templates.
NotificationMethod
Delivery channel: Email, SMS, or Push. Each method implements INotificationService.
NotificationTemplate
Handlebars-powered template defining the content for a given topic and method. Supports dynamic data and conditional logic.
Notification
A queued notification instance with recipient details, replacement data, and a reference to its template.
NotificationStatus
Tracks delivery state: Pending, Sent, Failed, Retrying. Enables monitoring and retry logic for failed deliveries.
Notification Processing Flow
NotificationBase
Subclass
QueueNotification
Pipeline
DB: Pending
Queued Record
ProcessBatch
Every Minute
INotificationService
Dispatch
SMS
Push
Template Capabilities
Handlebars Syntax
Full Handlebars support including loops, conditionals, and nested object access for rich template rendering.
Data-Driven
Templates are stored in the database and can be modified at runtime without redeployment, enabling business user customization.
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
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, }); } }
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
Setup Guide
Initial platform configuration. Sets the site name, base URL, and primary admin credentials.
Connectors
Added by ConnectCore. Each connector contributes its own setup routes and configuration forms.
Settings
Plugin-specific settings. Each plugin can contribute settings panels visible in the wizard.
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.
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
Connector Discovery
The GetConnectorSetupsPipeline scans registered plugins for IConnectorSettings implementations and builds the connector list for the setup wizard.
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
Mode
Controls the active payment environment. Options: Testing (sandbox credentials, mock transactions) or Live (production credentials, real transactions).
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
SalesCollection
Orders & Quotes
Invoicing
Invoice Generation
Payments
Payment Processing
PaymentTargets
Apply to Invoices
Connector Sync
Push to ERP
Key Pipelines
PayOnAccountPipeline
Processes payments against customer account balances. Applies credits and calculates remaining balances due.
PayInvoicePipeline
Handles direct invoice payment processing. Supports partial payments, multiple payment methods, and split payments across invoices.
CalculateBalanceDuePipeline
Computes the outstanding balance for a customer or invoice, factoring in payments, credits, adjustments, and pending transactions.
Payment Statuses
eCommerce & ERP Integration
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.
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.
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.
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
Create Submodule PRs
Open PRs in each submodule repository for the changes specific to that submodule.
Link in Project PR
In the parent project PR description, include links to all related submodule PRs for cross-reference.
Merge Submodules First
Wait for submodule PRs to be reviewed and merged to develop before merging the parent project PR.
Update References
After submodule merges, update the parent project's submodule references to point to the develop branch HEAD.
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.