Skip to content

Data Model: Merchant & Tenant

Status: Draft | Owner: Platform Team | Last Updated: 2026-04-03

Overview

Merchant and tenant domain model for Simpaisa's multi-market payment platform. Defines the identity, access control, configuration, and multi-tenancy boundaries for merchants operating across PK, BD, NP, and IQ. Integrates with ControlPlane.com for identity federation and tenant provisioning.


Entity: Merchant

Field Type Description
id string ULID
name string Legal entity name
tradingName string Display/brand name
tier MerchantTier STANDARD, PREMIUM, ENTERPRISE
status MerchantStatus PENDING, ACTIVE, SUSPENDED, CLOSED
markets []string ISO 3166-1 alpha-2 codes (PK, BD, etc.)
products []string Enabled product lines
kybStatus KYBStatus NOT_STARTED, IN_PROGRESS, VERIFIED, REJECTED
industry string MCC or industry classification
taxId string Tax registration number (encrypted)
address string Registered address
country string Country of incorporation
cpTenantId string ControlPlane.com tenant identifier
cpOrgId string ControlPlane.com organisation ID
createdAt datetime Onboarding timestamp
updatedAt datetime Last modification

Entity: MerchantUser

Field Type Description
id string ULID
merchantId string FK → Merchant
email string Unique email address
fullName string Display name
role string FK → Role
mfaEnabled bool Whether MFA is active
mfaMethod MFAMethod TOTP, SMS, WEBAUTHN (nullable)
status UserStatus ACTIVE, INVITED, SUSPENDED, DEACTIVATED
lastLoginAt datetime Last successful login
cpUserId string ControlPlane.com user identity
createdAt datetime Account creation

Entity: Role

Field Type Description
id string ULID
merchantId string FK → Merchant (nullable for system roles)
name string Role name (e.g. "admin", "finance")
description string Human-readable description
permissions []string FK → Permission IDs
isSystem bool System-defined (immutable) role
createdAt datetime Creation timestamp

Default System Roles

Role Description Key Permissions
owner Merchant account owner All permissions
admin Full administrative access All except ownership transfer
finance Financial operations view_transactions, initiate_payout, view_settlements
developer API integration manage_api_keys, view_webhooks, view_logs
viewer Read-only access view_transactions, view_dashboard

Entity: Permission

Field Type Description
id string ULID
code string Machine-readable code (e.g. manage_api_keys)
name string Human-readable label
category string Grouping (TRANSACTION, API, SETTINGS, USER)
description string What this permission grants

Permission Catalogue

Code Category Description
view_transactions TRANSACTION View transaction history
initiate_payin TRANSACTION Create pay-in transactions
initiate_payout TRANSACTION Create disbursements
initiate_refund TRANSACTION Refund completed transactions
manage_api_keys API Create/revoke API keys
view_webhooks API View webhook configurations
manage_webhooks API Create/update/delete webhooks
manage_users USER Invite/remove team members
manage_roles USER Assign/modify roles
view_settlements TRANSACTION View settlement reports
manage_settings SETTINGS Update merchant settings
view_dashboard TRANSACTION Access analytics dashboard

Entity: APIKey

Field Type Description
id string ULID
merchantId string FK → Merchant
keyHash string SHA-256 hash of the API key
keyPrefix string First 8 chars for identification
environment Environment SANDBOX, PRODUCTION
label string User-defined label
permissions []string Scoped permissions (subset of role)
ipWhitelist []string Allowed source IPs (empty = all)
status KeyStatus ACTIVE, REVOKED, EXPIRED
createdAt datetime Issuance timestamp
expiresAt datetime Expiry timestamp (nullable = no expiry)
lastUsedAt datetime Last API call timestamp
createdBy string FK → MerchantUser who created
revokedBy string FK → MerchantUser who revoked (nullable)
revokedAt datetime Revocation timestamp (nullable)

Security Note: The raw API key is shown once at creation time and never stored. Only keyHash is persisted. Key rotation requires creating a new key and revoking the old one.


Entity: WebhookConfig

Field Type Description
id string ULID
merchantId string FK → Merchant
url string HTTPS callback endpoint
events []string Subscribed event types
secret string HMAC-SHA256 signing key (encrypted)
version string Webhook payload version (v1, v2)
status WebhookStatus ACTIVE, PAUSED, FAILED
failureCount int Consecutive delivery failures
maxRetries int Retry attempts before PAUSED
createdAt datetime Creation timestamp

Entity: RateConfig

Field Type Description
id string ULID
merchantId string FK → Merchant
channelId string FK → Channel
product string PAY_IN, PAY_OUT, REMITTANCE, CARD
feeType FeeType FLAT, PERCENTAGE, TIERED
feeValue decimal Fee amount or percentage
minFee decimal Minimum fee floor (nullable)
maxFee decimal Maximum fee cap (nullable)
currency Currency Fee currency
effectiveFrom date Rate effective date
effectiveTo date Rate end date (nullable)

Entity: SLAConfig

Field Type Description
id string ULID
merchantId string FK → Merchant
product string Product line
uptimeSla decimal Uptime percentage target (e.g. 99.95)
latencyP99Ms int P99 latency target in milliseconds
supportTier string STANDARD, PREMIUM, DEDICATED
escalationEmail string Escalation contact

Entity: NotificationPreference

Field Type Description
id string ULID
merchantId string FK → Merchant
channel NotifChannel EMAIL, SMS, SLACK, WEBHOOK
events []string Event types to notify on
recipients []string Destination addresses/numbers
enabled bool Active/inactive

ControlPlane.com Identity Mapping

┌─────────────────────────────────────────────────────┐
│                  ControlPlane.com                     │
│                                                       │
│  ┌─────────────┐    ┌──────────────┐                 │
│  │  CP Org      │───▶│  CP Tenant   │                │
│  │  (cpOrgId)   │    │ (cpTenantId) │                │
│  └──────┬──────┘    └──────┬───────┘                 │
│         │                   │                         │
│         │    ┌──────────────┴───────┐                 │
│         │    │   CP Service Account │                 │
│         │    │   (per environment)  │                 │
│         │    └─────────────────────┘                  │
│         │                                             │
│    ┌────┴─────┐                                       │
│    │ CP User  │ ←── OIDC federation                   │
│    │(cpUserId)│                                       │
│    └──────────┘                                       │
└─────────────────────────────────────────────────────┘
          │                    │
          ▼                    ▼
   ┌──────────────┐    ┌──────────────┐
   │ MerchantUser │    │   Merchant   │
   │ .cpUserId    │    │ .cpTenantId  │
   └──────────────┘    │ .cpOrgId     │
                       └──────────────┘

Multi-Tenancy Boundaries

Boundary Enforcement
Data isolation SurrealDB namespace per merchant tier; row-level for STANDARD
API key scoping Every request authenticated with merchant-scoped key
Rate limiting Per-merchant, per-channel rate limits via KrakenD
Webhook isolation Webhook secrets unique per merchant
Log segregation merchantId mandatory on all log entries
Audit trail Immutable per-merchant audit log
Environment isolation SANDBOX and PRODUCTION fully separated

SurrealDB Table Mapping

DEFINE TABLE merchant SCHEMAFULL;
DEFINE FIELD name        ON merchant TYPE string;
DEFINE FIELD tradingName ON merchant TYPE string;
DEFINE FIELD tier        ON merchant TYPE string
  ASSERT $value IN ['STANDARD','PREMIUM','ENTERPRISE'];
DEFINE FIELD status      ON merchant TYPE string
  ASSERT $value IN ['PENDING','ACTIVE','SUSPENDED','CLOSED'];
DEFINE FIELD markets     ON merchant TYPE array<string>;
DEFINE FIELD products    ON merchant TYPE array<string>;
DEFINE FIELD kybStatus   ON merchant TYPE string;
DEFINE FIELD cpTenantId  ON merchant TYPE string;
DEFINE FIELD cpOrgId     ON merchant TYPE string;
DEFINE FIELD createdAt   ON merchant TYPE datetime DEFAULT time::now();

DEFINE INDEX idx_merchant_status ON merchant FIELDS status;
DEFINE INDEX idx_merchant_cp     ON merchant FIELDS cpTenantId UNIQUE;

DEFINE TABLE merchant_user SCHEMAFULL;
DEFINE FIELD merchantId ON merchant_user TYPE record<merchant>;
DEFINE FIELD email      ON merchant_user TYPE string;
DEFINE FIELD fullName   ON merchant_user TYPE string;
DEFINE FIELD role       ON merchant_user TYPE record<role>;
DEFINE FIELD mfaEnabled ON merchant_user TYPE bool DEFAULT false;
DEFINE FIELD status     ON merchant_user TYPE string;
DEFINE FIELD cpUserId   ON merchant_user TYPE string;

DEFINE INDEX idx_user_email ON merchant_user FIELDS email UNIQUE;
DEFINE INDEX idx_user_merchant ON merchant_user FIELDS merchantId;

DEFINE TABLE role SCHEMAFULL;
DEFINE FIELD merchantId  ON role TYPE option<record<merchant>>;
DEFINE FIELD name        ON role TYPE string;
DEFINE FIELD permissions ON role TYPE array<string>;
DEFINE FIELD isSystem    ON role TYPE bool DEFAULT false;

DEFINE TABLE api_key SCHEMAFULL;
DEFINE FIELD merchantId  ON api_key TYPE record<merchant>;
DEFINE FIELD keyHash     ON api_key TYPE string;
DEFINE FIELD keyPrefix   ON api_key TYPE string;
DEFINE FIELD environment ON api_key TYPE string
  ASSERT $value IN ['SANDBOX','PRODUCTION'];
DEFINE FIELD permissions ON api_key TYPE array<string>;
DEFINE FIELD ipWhitelist ON api_key TYPE array<string>;
DEFINE FIELD status      ON api_key TYPE string
  ASSERT $value IN ['ACTIVE','REVOKED','EXPIRED'];
DEFINE FIELD createdAt   ON api_key TYPE datetime DEFAULT time::now();
DEFINE FIELD expiresAt   ON api_key TYPE option<datetime>;

DEFINE INDEX idx_key_hash     ON api_key FIELDS keyHash UNIQUE;
DEFINE INDEX idx_key_merchant ON api_key FIELDS merchantId;

DEFINE TABLE webhook_config SCHEMAFULL;
DEFINE FIELD merchantId ON webhook_config TYPE record<merchant>;
DEFINE FIELD url        ON webhook_config TYPE string;
DEFINE FIELD events     ON webhook_config TYPE array<string>;
DEFINE FIELD status     ON webhook_config TYPE string;

DEFINE TABLE rate_config SCHEMAFULL;
DEFINE FIELD merchantId ON rate_config TYPE record<merchant>;
DEFINE FIELD channelId  ON rate_config TYPE record<channel>;
DEFINE FIELD product    ON rate_config TYPE string;
DEFINE FIELD feeType    ON rate_config TYPE string;
DEFINE FIELD feeValue   ON rate_config TYPE decimal;
DEFINE FIELD currency   ON rate_config TYPE string;
DEFINE FIELD effectiveFrom ON rate_config TYPE datetime;