Skip to content
v1.6BSL 1.1 → Apache 2.0API 67.0

Every Salesforce framework your team keeps rebuilding, in one Apex & LWC library.

Spend your team on the features that move the business, not on rebuilding the trigger framework, logging, security, and the rest of the plumbing every Salesforce org writes anyway. One managed package, secure by default, with public source on GitHub.

You write the line you expect. You get the depth you didn't: security by default, with logging, performance, and traceability built in.

Built to stay consistent after years of changes and dozens of contributors.

105global API classes
192production classes
70LWC components
100%Apex coverage · 4,082 tests

What it prevents

What happens when…

  • the trigger framework you copied into fifteen orgs has drifted in all fifteen, and the bug only reproduces in the one you didn't touch?
  • a query runs in system mode, skips the user's field permissions, and the security reviewer asks you to prove it didn't, with no single place to look?
  • a clean deploy finishes before the test suite that's supposed to guard it does, so the suite stops being a gate and starts being a formality?
  • the AI assistant writes Apex that compiles, passes review, and ignores every naming and bypass rule your team agreed on last quarter?

KernDX makes each one hard to reach: permissions on by default, one trigger path every org shares, every shortcut logged with the reason it was taken, and a standards file your AI assistant actually reads before it writes.

Why it exists

An accelerator you own, not a product you rent.

Most enterprise Salesforce orgs spend three to five years rebuilding the same foundations: selectors, trigger handling, logging, web services, async, masking, and field-level security (FLS) enforcement. They take on tech debt at every layer. KernDX is the integrated package those teams would have built with the budget and design discipline upfront: one library of frameworks for Apex and Lightning Web Components (LWC), plus the onboarding docs, continuous-integration (CI) tooling, and enforced defaults (permissions on by default, audit-logged bypasses, a coverage gate run on every release) that keep those guarantees true after a dozen contributors. The aim is to skip the rebuild cycle, not pay to rent your way around it.

And the architecture stays yours. The source is public (BSL 1.1 today, converting to Apache 2.0 on a fixed schedule), so you can read it, fork it, deploy it unmanaged, or repackage it under your own namespace. No black box and no lock-in: if you ever move off the managed package, the framework stays in your org as your code, not a vendor's.

What you get on day one

One install. The whole stack.

Nothing to assemble. Every layer ships in the one package, each one ready to switch on when you reach for it.

The same task, with and without KernDX

You write the line you expect. You get the depth you didn't.

Plain Apex on the left, KernDX on the right. Under each KernDX snippet is a list of what you also got, without typing it: the security, logging, performance, and correlation the framework added for free. Every item links to the guide section that proves it.

Paginate past 2,000 rows, and know when the data shifts under youSelectors Guide → Pagination

Plain Apex Manual

apex
// Page 82 of 25-row pages = OFFSET 2025 → governor blow-up
Integer offset = (pageNumber - 1) * pageSize; // over the 2,000 cap
List<Account> rows = [
	SELECT Id, Name FROM Account WHERE Type = 'Customer'
	ORDER BY Name ASC LIMIT :pageSize OFFSET :offset // ☠
];
Integer total = [SELECT COUNT() FROM Account]; // 2nd query
// FLS by hand. Rows deleted under you? You'll never know.

KernDX Included

apex
kern.QRY_Builder.QueryPage page = kern.QRY_Builder
	.selectFrom(Account.SObjectType)
	.condition(Account.Type).equals('Customer')
	.orderBy(Account.Name).ascending()
	.getPage(82, 25); // no OFFSET wall, USER_MODE FLS on

page.records; // this page
page.totalRecords; // count came back — no 2nd query
page.totalPages; // computed for you
page.hasMorePages; // wire to a "Next" button
page.deletedRecords; // rows the cursor saw deleted
What's actually happening

Salesforce exposes query cursors, but you normally wire one by hand. getPage() runs a pagination cursor for you, folds the row count into that same call instead of a second query, and reports what the cursor saw deleted mid-fetch. All of it runs in the running user's CRUD, FLS and sharing mode by default.

Create an Account, Opportunity, and Contact in a single atomic saveDML Fast Start → Parent-Child Insert

Plain Apex Manual

apex
Account newAccount = new Account(Name = name);
insert newAccount; // committed immediately
Opportunity newOpportunity = new Opportunity(
	Name = dealName, StageName = 'Prospecting',
	CloseDate = Date.today().addDays(30),
	AccountId = newAccount.Id); // manual FK stitch
Contact newContact = new Contact(
	LastName = lastName, AccountId = newAccount.Id);
insert newOpportunity;
insert newContact; // if this throws and the service catches it,
// the Account + Opportunity stay committed — orphaned. Safe means a
// Savepoint + try/catch + Database.rollback(savepoint). Every time.

KernDX Included

apex
kern.DML_Builder.newTransaction()
	.doInsert(newAccount)
	.doInsert(newOpportunity, Opportunity.AccountId, newAccount)
	.doInsert(newContact, Contact.AccountId, newAccount)
	.execute(); // one savepoint, all-or-nothing.
// FKs auto-wired after each parent inserts; any failure rolls back
// the whole graph. USER_MODE FLS by default.
What's actually happening

Each foreign key is set from the new parent's Id the moment that parent inserts, and all three rows commit on a single savepoint. So if any insert fails, the whole graph rolls back and a half-saved parent never leaks.

A resilient callout and its test, in one chainResilience Fast Start → Resilient Callout

Plain Apex Manual

apex
// Hand-rolled resilient POST — and you still own the test.
HttpRequest request = new HttpRequest();
request.setEndpoint('callout:PaymentGateway/charges');
request.setMethod('POST');
request.setBody(JSON.serialize(chargeRequest));
Set<Integer> retryable = new Set<Integer>{ 500, 502, 503, 504 };
HttpResponse response;
Integer attempts = 0;
do
{
	attempts++;
	response = new Http().send(request);
}
while(retryable.contains(response.getStatusCode()) && attempts < 4);
// no circuit breaker · no masking before you log the body · no
// dead-letter — and to test it you hand-write an HttpCalloutMock
// class and wire Test.setMock yourself

KernDX Included

apex
HttpResponse charge = kern.UTIL_HttpClient
	.post('PaymentGateway', '/charges')
	.body(chargeRequest)
	.withRetry(5, 10)
	.withCircuitBreaker()
	.onFailure(kern.UTIL_HttpClient.FailureAction.RETRY_THEN_LOG)
	.send(); // a transient 5xx comes back ON the response — never thrown

// …and the whole mock + assertion in the test, one chain:
kern.API_MockFactory.forService('PaymentGateway')
	.body('{"error":"unavailable"}').statusCode(503).register();
What's actually happening

The non-obvious part: a transient 5xx comes back on the response rather than thrown, and once in-transaction retries are spent the call is persisted so a scheduled Flow can re-drive it at a backoff date. The resilience outlives the original transaction. After repeated failures the framework stops calling a failing dependency for a cool-off, then resumes (a circuit breaker), and that state lives in Platform Cache keyed by the credential, so a dependency that's down stays shorted across transactions, not just within one.

Log an error that survives the rollback that erased the recordLogging Fast Start →

Plain Apex Manual

apex
try
{
	chargeCard(payment);
}
catch(Exception error)
{
	// Write a trace so we know what failed...
	insert new Error_Log__c(
		Message__c = error.getMessage(),
		Stack__c = error.getStackTraceString(),
		Record__c = payment.Id);
	throw error; // ...then the rollback ERASES that Error_Log__c too.
}

KernDX Included

apex
try
{
	chargeCard(payment);
}
catch(Exception error)
{
	kern.LOG_Builder.build()
		.error(error)
		.forRecord(payment.Id)
		.emitAt('PaymentService.charge');
	throw error; // the log is a platform event — it OUTLIVES the rollback.
}
What's actually happening

The log is published as a platform event committed to the event bus immediately, independent of your transaction, so the rollback that erases the payment can't un-publish the record of why it failed. A plain insert Error_Log__c shares the failing transaction and is undone by the very rollback the re-throw triggers.

Show 5 more examplesShow fewer examples
Reject a replayed request with a changed body: HTTP 409, automaticallyInbound APIs Fast Start → Idempotency

Plain Apex Manual

apex
// Hand-rolled inbound endpoint — you own the dedupe, or you don't have it.
@RestResource(urlMapping='/charge/*')
global with sharing class ChargeApi
{
	@HttpPost
	global static void doPost()
	{
		String key = RestContext.request.headers.get('Idempotency-Key');
		String body = RestContext.request.requestBody.toString();

		// Naive dedupe: did we see this key already?
		List<Payment__c> prior =
		[
			SELECT Id
			FROM Payment__c
			WHERE IdempotencyKey__c = :key
			LIMIT 1
		];
		if(!prior.isEmpty())
		{
			RestContext.response.statusCode = 200;   // assume same request...
			return;                                  // ...never checked the BODY.
		}

		chargeCard(body);                            // a retry with a CHANGED body
		insert new Payment__c(IdempotencyKey__c = key);  // double-charges silently.
	}
}

KernDX Included

apex
// 1. A normal inbound handler — no dedupe code at all.
@RestResource(urlMapping='/charge/*')
global inherited sharing class REST_Charge
{
	@HttpPost
	global static void doPost()
	{
		kern.API_Dispatcher
			.processInboundService(API_Charge.class.getName());
	}
}

global inherited sharing class API_Charge extends kern.API_Inbound
{
	public override void processRequest()
	{
		chargeCard(requestBody);
	}
}

// 2. Flip ONE field on this service's kern__ApiSetting__mdt record:
//      kern__IdempotencyEnabled__c = true   (kern__Direction__c = Inbound)
//
// Now, for every caller that sends an Idempotency-Key header:
//   • same key + same body    → cached HTTP 200 (handler never re-runs)
//   • same key + CHANGED body → HTTP 409, names the original ApiCall__c.Id
//   • new key                 → fresh processing
// All three outcomes, zero dedupe code. INBOUND, body-hash based.
What's actually happening

A naive hand-rolled dedupe checks only that the key was seen before, never that the body still matches, so a retry that mutates the payload under the same key double-charges in silence. The framework instead stores a SHA-256 hash of the request body next to the (External-ID-indexed) Idempotency-Key, so a replay with a changed body is detectable: it returns HTTP 409 naming the original ApiCall__c.Id, while an identical replay returns the cached 200 without re-running your handler. This is inbound body-hash idempotency. Outbound instead uses an explicit idempotency key you stamp via UTIL_HttpClient.withIdempotencyKey().

A governor-limit crash won't leave your chain stuck "Running"Async Processing Fast Start →

Plain Apex Manual

apex
// Hand-rolled Queueable chain
public class Step1 implements Queueable
{
	public void execute(QueueableContext context)
	{
		doWork(); // a CPU/heap limit here and the job just dies
		System.enqueueJob(new Step2()); // never reached — no error row, no status
	}
}
// AsyncApexJob says "Failed", detail-free. Your tracking record sits
// in "Running" forever. You find out from an angry user, not a query.

KernDX Included

apex
kern.UTIL_AsyncChain.newChain('OrderSync')
	.then(new Step1())
	.then(new Step2())
	.onError(new NotifyAdminStep())
	.execute();

// Each step runs in its own transaction (fresh limits).
// A Finalizer is attached to EVERY step, so even an
// uncatchable governor-limit crash marks the run Failed —
// with a reason + correlation id + a durable log. Never a zombie.
What's actually happening

A hand-rolled Queueable that hits an uncatchable governor limit just dies: no finish() hook, no error row, status stuck on "Running." The framework attaches a Finalizer to every step, and a Finalizer is guaranteed to run with fresh limits even after that crash. So the run is marked Failed with a reason and a correlated log instead of vanishing.

Scrub card numbers from your logs without shredding your order IDsData Masking Guide → Shipped Rules

Plain Apex Manual

apex
// Hand-rolled redaction in the log message: any long digit run goes.
String body = '{"card":"4111 1111 1111 1111","orderId":"1234567890123456"}';

String safe = body.replaceAll('\\b(?:\\d[ -]?){13,19}\\b', '[REDACTED]');
// Now BOTH are gone:
//   {"card":"[REDACTED]","orderId":"[REDACTED]"}
// The order ID was never a card — but the regex can't tell, so support
// loses the one ID they needed. Loosen it to spare the order ID and a
// real card slips through. There is no win.
insert new Error_Log__c(Message__c = safe);

KernDX Included

apex
// Just log it. The framework redacts the payment card on the way out.
String body = '{"card":"4111 1111 1111 1111","orderId":"1234567890123456"}';

kern.LOG_Builder.build()
	.info(body)
	.at('PaymentService.charge')
	.emit();

// The persisted LogEntry message reads:
//   {"card":"[CARD_REDACTED]","orderId":"1234567890123456"}
// 4111 1111 1111 1111 passes the Luhn check, so it is redacted.
// The 16-digit order ID FAILS Luhn, so it survives untouched.
What's actually happening

A blunt regex can't tell a card from any other 16-digit number: it either shreds the order ID alongside the card or, loosened to spare the order ID, lets a real card through. The shipped rule runs in CreditCard mode and checks each candidate match against a Luhn (mod-10) checksum first, so the valid card (4111 1111 1111 1111) is redacted while the order ID, which fails Luhn, passes through untouched. The masking runs on the framework's own LogEntryEvent before it is published, with nothing to configure.

Roll out a validation rule to production that logs violations but never blocks a saveCustom Validations Fast Start → Shadow Mode

Plain Apex Manual

apex
// Native Salesforce validation rule. It has exactly two states:
// off, or blocking EVERY save the instant you mark it Active —
// including the historical, dirty data you haven't cleaned yet.
//
//   Rule:    Customer_Requires_Contact
//   Formula: AND(ISPICKVAL(Type, "Customer"), ISBLANK(Phone))
//   Active:  [x]   <-- day one, prod saves start failing
//
// There is no "log it, don't block it" switch. To preview the
// blast radius you hand-roll a Flow that writes to a custom log
// object, or you flip Active on and brace for the support tickets.
update accounts;   // some now throw FIELD_CUSTOM_VALIDATION_EXCEPTION

KernDX Included

apex
// 1. AUTHOR two CMDT records, no Apex: a ValidationRuleGroup__mdt that
//    binds the object + timing (Account · Before · Insert/Update), then a
//    ValidationRule__mdt under it. RuleFormula__c returns TRUE when INVALID.
//
//   customMetadata/kern__ValidationRule.Customer_Requires_Contact.md-meta.xml
//   <values><field>kern__ValidationRuleGroup__c</field>
//     <value xsi:type="xsd:string">Account_Before_Save</value></values>
//   <values><field>kern__RuleFormula__c</field>
//     <value xsi:type="xsd:string">AND(ISPICKVAL(newRecord.Type, "Customer"), ISBLANK(newRecord.Phone))</value></values>
//   <values><field>kern__ErrorMessage__c</field>
//     <value xsi:type="xsd:string">Customer accounts need a phone</value></values>
//   <values><field>kern__Severity__c</field><value xsi:type="xsd:string">Error</value></values>
//   <values><field>kern__Order__c</field><value xsi:type="xsd:double">1</value></values>

// 2. Turn on Shadow Mode — ONE field. The live rule still fires on
//    every in-scope save and evaluates the SAME formula, but each
//    would-be violation is logged instead of calling addError().
//    The save commits — even with Severity = Error.
//
//   <values><field>kern__ShadowMode__c</field>
//     <value xsi:type="xsd:boolean">true</value></values>

// 3. Watch the blast radius accumulate, then flip ShadowMode__c to
//    false to enforce. Violations land in LogEntry__c tagged [SHADOW]:
List<LogEntry__c> shadowViolations = kern.QRY_Builder.selectFrom(LogEntry__c.SObjectType)
	.fields(new List<SObjectField>{ LogEntry__c.ShortMessage__c, LogEntry__c.CreatedDate })
	.condition(LogEntry__c.LogLevel__c).equals('WARN')
	.andCondition(LogEntry__c.ShortMessage__c).contains('[SHADOW]')
	.orderBy(LogEntry__c.CreatedDate).descending()
	.withLimit(100)
	.toList();
// Each ShortMessage__c reads: "[SHADOW] Customer_Requires_Contact:
// Customer accounts need a phone". Zero blocked saves.
What's actually happening

A native validation rule has two states only: off, or blocking every offending save the instant it is Active. So a new rule meets your dirty production data as a wall of FIELD_CUSTOM_VALIDATION_EXCEPTION errors. With ShadowMode__c = true the rule still fires on every in-scope save and evaluates the very same deployed formula, but each would-be violation is written to LogEntry__c as a [SHADOW] WARN instead of calling addError(), so nothing is blocked. You measure the blast radius from the log, then flip one checkbox to enforce.

One trigger. Metadata-ordered actions. No hand-rolled recursion guards.Triggers Guide →

Plain Apex Manual

apex
// Plain Apex: one fat trigger, inline logic, hand-rolled recursion guard.
trigger AccountTrigger on Account(before update)
{
	if(AccountTriggerHandler.alreadyRan) // static re-entry guard
	{
		return;
	}
	AccountTriggerHandler.alreadyRan = true;

	for(Account a : Trigger.new)
	{
		Account prior = Trigger.oldMap.get(a.Id);
		// rating + naming + territory logic all crammed here, in an
		// order nobody can change without editing + redeploying Apex.
		if(a.AnnualRevenue != prior.AnnualRevenue)
		{
			a.Rating = a.AnnualRevenue > 1000000 ? 'Hot' : 'Warm';
		}
		// ...next dev appends here; ordering is "whoever edited last"...
	}
}
// Disable in an incident: comment it out + deploy. Reorder: edit + deploy.
// Unit-test one rule alone: you can't — it's welded to the loop + guard.

KernDX Included

apex
// 1. The trigger is one line — forever.
trigger AccountTrigger on Account(before update)
{
	new kern.TRG_Dispatcher().run();
}

// 2. Each rule is its own class, testable in isolation, ordered by metadata.
public inherited sharing class TRG_SetAccountRating
	extends kern.TRG_Base implements kern.IF_Trigger.BeforeUpdate
{
	public void beforeUpdate(List<SObject> newRecords, List<SObject> oldRecords)
	{
		for(Account a : (List<Account>) newRecords)
		{
			Account prior = (Account) triggerOldMap.get(a.Id); // TRG_Base helper
			if(a.AnnualRevenue != prior.AnnualRevenue)
			{
				a.Rating = a.AnnualRevenue > 1000000 ? 'Hot' : 'Warm';
			}
		}
	}
}

// 3. Order, kill switch, and recursion are CONFIG, not code — TriggerAction__mdt:
//    ApexClassName__c   = TRG_SetAccountRating
//    Event__c           = Before Update
//    Order__c           = 20        // reorder without a deploy
//    BypassExecution__c = false     // flip to true to kill it mid-incident
//    AllowRecursion__c  = false     // shield re-entry (defaults to true)
What's actually happening

A one-line trigger hands off to TRG_Dispatcher().run(), which queries the TriggerAction__mdt rows for that object and event, sorts them by Order__c, and dispatches each to a small single-purpose class through the matching IF_Trigger interface. Because order, bypass, recursion, and flag-gating are rows rather than Apex, they change with no redeploy. And because each action just takes a record list, you unit-test one in isolation.

Built for AI-assisted development

Your AI assistant gets the standards before it writes.

KernDX ships its AI context as plain files in the public repo, with nothing to install, so Claude, Cursor, Copilot, or Gemini generate Apex and LWC that follow the framework (the naming, the secure defaults, the audited-bypass rule) instead of inventing their own.

KernDX is a library of Salesforce frameworks for Apex and LWC, with public source. It is one managed package (namespace kern, API 67.0). It covers trigger handling, secure queries and saves, structured logging, background-job orchestration, inbound and outbound REST, no-code feature flags and validation, and write-time data masking, plus the CI tooling (PMD rulesets, an ESLint plugin, secret scanning, and coverage gates).

What ships, and how to wire it up

  1. AGENTS.md sits at the repo root: the tool-neutral on-ramp that Claude Code, Cursor, Codex, and Cline read first. It orients your assistant and points it at the conventions and the full reference.
  2. AI Agent Instructions is the complete code-generation reference. Copy it into the rules file your tool auto-loads (AGENTS.md, CLAUDE.md, or .cursorrules), or reference it so a git pull keeps it current.
  3. From then on your assistant writes framework-correct Apex and LWC: the right namespace prefix, FLS-enforced reads and writes, the naming rules, and the audited-bypass pattern, instead of reinventing them.

AGENTS.md · /llms.txt · Code Conventions Guide · PMD + ESLint rules

Those same standards ship as PMD rulesets and an ESLint plugin, so what the assistant follows is also enforced where you already work: inline in VS Code or IntelliJ / Illuminated Cloud, and gated on every pull request in CI.

Open the AI Agent Instructions →

Ideas worth taking

Worth reading even if you never adopt KernDX.

These are design decisions, not feature names, so take the ideas with you. Each links to the guide that explains the thinking. The source is public (BSL 1.1, becoming Apache 2.0).

How a request flows

Many ways in. One secure core. Optional layers.

An LWC call, a DML save, a trigger, a REST endpoint, a batch job: any of them starts the same way. The core transaction is always there; the rest snap in only when your code uses them, and one correlation id (the KERN-ID) ties whichever layers ran into a single trace.

Any entry point
LWC / AuraApex serviceTriggerREST endpointBatch / Scheduled
KERN-2F3A·9C threads everything below
Always runsThe KernDX core transaction
🔒 Security & FLSQuery · QRY_Builder / SEL_BaseDML · DML_Builder📦 Atomic save🧵 Correlation · KERN-ID
only the layers your code actually uses
Optional snap-ins
⚙️ Async · UTIL_AsyncChain🌐 Outbound REST · UTIL_HttpClient📝 Telemetry · LOG_Builder

Adopt one layer today (just the query library, say) and snap in the trigger framework or the async chain the day you need it. The KERN-ID makes whatever ran traceable as one request.

Already using fflib, Trigger Actions, or rolling your own?

What's different by default.

  • Security is enforced on reads and writes, with an audit entry written every time it's deliberately bypassed.
  • One integrated package: triggers, queries, transactions, logging, REST, async, and masking share one model, instead of you wiring separate libraries together.
  • 100% Apex coverage and PMD-clean, enforced on every build, not aspirational.
Full capability-by-capability comparison →

Honest about scope: a logging-only library can go deeper on logging, and a mocking library on mocking. The guide says so, capability by capability.

Adopt at your own pace

Not all-or-nothing.

No rip-and-replace, and nothing you take all at once, which is what makes it low-risk to bring into an existing program. A typical path in: the query library first, then secure DML, then the trigger framework, adding async, masking, and the AI standards whenever you reach for them. Each layer stands alone, so adoption stays incremental and reversible.

A fit if…

  • more than one developer works in the org
  • AI is writing more of your Apex
  • consistency is getting expensive
  • onboarding a contributor takes too long
  • you need security you can show a reviewer

Maybe more than you need if…

  • a throwaway or greenfield org with no Apex and no near-term plans
  • you already run a framework you're happy with

Can't install a managed package, or would rather not? Because the source is available, you can deploy KernDX straight into your org as unmanaged code (no namespace, no Dev Hub), or repackage it under your own namespace. There's a way in for every org.

Install KernDX v1.6