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 │
│ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ 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;