Phoenix integrates with ERP systems through a normalized connector layer, with EkDB providing an intermediate staging database for reliable data synchronization.
ConnectCore is the foundation for all external system connectors in the Clarity platform. It provides the base interfaces, configuration patterns, and discovery mechanisms that every connector builds upon.
Marker interface for all connector API clients. Exposes a ConnectorId property that uniquely identifies the connector instance. Enables mock support across all connectors for testing.
Base interface for all connector configuration classes. Each connector provides its own implementation with vendor-specific properties (credentials, endpoints, timeouts).
Global connector settings shared across all connectors, including common timeouts and retry policies.
HTTP proxy configuration for connectors that need to route traffic through a corporate proxy or gateway.
Connectors register themselves with the platform through the GetConnectorSetupsPipeline hook. This pipeline is invoked at startup and whenever the admin UI needs to display available connectors and their configuration status.
// Each connector adds a hook to announce itself
[HookFor(typeof(GetConnectorSetupsPipeline))]
public class NetSuiteConnectorSetupHook : IPipelineHook
{
public Task ExecuteAsync(PipelineContext context)
{
context.GetResult<List<ConnectorSetup>>().Add(new ConnectorSetup
{
ConnectorId = "netsuite",
DisplayName = "NetSuite",
Vendor = "Oracle",
IsConfigured = _settings.IsValid()
});
return Task.CompletedTask;
}
}
EkDB maps external system IDs to internal Phoenix entities, providing a unified identity layer that spans multiple ERP systems, payment providers, and third-party services. It stores metadata alongside each mapping and supports caching for high-throughput lookups.
The primary interface for all external key database operations: resolve, store, update, and delete external ID mappings.
PostgreSQL implementation of IExtKeyDB. Stores mappings in a dedicated table with JSONB metadata columns for flexible, schema-less extension data.
Defines an external ID for an entity. Composed of a key definition type and the actual external value. Represents a single external system's identifier for a Phoenix entity.
Contains the full set of external ID mappings for a Phoenix entity. An EntRef can hold keys from multiple external systems simultaneously.
Controls resolution behavior: Read for lookup-only operations, ReadWrite when the mapping should be created if it does not exist.
Each mapping can carry arbitrary JSON metadata, job ID tracking for sync auditing, and timestamps. Caching reduces repeated database lookups during batch operations.
| Type | Description | Example |
|---|---|---|
StringEntKeyDef |
Simple string-based external key | "CUST-12345" |
CompositeEntKeyDef |
Multi-part key composed of several fields | Company+Branch+Id |
ParsableEntKeyDef |
Key that can be parsed from a formatted string | int, Guid, etc. |
ColonSepStringEntKeyDef |
Colon-separated composite key as a single string | "NS:12345:INV" |
Every connector follows a standardized directory layout that separates backend logic from frontend UI. This consistency makes it straightforward to navigate any connector once you understand the pattern.
Phoenix.Connector.XYZ/
Backend/
Clients/ — API client classes (HTTP/REST communication)
Controllers/ — API endpoints for admin configuration
DataModel/ — External system data models and DTOs
Pipelines/ — Integration pipelines (Search, Get, Create, Update)
Hooks/ — Setup and configuration hooks
XyzPlugin.cs — Plugin registration (IPlugin implementation)
XyzSettings.cs — Connector-specific configuration
Frontend/
plugin.json — Frontend plugin metadata
components/ — Setup UI components
routes/ — Admin routes for connector management
| Property | Purpose | Example |
|---|---|---|
Name | Plugin class display name | "NetSuite" |
WorkflowVendor | Vendor identifier for pipeline routing | "Oracle" |
WorkflowProduct | Product identifier for pipeline routing | "NetSuite" |
Follow these steps to create a new connector plugin. Each step includes the interface or base class to implement and a code example.
Implement IPlugin with Name, WorkflowVendor, and WorkflowProduct properties.
public class XyzPlugin : IPlugin
{
public string Name => "XYZ ERP";
public string WorkflowVendor => "XyzCorp";
public string WorkflowProduct => "XyzErp";
public void Configure(IServiceCollection services)
{
services.AddSingleton<XyzSettings>();
services.AddScoped<IXyzClient, XyzClient>();
}
}
Implement IConnectorSettings with connector-specific configuration properties.
public class XyzSettings : IConnectorSettings
{
public string BaseUrl { get; set; }
public string ApiKey { get; set; }
public int TimeoutMs { get; set; } = 30000;
public int MaxRetries { get; set; } = 3;
public bool IsValid() =>
!string.IsNullOrEmpty(BaseUrl) &&
!string.IsNullOrEmpty(ApiKey);
}
Build an HTTP client class using HttpClient or RestSharp with authentication and retry logic.
public class XyzClient : IConnectorClient
{
public string ConnectorId => "xyz";
private readonly RestClient _client;
public XyzClient(XyzSettings settings)
{
_client = new RestClient(new RestClientOptions(settings.BaseUrl)
{
Authenticator = new HttpBasicAuthenticator(settings.ApiKey, ""),
MaxTimeout = settings.TimeoutMs
});
}
}
Add admin endpoints for connector configuration and health checks.
[ApiController]
[Route("api/connectors/xyz")]
public class XyzController : ControllerBase
{
[HttpGet("status")]
public IActionResult GetStatus() => Ok(new { Connected = true });
[HttpPost("test-connection")]
public async Task<IActionResult> TestConnection() { ... }
}
Implement domain-specific pipelines (Search, Get, Create, Update) with [WorkflowNode] attributes for automatic discovery.
[WorkflowNode(
Vendor = "XyzCorp",
Product = "XyzErp",
Operation = "SearchCustomers")]
public class SearchXyzCustomersPipeline : SerialPipeline
{
public override async Task ExecuteAsync(PipelineContext context)
{
var query = context.GetInput<SearchQuery>();
var results = await _client.SearchCustomersAsync(query);
context.SetResult(results);
}
}
Register a GetConnectorSetupsPipeline hook so the platform discovers your connector.
Build the admin UI components in Frontend/components/ and routes in Frontend/routes/ for connector configuration and status display.
Required for all new integrations starting with NetSuite 2027.1. Uses client credentials flow with automatic token refresh on expiration.
Legacy authentication method. Still supported for existing integrations. Uses consumer key/secret with token key/secret for request signing.
Both authentication methods support automatic token refresh on expiration. The connector determines the active method from configuration and initializes the appropriate authenticator at client construction time.
public class NetSuiteSettings : IConnectorSettings
{
// Account
public string AccountId { get; set; }
public string Environment { get; set; } // "production" | "sandbox"
public string BaseUrl { get; set; }
// OAuth 2.0
public string OAuthClientId { get; set; }
public string OAuthClientSecret { get; set; }
public string OAuthTokenUrl { get; set; }
// Token-Based Auth (TBA)
public string ConsumerKey { get; set; }
public string ConsumerSecret { get; set; }
public string TokenId { get; set; }
public string TokenSecret { get; set; }
// Behavior
public int TimeoutMs { get; set; } = 30000;
public int DefaultPageSize { get; set; } = 100;
public int MaxRetries { get; set; } = 3;
}
The NetSuite client is built on RestSharp with lazy initialization and thread-safe access. It configures JSON serialization to handle NetSuite's specific date formats and null handling requirements.
[WorkflowNode(
Vendor = "Oracle",
Product = "NetSuite",
Operation = "SearchCustomers")]
public class SearchNetSuiteCustomersPipeline : SerialPipeline
{
private readonly INetSuiteClient _client;
private readonly IExtKeyDB _ekdb;
public override async Task ExecuteAsync(PipelineContext context)
{
var query = context.GetInput<CustomerSearchQuery>();
// Use SuiteQL for search (bulk read strategy)
var suiteQlResults = await _client.QueryAsync<NetSuiteCustomer>(
$"SELECT * FROM customer WHERE companyname LIKE '%{query.Term}%'"
);
// Resolve external keys through EkDB
var mapped = await _ekdb.ResolveAsync(suiteQlResults, ResolveOp.Read);
context.SetResult(mapped);
}
}
Used for Create, Update, and Delete operations where record-level access is needed.
Used for Search, Filter, and Bulk Read operations where SQL-like querying is more efficient.
All components move together per deployment. Every connector ships in the same Docker image with the same version tag. There is no per-customer or per-connector version selection in code.
Customer-specific behavior is achieved through configuration and data, not through different DLL versions. Feature flags, settings, and EkDB metadata drive behavioral differences between customers.
The deployment version is the Docker image tag, typically a commit hash or latest. This tag is the single source of truth for what code is running in any environment.
JsonExtensionData for handling custom fields without breaking deserialization.Version differences between ERP releases are handled inside each connector's client and DTOs. The connector layer absorbs API changes so that the rest of the platform sees a stable interface.
DTOs use the [JsonExtensionData] attribute to capture custom and extra fields from external API responses without breaking deserialization. This ensures forward-compatibility when the external API adds new fields.
Each connector owns its own NuGet packages. There is no shared ERP SDK across connectors. This isolation prevents version conflicts and allows each connector to upgrade its dependencies independently.
// Example: Capturing extra fields from NetSuite API responses
public class NetSuiteCustomerDto
{
public string Id { get; set; }
public string CompanyName { get; set; }
public string Email { get; set; }
// Captures any fields not mapped to properties above
[JsonExtensionData]
public Dictionary<string, JsonElement> ExtensionData { get; set; }
}
Clarity uses a layered dependency model rather than strict plugin isolation. Core plugins (ConnectCore, Payments, Invoicing, Sales) are allowed to reference each other within domain boundaries — for example, the eCommerce plugin orchestrates products, sales, and payments as a composite plugin. However, dependency direction is strictly enforced: plugins depend on Core, never the reverse. Connectors depend on ConnectCore but not on each other. Client-specific customizations interact with plugins exclusively through the pipeline hook system, never by direct code reference. This approach provides practical modularity while acknowledging the reality that a payments platform requires coordination between invoicing, sales, and payment processing.
Each ERP connector is a standalone plugin project that references ConnectCore. Development follows: scaffold from template → implement sync pipelines → configure EkDB mappings → test in sandbox → promote to production. Connectors register their pipelines during startup. See Connector Creation.
Connectors use config-driven behavior adaptation to handle version differences. For example, the NetSuite connector supports both TBA and OAuth 2.0 authentication modes. JsonExtensionData allows connectors to deserialize unknown fields from newer ERP API versions without breaking. Major version differences (like Epicor Eagle vs. Eclipse) are handled as separate connector projects. See Dependency Handling.
Payment gateways are provider plugins, not connectors. They implement the IPaymentProvider interface and register their capabilities. Connectors are specifically for ERP integrations (NetSuite, D365, Epicor, etc.). See Adding a Payment Provider.
Connectors are isolated from each other but share a common foundation through ConnectCore. No connector can reference another connector directly. However, connectors do interact with core plugins (Payments, Invoicing, Sales) through pipeline hooks — this is by design, as syncing ERP data requires writing to these domain entities. See Dependency Handling.
Minor API changes are absorbed via config-driven adaptation and JsonExtensionData. For breaking ERP API changes, a new connector version is branched, and migration tooling assists with the transition. Each connector build produces a separate hash, enabling independent deployment tracking. See Connector Versioning.
EkDB (External Key Database) is a middleware layer that maps between Clarity's internal entity IDs and the corresponding IDs in external systems (NetSuite internal IDs, D365 record numbers, etc.). This bidirectional mapping enables reliable sync operations — when an invoice is updated in NetSuite, EkDB resolves it to the correct Clarity entity, and vice versa. See EkDB.