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:
- Scratch orgs (temporary, 1-30 days)
- Sandboxes (requires existing production org)
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:
- Code running inside the managed package
- Users logged in via LMA (License Management App)
The Search for a Global Method:
| What We Tried | Result |
|---|---|
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:
- Create a
KtapiSetupServiceglobal class (see spec in outcome-demo-setup-findings) - Document the
ServiceCall.execute('PostInstallClass.doInstall', ...)pattern - 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:
- S3 (document storage)
- SendGrid (email)
- OpenExchangeRates (currency)
- FlightStats (flight data)
- ElasticSearch (search)
- GoogleMaps (maps)
- ContentEncryption
- HandsonTable (license)
- FieldEncryption
- KTAPI (obviously)
Discovery: The endpoints are only created when:
- Code tries to access an endpoint that doesn’t exist
EndpointsService.buildInstance()is called- 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 Tried | Result |
|---|---|
| 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:
- Add “Foundation Setup” as a prerequisite golden config
- Document the dependency chain clearly
- Make the Brand trigger error message more helpful
- 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.
| ID | Pain Point | Status | Added |
|---|---|---|---|
| P001 | SFDX cannot create permanent orgs | Documented | 2026-01-11 |
| P002 | Protected endpoints require LMA | Documented | 2026-01-11 |
| P003 | Default endpoints not created on install | Documented | 2026-01-11 |
| P004 | Missing staging Remote Site Setting | Documented | 2026-01-11 |
| P005 | Golden config fields may not exist in all package versions | Documented | 2026-01-11 |
| P006 | Brand creation requires hidden foundation data (CurrencyBook, Language) | Documented | 2026-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