← Back to Gallery
reference Documentation

The Demo Org Setup Chronicle

A living narrative documenting the friction, pain points, and lessons learned while setting up Kaptio demo orgs. Internal reference for improving the onboarding experience.

Published: January 11, 2026
deliverytechnical

The Demo Org Setup Chronicle

What is this? A living document capturing real friction encountered during demo org provisioning. Each pain point informs product improvements, documentation needs, and automation opportunities.


Prologue: The Mission

Date: January 11, 2026
Objective: Provision a fresh Salesforce org for the Kaptio Voyage Cruise Solution demo
Org Name: Voyage Cruise Demo
Target: Full KTAPI connectivity with staging environment

What should be a 15-minute task became a multi-hour expedition through undocumented workflows, protected settings, and tribal knowledge. This chronicle captures that journey.


Chapter 1: The Org Creation Paradox

Pain Point #1: SFDX Cannot Create Permanent Orgs

Expectation: Use sf org create to spin up a demo org programmatically.

Reality: SFDX can only create:

What Actually Works: Manual signup at developer.salesforce.com/signup

Impact: Every demo org requires manual web signup. No automation possible for permanent Developer Edition orgs.

Evidence:

# What we wanted
sf org create scratch -f config/project-scratch-def.json -a voyage-cruise-demo -d -y 30

# What we got
# "This creates a TEMPORARY org that expires in 30 days"

Workaround: Web signup, then authenticate via SFDX:

sf org login web -a voyage-cruise-demo

Recommendation: Document this clearly. Consider Trialforce for repeatable demo environments.


Chapter 2: The Protected Endpoint Labyrinth

Pain Point #2: KTAPI Credentials Require LMA Access

Expectation: After creating a KTAPI client in the dashboard, configure the Salesforce org via CLI or API.

Reality: The ProtectedEndpoint__c custom setting is a protected setting in the managed package. It can only be written by:

  1. Code running inside the managed package
  2. Users logged in via LMA (License Management App)

The Search for a Global Method:

What We TriedResult
GlobalTestDataFactory.createProtectedEndpoint()@IsTest only
IntegrationsController.upsertEndpoints()❌ Creates custom endpoints, not KTAPI
EndpointsService.createProtectedEndpoints()public not global
Direct Anonymous Apex❌ Can’t access protected setting

The Discovery: PostInstallClass.doInstall is callable via ServiceCall.execute():

// This works! But it's not documented anywhere
Map<String, Object> args = new Map<String, Object>{
    'fromVersion' => new Map<String, Integer>{'major' => 1, 'minor' => 0, 'patch' => 0},
    'toVersion' => new Map<String, Integer>{'major' => 1, 'minor' => 0, 'patch' => 1},
    'notificationTo' => new List<String>{'your-email@example.com'},
    'throwException' => false
};
KaptioTravel.ServiceCall.execute('PostInstallClass.doInstall', args);

Impact: 2+ hours spent reverse-engineering the codebase to find a programmatic way to create default endpoints. This knowledge exists only in tribal memory.

Recommendation:

  1. Create a KtapiSetupService global class (see spec in outcome-demo-setup-findings)
  2. Document the ServiceCall.execute('PostInstallClass.doInstall', ...) pattern
  3. Consider customer self-service for sandbox reconnection

Chapter 3: The Default Endpoints That Weren’t

Pain Point #3: Package Install Doesn’t Create Default Endpoints

Expectation: Installing the Kaptio Travel package creates all required ProtectedEndpoint__c records.

Reality: The post-install script is disabled due to “Ghost install user” issues. Default endpoints are NOT created on package installation.

What’s Missing After Fresh Install:

Discovery: The endpoints are only created when:

  1. Code tries to access an endpoint that doesn’t exist
  2. EndpointsService.buildInstance() is called
  3. Which calls createProtectedEndpoints() internally

But: If KTAPI endpoint exists (manually created), accessing it won’t trigger creation of the others.

Solution Found:

sf apex run -o your-org << 'EOF'
Map<String, Object> args = new Map<String, Object>{
    'fromVersion' => new Map<String, Integer>{'major' => 1, 'minor' => 0, 'patch' => 0},
    'toVersion' => new Map<String, Integer>{'major' => 1, 'minor' => 0, 'patch' => 1},
    'notificationTo' => new List<String>{'your-email@example.com'},
    'throwException' => false
};
KaptioTravel.ServiceCall.execute('PostInstallClass.doInstall', args);
EOF

Impact: Every new org requires manual intervention to create default endpoints. This is a hidden step that causes confusion.

Recommendation: Re-enable post-install script or provide clear documentation and tooling.


Chapter 4: The Remote Site Setting Surprise

Pain Point #4: Missing Remote Site for Staging KTAPI

Expectation: KTAPI connectivity works after configuring credentials.

Reality: First API call fails with:

System.CalloutException: Unauthorized endpoint, please check Setup->Security->Remote site settings

The Fix: Deploy a Remote Site Setting for the staging environment:

<?xml version="1.0" encoding="UTF-8"?>
<RemoteSiteSetting xmlns="http://soap.sforce.com/2006/04/metadata">
    <disableProtocolSecurity>false</disableProtocolSecurity>
    <isActive>true</isActive>
    <url>https://ktapi-staging-gcp.kaptioapis.com</url>
    <description>KTAPI Staging Environment</description>
</RemoteSiteSetting>

Note: Production KTAPI URL is already in the package. Staging requires manual addition.

Recommendation: Include staging Remote Site Settings in demo setup documentation or package.


Chapter 6: The Foundation Data Abyss

Pain Point #6: Brand Creation Requires Hidden Foundation Data

Expectation: Create a Brand record with basic fields like Name, SalesInvoicePrefix, and Currency.

Reality: Brand creation fails with cryptic NullPointerException from the OnBrands trigger:

KaptioTravel.OnBrands: execution of BeforeInsert
caused by: System.NullPointerException: Attempt to de-reference a null object

The Investigation:

What We TriedResult
Create Brand with Name + SalesInvoicePrefix❌ NPE in trigger
Add CurrencyIsoCode❌ Still NPE
Add BuyRates + SellRates lookups❌ Still NPE
Check required fields via describe💡 Found 5 required fields!

The Hidden Requirements:

The Brand object has 5 required fields that aren’t obvious from the UI:

// Required Brand fields (discovered via Schema.describe)
kaptiotravel__creditinvoiceprefix__c (STRING)      // Credit note prefix
kaptiotravel__resellerstatementprefix__c (STRING)  // Reseller statement prefix
kaptiotravel__salesinvoiceprefix__c (STRING)       // Sales invoice prefix
kaptiotravel__supplierinvoiceprefix__c (STRING)    // Supplier invoice prefix
kaptiotravel__taxhandling__c (PICKLIST)            // Tax handling mode

But Wait, There’s More:

Even with all required fields, Brand creation fails if CurrencyBook records don’t exist:

// BuyRates__c and SellRates__c are lookups to CurrencyBook__c
// The trigger expects these to exist!

The Complete Dependency Chain:

┌─────────────────────────────────────────────────────────┐
│                FOUNDATION LAYER (Level 1)                │
├─────────────────────────────────────────────────────────┤
│  1. CurrencyBook__c (for Brand.BuyRates/SellRates)      │
│     └─ Name, CurrencyIsoCode, ExternalCode              │
│                                                          │
│  2. Language__c (for Channel.Language)                  │
│     └─ Name, IsoCode, ExternalCode, IsActive            │
│                                                          │
│  3. BookingNumberScheme__c (for Channel)                │
│     └─ Name, BookingNumberSequence, BookingPrefix       │
│                                                          │
│  4. ChannelRoleConfiguration__c (for Channel)           │
│     └─ Name (only required field!)                      │
├─────────────────────────────────────────────────────────┤
│                  BRAND LAYER (Level 2)                   │
├─────────────────────────────────────────────────────────┤
│  5. Brand__c (requires CurrencyBook)                    │
│     └─ Name, SalesInvoicePrefix, CreditInvoicePrefix    │
│     └─ ResellerStatementPrefix, SupplierInvoicePrefix   │
│     └─ TaxHandling, BuyRates (→CurrencyBook)            │
│     └─ SellRates (→CurrencyBook)                        │
├─────────────────────────────────────────────────────────┤
│                 CHANNEL LAYER (Level 3)                  │
├─────────────────────────────────────────────────────────┤
│  6. Channel__c (requires Brand + Language + BNS + CRC)  │
│     └─ Name, ChannelCode, Brand (→Brand)                │
│     └─ Language (→Language__c ID, not string!)          │
│     └─ BookingNumberScheme (→BookingNumberScheme__c)    │
│     └─ ChannelRoleConfiguration (→CRC)                  │
├─────────────────────────────────────────────────────────┤
│               CHANNEL CONFIG LAYER (Level 4)             │
├─────────────────────────────────────────────────────────┤
│  7. ChannelConfiguration__c (requires Channel)          │
│     └─ Name, Channel (→Channel), ExternalCode           │
└─────────────────────────────────────────────────────────┘

The Iceberg Effect: What looks like a simple “create Brand and Channel” task actually requires 7 different object types in a specific order!

The Fix - Create Foundation Data First:

# 1. Create CurrencyBook
sf data create record -s KaptioTravel__CurrencyBook__c \
  -v "Name='Default Currency Book' CurrencyIsoCode='USD' KaptioTravel__ExternalCode__c='DEFAULT-USD'" \
  -o voyage-cruise-demo

# 2. Create Language (if not exists)
sf data create record -s KaptioTravel__Language__c \
  -v "Name='English' KaptioTravel__IsoCode__c='en' KaptioTravel__ExternalCode__c='EN' KaptioTravel__IsActive__c=true" \
  -o voyage-cruise-demo

# 3. Now Brand works!
sf data create record -s KaptioTravel__Brand__c \
  -v "Name='Voyage Expeditions' \
      KaptioTravel__SalesInvoicePrefix__c='VE' \
      KaptioTravel__CreditInvoicePrefix__c='VC' \
      KaptioTravel__ResellerStatementPrefix__c='VR' \
      KaptioTravel__SupplierInvoicePrefix__c='VS' \
      KaptioTravel__TaxHandling__c='Exclusive' \
      KaptioTravel__BuyRates__c='CURRENCYBOOK_ID' \
      KaptioTravel__SellRates__c='CURRENCYBOOK_ID'" \
  -o voyage-cruise-demo

Impact: Golden Config deployment fails silently or with cryptic errors. Users have no idea that CurrencyBook and Language records are prerequisites for Brand creation.

Recommendation:

  1. Add “Foundation Setup” as a prerequisite golden config
  2. Document the dependency chain clearly
  3. Make the Brand trigger error message more helpful
  4. Consider auto-creating foundation data in PostInstallClass

Epilogue: The Happy Path (Eventually)

After navigating all obstacles, the complete setup sequence is:

# 1. Create Developer Edition org (manual - web signup)

# 2. Authenticate via SFDX
sf org login web -a voyage-cruise-demo

# 3. Enable Multi-Currency (manual - Setup UI)

# 4. Install Kaptio Travel package (manual or CLI)

# 5. Create KTAPI client in dashboard (manual - ktapi-dashboard UI)

# 6. Set up KTAPI endpoint (requires LMA or someone who has access)

# 7. Create default endpoints (programmatic - found the workaround!)
sf apex run -o voyage-cruise-demo << 'EOF'
Map<String, Object> args = new Map<String, Object>{
    'fromVersion' => new Map<String, Integer>{'major' => 1, 'minor' => 0, 'patch' => 0},
    'toVersion' => new Map<String, Integer>{'major' => 1, 'minor' => 0, 'patch' => 1},
    'notificationTo' => new List<String>{'your-email@example.com'},
    'throwException' => false
};
KaptioTravel.ServiceCall.execute('PostInstallClass.doInstall', args);
EOF

# 8. Add Remote Site Setting for staging (if using staging KTAPI)
# Deploy via metadata API

# 9. Verify connectivity
sf apex run -o voyage-cruise-demo << 'EOF'
String endpoint = KaptioTravel.KtApiAuthorizationService.getKtApiEndpoint();
String auth = KaptioTravel.KtApiAuthorizationService.getKtApiAuthorization();
System.debug('Endpoint: ' + endpoint);
System.debug('Auth: ' + auth.left(50) + '...');
EOF

# 10. Activate sync in ktapi-dashboard

Total Time: ~3 hours (should be ~30 minutes)


The Backlog: Future Pain Points

This section will grow as more setup attempts reveal additional friction.

IDPain PointStatusAdded
P001SFDX cannot create permanent orgsDocumented2026-01-11
P002Protected endpoints require LMADocumented2026-01-11
P003Default endpoints not created on installDocumented2026-01-11
P004Missing staging Remote Site SettingDocumented2026-01-11
P005Golden config fields may not exist in all package versionsDocumented2026-01-11
P006Brand creation requires hidden foundation data (CurrencyBook, Language)Documented2026-01-12

Appendix: Quick Reference Commands

Verify KTAPI Connection

sf apex run -o YOUR_ORG << 'EOF'
try {
    String endpoint = KaptioTravel.KtApiAuthorizationService.getKtApiEndpoint();
    String auth = KaptioTravel.KtApiAuthorizationService.getKtApiAuthorization();
    System.debug('✅ KTAPI: ' + endpoint);
    System.debug('✅ Auth: ' + auth.left(50) + '...');
} catch (Exception e) {
    System.debug('❌ ' + e.getMessage());
}
EOF

Create Default Endpoints

sf apex run -o YOUR_ORG << 'EOF'
Map<String, Object> args = new Map<String, Object>{
    'fromVersion' => new Map<String, Integer>{'major' => 1, 'minor' => 0, 'patch' => 0},
    'toVersion' => new Map<String, Integer>{'major' => 1, 'minor' => 0, 'patch' => 1},
    'notificationTo' => new List<String>{'your-email@example.com'},
    'throwException' => false
};
KaptioTravel.ServiceCall.execute('PostInstallClass.doInstall', args);
EOF

Test KTAPI API Call

sf apex run -o YOUR_ORG << 'EOF'
HttpRequest req = new HttpRequest();
req.setEndpoint(KaptioTravel.KtApiAuthorizationService.getKtApiEndpoint() + '/v1.0/item_types');
req.setMethod('GET');
req.setHeader('Authorization', KaptioTravel.KtApiAuthorizationService.getKtApiAuthorization());
req.setHeader('Content-Type', 'application/json');
req.setTimeout(30000);

HttpResponse res = new Http().send(req);
System.debug('Status: ' + res.getStatusCode());
System.debug('Body: ' + res.getBody().left(200));
EOF

Last updated: January 11, 2026