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.
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.
Plain Apex Manual
// 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
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 deletedWhat you also get
5lines→8capabilities you didn't write
Reliability
Cursor paginationClears the 2,000-row OFFSET ceiling that plain LIMIT/OFFSET hits: getPage() runs and wires a pagination cursor for you.Total count, no 2nd querypage.totalRecords folds into the same cursor call, with no separate COUNT() query.Deleted-row trackingpage.deletedRecords flags rows the cursor saw deleted mid-fetch, not silently dropped.Governance
USER_MODE by defaultRuns the running user's object and field permissions (CRUD and FLS) plus record sharing, with no hand-rolled checks.Audited bypasswithSystemMode() opt-outs are logged to an audit trail.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.
Plain Apex Manual
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
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 you also get
5lines→7capabilities you didn't write
Reliability
Atomic graphOne savepoint. Any insert failure rolls the whole graph back, so no orphaned parent leaks.Auto-wired foreign keysEach foreign key is set from the new parent's Id after it inserts, with no manual AccountId stitching.Dependency orderingParents insert before children, so the Account always saves before its Opportunity and Contact.DML-row-limit guardGuards the per-transaction DML-row limit before committing. It fails fast and points you to .async().Match-or-create on an external keydoUpsert(record, externalIdField) matches on a stable external ID, so a replayed integration write updates the existing record instead of duplicating it.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.
Plain Apex Manual
// 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 yourselfKernDX Included
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 you also get
9lines→9capabilities you didn't write
Reliability
Two-layer retryRetries transient 5xx responses {500, 502, 503, 504} immediately. You never typed the loop or the codes.Async re-driveStill failing? The call is persisted and a scheduled Flow re-drives it at a backoff date.Cross-transaction breakerCircuit-breaker state lives in Platform Cache, keyed by the credential, shared across transactions.Governance
Masked before saveCard and secret-key rules redact Request__c / Response__c before the ApiCall__c row is saved.Large-payload overflowA body too large for the field overflows to a ContentVersion file, masked the same way.Audit in SYSTEM_MODEThe whole audit write runs in SYSTEM_MODE, fixed up front and not overridable per-call.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.
Plain Apex Manual
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
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 you also get
4lines→8capabilities you didn't write
Reliability
Rollback-proofPublished as a platform event, so the saved log outlives the rollback that erases the payment.ERROR flushes earlyAn ERROR-level entry is never held in the suspended-save buffer; it flushes before the re-throw.Observability
Governor-limit snapshotSnapshots every limit (SOQL, DML, CPU, heap, callouts, each as used of maximum) onto the row.Correlation idOne startCorrelation() stamps a shared id on every log, so you query the whole flow by that one id.Correlation survives the async hopserializeContext() / hydrateContext() carry the correlation id across a Queueable, Batch, or Future, so one flow stays joinable.Full execution contextCaptures class + method, the context (trigger/REST/batch…), and the user who emitted the log.Exception detailRecords the exception type, the full stack trace, and the failing line number.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
Plain Apex Manual
// 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
// 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 you also get
1 handler + a checkbox→8capabilities you didn't write
Reliability
Body-hash dedupeSame key + changed body → HTTP 409 naming the original ApiCall__c.Id.Cached replaySame key + same body → cached HTTP 200, your handler never re-runs.One-checkbox SHA-256The dedupe is a SHA-256 hash of the body, turned on by one config checkbox.Completed-only matchReplay matches the indexed Idempotency-Key, only against requests that completed successfully.Inbound, not outboundThis is INBOUND body-hash idempotency. Outbound uses an explicit idempotency key you set, not a body hash.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().
Plain Apex Manual
// 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
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 you also get
5lines→10capabilities you didn't write
Reliability
Durable recoveryAn uncatchable governor-limit crash still flips the run to Failed, never a zombie stuck on Running.Fresh limits per stepEach step ran in its own Queueable transaction, with a fresh set of governor limits.Callout-capable onErrorAn onError handler runs in its own callout-capable transaction, even after the failed step did DML.Observability
Real-time Chain MonitorA live UI surfaces running and failed chains without writing a query.Queryable statusStatus persists to an AsyncChainExecution__c row at every transition (Running → Completed/Failed/Aborted).Shared correlation idLogs inside a step share the chain's id, so one filter traces the whole multi-transaction run.Field-history auditStatus__c, CompletedSteps__c, CurrentStepName__c, and CompletedAt__c carry field-history, giving a step-by-step trail.Durable crash logOn crash the Finalizer wrote a durable Error log stamped with the failed chain-execution id.Quiet when cleanLogs are reserved for actionable events, so a clean, fast, successful run emits no noise.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.
Plain Apex Manual
// 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
// 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 you also get
4lines→8capabilities you didn't write
Governance
Luhn-preciseLuhn-checked each match: the card redacted, the 16-digit order ID survived.On by defaultOn by default when you log. You configured nothing to get this.Pre-wired ruleThe replacement reads [CARD_REDACTED], and the rule ships pre-wired, with no Apex.Covers framework recordsThe same rule also masks the framework's outbound-API, API-issue, async-chain, and log records.Point at your own objectsAim the same engine at your object with config: a masking target plus the object's toggle, no Apex.Master kill switchA master kill switch disables all framework masking for diagnostics.15 dormant rule templatesSSN, JWT, AWS keys, IBAN, SWIFT, private IPs and more ship as proven patterns. Activate one by wiring a target and flipping its IsActive flag, with no regex to write.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.
Plain Apex Manual
// 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_EXCEPTIONKernDX Included
// 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 you also get
2 CMDT records + a checkbox→8capabilities you didn't write
Reliability
Shadow modeSave never blocked: even with Severity = Error, a shadow violation is logged, not raised.Same deployed formulaFires on every in-scope save against the SAME formula, and one checkbox flips shadow to enforce.Throw-safe evaluationA formula that throws still won't block the save. The error is logged and swallowed, not rethrown.Same rules, callable from FlowAn Execute Validation Rules invocable runs the very same formula rules from a Flow and returns errors/warnings without blocking the save.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.
Plain Apex Manual
// 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
// 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 you also get
a 1-line trigger→9capabilities you didn't write
Governance
Config, not codeActions dispatch in Order__c sequence, so you reorder them without a deploy.Per-action kill switchFlip BypassExecution__c to kill an action with no deploy.Object-level bypassDisable every trigger action for an object in one call with TRG_Base.bypass().Feature-flag gatingGate an action on a feature flag without touching code.Flow as a stepRegister a Flow as an ordered step in the same pipeline.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
- 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.
- 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 agit pullkeeps it current. - 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).
Why a ‘safe to retry’ key isn't enough
If a retry shows up carrying different data than the first try, KernDX rejects it instead of replaying the old result, so a changed request can't silently overwrite the original.
Read the decision →Why a logging tool must avoid logging about itself
When the thing recording events is itself event-driven, naively logging its own activity loops forever. KernDX detects that case and writes the record directly.
Read the decision →Why an empty filter should match nothing, not everything
An empty “only include these” list should return nothing; an empty “exclude these” list, everything. Getting that right stops a filter bug from quietly scanning your whole table.
Read the decision →Why hiding card numbers takes more than pattern-matching
Plenty of 16-digit numbers aren't credit cards. KernDX matches the shape loosely, then runs the card-number checksum, so real cards get hidden while order numbers and dates are left alone.
Read the decision →Why the framework looks for your version of a class first
It checks your project for a class before falling back to its own, so “write your own and it wins” becomes a built-in way to customise behaviour, with nothing to register.
Read the decision →Why feature flags decide like an access list
Each rule can answer yes, no, or “not my call”, and that third answer is what lets you stack “block these, then allow those” rules in order without them fighting.
Read the decision →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.
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.
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.
Get started
Three ways in.
kern at v1.6 to your org. Swap it in within days.Repackage under your own nameBuild KernDX into your own package, as your code.CI tooling onlyPMD rulesets + ESLint plugin that flag violations inline in VS Code or IntelliJ / Illuminated Cloud and gate them in CI, with no framework code.