Admission Decision Wizard Architecture
Overview
We see both sides of the coin here, and we want to be upfront, LWR is effectively off the table because it just can't embed cleanly, the only way it gets onto the page at all is through an iframe, and that drags in a whole pile of problems (cross-domain auth, blocked sessions, styling seams), so it's a poor fit for the inline, flashy survey we're after.
That leaves Lightning Out 2.0 and React + API as the two real options, and we've weighed them honestly.
Our call is React + API, because it's the faster path for our team to build, ship, and support.
We cannot cleanly add the Experience Cloud (LWR) site onto the page.
It only embeds through an iframe, and between the cross-domain auth and the styling seams, that's just a poor fit for an inline, flashy intake, so the real choice narrows down to two, Lightning Out 2.0 or React + API.
You want a polished, interactive intake on the external website.
Both Lightning Out 2.0 (now GA) and React with an API layer get you there, and the one hard constraint is that the intake has to live on the site, so we embed it right there in the page rather than bouncing people off to Salesforce (that's really the whole point, we keep them on our turf the entire time).
Our recommendation is React + API.
We're more comfortable building it and running it, we think we can hit the timeline better with it, and we can support it better over the long term, and either way the question logic stays in Salesforce as the single source of truth, so this isn't a one-way door at all (any future piece can ship as an LWC through Lightning Out, or as a React component, whenever we decide we want it).
The logic decision that makes all of this work.
Hardcode the question display logic in the React front end and keep Salesforce as the system of record for the answers and for any server-side validation, so the rules live in one version-controlled place no matter which front end we land on.
One hundred people filling out the form at once is manageable.
It just takes a lean save and a bit of background processing, and it's really a backend concern, so it doesn't hang on the front-end choice at all.
Context (what we know about the org)
Admission_Decision__ccarries 592 fields today, and about 434 of those are questions people actually fill in, while the other ~150 are the derived and automation layer (127 formulas, 16 roll-up summaries, 9 lookups, 1 auto-number), plus system control flags likeRun Contact Metrics RollupandTrigger Rollup Recalculation.- the object is closing in on the 800 custom-field cap, and right now every single new question we add means a brand-new field.
- Salesforce is doing real work on this object, not just storing data, there's a Rollup Helper trigger (
RHX_Admission_Decision) and a set of record-triggered flows (Subflow_AD_Seat_Capacity_Rollup,Certification_to_AD_Rollup_Subflow,Platform_Event_Reset_Rollup_Trigger_Fields,Resume_Associate_Admission_Decision). - the org already runs live Experience Cloud sites (
Career Accelerator,Career Portal,JRA File Upload,The Hub), so the platform, the licensing, the guest-user setup, and the ops are all already in place, the only catch is that they run on Aura, not LWR (we checked on 2026-06-16, and all 8 sites areExperienceBundle/Aura, there are zero LWR/EnhancedDigitalExperienceBundlesites). - the team is already LWC-first, there are 67 custom LWC bundles against only 11 Aura bundles (one of which is a
flowIframecomponent, so iframe embedding is already in use here), so the LWC _component_ skills are strong even though no LWR _site_ exists yet. - OmniStudio is not installed, and it isn't free for this org (no Industry Cloud), it'd be a custom-quoted add-on, so we park it unless Screen Flows can't deliver the UX.
1. Can an LWR be embedded on the site?
Short answer: yes, you can embed the LWR Experience Cloud site in an <iframe> (with the framing and cross-domain auth caveats we get into below), and that same embed can carry the conditional question logic and write the answers right back to Salesforce.
The one idea to hold onto here is that "LWR" is just the rendering surface, it is not where the validation logic lives, the validation logic that protects data integrity belongs server-side in Salesforce (Apex, flows, validation rules) where it can't be bypassed, and LWR simply renders the form and writes the answers back.
What an "LWR embed" actually is
LWR (Lightning Web Runtime) is a Salesforce-hosted site runtime, it is not a widget you paste into a page, so to get it onto an external site at all you have to embed the LWR Experience Cloud site in an <iframe>, and that means configuring Clickjack Protection / CSP / X-Frame-Options to allow framing from the parent domain, because Salesforce sends X-Frame-Options: SAMEORIGIN by default, so out of the box it just refuses. Source: 1.
Why LWR is the lower-friction embed here
You already run Experience Cloud, so the platform, the licenses, and the guest users are all in place, and the components call Apex in-platform, so there's no API layer to build, secure, version, and maintain (that API layer is really the hidden cost of the React path).
What the LWR embed costs you
- iframe friction, the clickjacking exposure, the cross-domain auth/cookies/SSO mess, and the styling seams right where our host site meets the framed form.
- Deploy by republish, because LWR serves statically and ships fewer out-of-the-box components than Aura does. Source: 2.
- less pixel-perfect control than a bespoke React/CMS front end would give us.
Why the iframe path struggles with cross-domain auth
Cross-domain authentication is really the reason the raw-iframe path only works for public, guest content, because when the framed form lives on a Salesforce domain and the host page lives on ours, the browser treats the form as third-party, and three things go wrong all at once:
- Third-party cookies are getting blocked, Safari blocks them outright (Intelligent Tracking Prevention) and Chrome keeps tightening the screws, so a Salesforce session cookie set on the SF domain doesn't reliably travel with the requests the iframe makes under our domain, which means a logged-in session just doesn't "carry over" the way people expect it to.
- Login pages refuse to be framed, Salesforce blocks its sensitive pages (the login screen included) from rendering inside a frame, so the login redirect inside the iframe tends to fail even after you've allowlisted the parent domain.
frame-ancestors/X-Frame-Optionsonly fixes the framing, not the session, allowlisting the parent domain lets the page render, but it does nothing at all for the cookie problem above. Source: 1.
The moment a logged-in Salesforce session is required, the raw-iframe path just breaks, and the other two paths both sidestep this (Lightning Out 2.0 runs inside Salesforce's trust boundary with a postMessage bridge, and React + API authenticates server-side through an API layer, so the browser only ever talks to our own origin and no third-party cookies are ever in play), and Section 2 compares them head to head.
Can the embed also carry the question logic?
"Logic" actually means three different things here, and the embed handles all three of them:
- Conditional display (State = NC shows the NC courses, a trades program shows the lift/ladder questions), and LWC inside LWR does this natively in the UI, it just reads the condition off the
Questionconfig and shows or hides. - Business rules and validation (eligibility, required answers, which records actually get created), and because LWR runs inside Salesforce's trust boundary and calls Apex directly, there's no separate API layer to build or secure, and the validation rules, flows, and existing AD automation all still fire on write, so the system of record stays authoritative.
- Writing the answers back, the form writes
Answer/response records (or fields) through Apex, and the existing triggers, flows, and rollups onAdmission_Decision__call keep running just like they do today.
The one thing to get right
LWR can render the form and even apply display rules, but it should never _own_ the validation logic, because that has to stay server-side in Salesforce where it can't be bypassed, the validation rules, flows, and Apex fire on write no matter what the front end does, so the system of record stays authoritative. Display logic (the "show when" conditions) is the front end's job; the validation that protects data integrity is Salesforce's. Source: 3.
2. Embedded + interactive + flashy: is it possible?
Yes, a polished, interactive, flashy intake absolutely can live on the external site, and in every path that actually works the question logic still lives as config in Salesforce (Question records, or custom metadata), so a flashy front end and one single source of truth are not at odds, you really can have both.
Here's what changed the picture for us, Lightning Out 2.0 went GA in Winter '26, and it's Salesforce's supported way to embed Salesforce-connected LWCs inside an external React/Vue/Angular/plain-JS site, it's built on LWR, it isolates the components in iframes and shadow DOM, and it bridges to the host page with postMessage. Sources: 4, 5.
The three paths that work (all keep logic centralized in SF)
In every option here, all the question logic stays in Salesforce, the only thing that actually differs is how the experience looks and feels to the person filling it out.
| Approach | How it fits into your site | How polished it can look | Who maintains the front end |
|---|---|---|---|
| Lightning Out 2.0 (Salesforce components dropped into our site) | Sits inside our site, near-native | Good, but limited by what Salesforce allows | Salesforce hosts the components |
| Full React + API layer | Fully part of our site, seamless | Best in class. Complete control over the look and feel | Our team owns it end to end |
| iframe an LWR site | A Salesforce page shown within ours | Functional, but can feel like a window into another system | Salesforce hosts it |
The raw iframe is really just the fallback for "flashy and seamless," it needs postMessage for auto-height, it's weak on mobile, and adblockers can hide iframe content entirely.
Front-end decision: Lightning Out 2.0 vs. full React, kept neutral
Full React + API layer
The case for it is genuinely strong, you get maximum brand and UX control and the most flash of any option here, it reuses the existing CMS/React team and the skills they already have, and it's fully decoupled from the Salesforce release cadence, so you deploy on your own pipeline whenever you want to.
The case against it is the weight you can't see at first, a Backend-for-Frontend becomes non-negotiable (the Matchfire team ends up owning the caching, the state, the retries, the auth, the security, the scaling, and the whole deploy lifecycle, all of it net-new to maintain), and on top of that a public app writing through the API spends guest/integration-user API calls against org limits and adds a fresh attack surface that has to be secured.
Lightning Out 2.0
The case for it is that it's GA and Salesforce-supported right now (not a beta gamble), it works inside React/Vue/Angular/JS so it drops straight into the existing CMS shell instead of replacing it, its components run inside Salesforce's trust boundary so identity, sharing, and FLS all come for free and they call Apex directly (which means no separate API layer to build, secure, and version), it's the lowest net-new front end to own (a strong fit for a lean nonprofit run by admins and consultants), and honestly the flashy ceiling is higher than people assume, these are custom LWCs with full CSS and JS, not stock components, and for a self-contained intake widget the iframe and shadow-DOM isolation is rarely the thing actually holding you back.
The case against it is the plumbing, the per-component iframe isolation plus the postMessage bridging adds some wiring, coordinated animation that spans the whole host page is more constrained than it'd be in pure React, and it's newer (Winter '26 GA), so there are simply fewer community patterns out there than there are for React.
Ramp reality (checked 2026-06-16): the existing sites are all Aura, not LWR, so the "Experience Cloud already exists" advantage is only partial, and a full LWR site would genuinely be new ground for us, but Lightning Out 2.0 is really just "build LWCs and embed," and the team already has 67 custom LWC bundles, so the skill it leans on is already in-house, and if you line the three up by how big a leap each one is, Lightning Out 2.0 is the smallest (it reuses LWC), a full LWR site is a bigger one (a whole new template), and full React reuses the web team's skills but leaves the 67 LWCs sitting on the table.
3. Concurrency & the buffer question (100 concurrent users)
The worry: roughly 100 candidates filling out and submitting the intake all at the same moment.
The reassuring part: 100 at once is genuinely _small_ for Salesforce at the web tier, the platform is built for 6, so serving pages and reading the cached question config out to 100 people is a complete non-event.
The actual risk is the write path, the real limit is 10 concurrent synchronous Apex requests that run longer than 5 seconds per org, and it scales with licenses (roughly 100 licenses to 1, with a minimum of 10 and a maximum of 50), and the good news buried in there is that requests under 5 seconds don't count and async jobs, Bulk API, and SOQL-only calls don't count either. Source: 7. Each submission writes to Admission_Decision__c, which fires synchronous automation on save (the real-time RHX_Admission_Decision Rollup Helper trigger plus the record-triggered flows), so if that save chain runs longer than 5 seconds under load, a tight burst of submissions can absolutely trip the limit.
Two ways to absorb the burst (both work, and the front end can sit in front of either)
| Option | Where the buffer lives | Pros | Cons |
|---|---|---|---|
| External DB buffer (the "save to a DB, then write to SF" idea) | Your middle layer | Best resilience; form survives SF maintenance windows; fully insulates from SF limits | Dual-write problem: two sources of truth, eventual consistency, reconciliation work, and pressure to duplicate SF validation in the middle layer |
| Salesforce-native buffer (Platform Events) | Inside Salesforce | One source of truth; nothing extra to host/maintain; validation stays centralized | Smooths the burst rather than fully insulating; tied to SF availability |
A caution on dual-write: "save to a DB and then write to Salesforce" really means writing the same fact into two systems with no guarantee that both actually succeed, and the user can get a cheerful "success" message before SF validation has even run, so if SF later rejects the record you're left with an orphan sitting in the buffer, and doing this safely takes a real outbox pattern with retries and idempotent upserts via External IDs, not the "light" layer it sounds like at first. Sources: 8, 9.
Plain-language version: what is a Platform Event?
Picture the intake form as a busy coffee shop.
Without a buffer: every customer stands at the counter while the barista makes their drink _and_ restocks the shelves _and_ updates inventory before serving the next person. When 100 people show up at once, the line jams and some get turned away. That is the longer-than-5-second synchronous save hitting the "only 10 slow things at a time" limit.
With a Platform Event: the customer drops their order ticket on a rail and walks away happy, fast. Behind the scenes, staff pick tickets off the rail and make the drinks at their own pace. If 100 people show up at once, the tickets just stack up on the rail and get worked through a batch at a time. Nobody gets turned away, and the counter never jams.
In Salesforce terms: the form does a quick, lightweight save and publishes an event (drops the ticket). Separate background processes subscribe to those events and do the slow work, rollups, validation, and related records, on their own schedule. The candidate already has their confirmation. The "rail" is the buffer, and it lives inside Salesforce, so there is no second database to keep in sync.
Why it matters here: it absorbs the 100-at-once burst without hitting the slow-request limit, _and_ it keeps everything in one system, so no dual-write. This org already works this way, with
Platform_Event_Reset_Rollup_Trigger_Fieldsand theTrigger Rollup Recalculationflags.
The pattern we recommend, whatever the front end
Lean synchronous save (just insert the raw answers, under 5 seconds, none of the heavy automation) → publish a Platform Event → process the rollups and validation async, so each save stays fast and never counts against the concurrency limit at all.
Finding: the AD save path is synchronous-heavy (checked 2026-06-16)
Everything below fires inside the save transaction on every single Admission_Decision__c insert or update:
| Layer | Count | Notes |
|---|---|---|
| Active validation rules | 23 | synchronous, before commit |
| Record-triggered flows | 22 | 2 before-save, 20 after-save |
| Apex triggers | 2 | AdmissionDecisionTrigger + RHX_Admission_Decision |
| Workflow rules | 1 |
AdmissionDecisionTrigger→AdmissionDecisionHandler(12,825 chars, 6 SOQL), and it does already make 2enqueueJob(Queueable) calls, so some of the work is async already, which means the pattern already exists in-house and we're not inventing it.RHX_Admission_Decision(Rollup Helper real-time): the config isrh2__Asynchronous__c = falseand realtime is not disabled, so the rollups run synchronously, inline, on every single save.- the expensive after-save flows include
Create_Portal_UserandDeactivate_Portal_User(synchronous Experience Cloud user creation),OIC_Automated_Email,Pre_Work_Notification, and a handful of cross-object rollup, eligibility, and field-update flows.
Verdict: under a real burst it's entirely realistic to cross the 5-second threshold and start eating into the 10-concurrent-long-running-Apex slots, so the lean-save refactor is warranted, and the nice part is that most of the fixes are genuinely low-effort.
Cheapest wins, in order:
- flip Rollup Helper
rh2__Asynchronous__c → trueforadmission_decision__c(a one-field change, about as cheap as it gets). - move the heavy after-save flows (the portal user, the emails, the cross-object rollups) onto scheduled/async paths, or tuck them behind a Platform Event.
- audit the 23 validation rules for expensive cross-object formula lookups, keep them synchronous, but keep them lean.
- for the new web intake, write through a lean save (the raw answers plus minimal automation) and drain the heavy work async.
4. Where the question logic should live
This is the section that delivers on the "all logic in one place" goal, and it shapes the front-end choice in #2, because whatever owns the logic is what each front end has to read and apply.
Where does the logic live? (three viable splits)
| Approach | Logic lives in | Pros | Cons |
|---|---|---|---|
| Logic as data (config records in Salesforce) | Records | Admins edit in prod, no deploy; easy UI | Not version-controlled; changes aren't deployable artifacts |
Logic as Custom Metadata (__mdt config) | Metadata | Version-controlled, deploys through envs, testable | Admin edits = metadata changes (slower for non-devs) |
| Logic hardcoded (React front end) — recommended | Front-end code | Fully unit-testable, type-safe, code-reviewed; deterministic with no runtime config lookups; lives in the same codebase, language, and skills the team already owns | Every question, wording, or rule change is a dev ticket plus a deploy; the question set becomes a large code surface; the front end, not Salesforce, becomes the system of record for the logic, and a Lightning Out path would have to re-implement the rules |
Our pick is the hardcoded path, in the React front end. The problem today is not that logic lives in code, it is that the logic is _scattered_, spread across the 127 formulas, 22 flows, 23 validation rules, UI code, and the fields themselves, each owned by a different team. Consolidating all of that into one version-controlled rules layer inside the React app (a single module the components call, not logic sprinkled through the UI) puts it in the codebase, language, and skill set the team already owns, and gives us the thing the data and metadata options cannot match: every rule is unit-tested, type-checked, and code-reviewed before it ships. Salesforce keeps the answers and any server-side guardrails, while the _question and rule definitions_ live in the React code. Two consequences come with this and we are taking them on purpose: the front end, not Salesforce, becomes the system of record for the question logic, and a future Lightning Out path would have to re-implement those rules rather than inherit them, so this commits to the React + API direction rather than hedging toward Lightning Out. The cost we accept is that adding or changing a question is a developer task and a deploy instead of an admin edit, which we are comfortable with because admissions rules are high-stakes, change in reviewable batches, and benefit more from test coverage than from in-prod editing. The data and Custom Metadata options stay in the table for comparison, but for the reasons in the note below we do not recommend putting this logic in Salesforce at all.
Why this logic does not belong in Salesforce
The first thing to be clear about is that what we are hardcoding is display logic, not validation logic. Display logic only decides which questions and which options a person sees, so it only has to run where the form renders, which is React, and there is no reason for it to flow into Salesforce at all.
Validation logic is the opposite case, the rules that protect data integrity genuinely belong server-side, and that is exactly where Apex or a flow would earn its place, so if we later need real validation we still put that in Salesforce. But a rule like _"show the NC course question when State = NC"_ is purely visual, and pushing it into Salesforce buys us nothing.
It would also cost us a great deal. Encoding this display logic as Custom Metadata, or as config records in Salesforce, gets complex quickly, and it would take a very thorough and error-prone process to be confident every rule was captured. The deeper problem is multi-step, compound logic. The real rules look like _"the answers to questions 1, 2, and 4 together determine which values are available for question 6,"_ and conditions of the shape _"if this and this and this, or this, then that."_ Custom Metadata is built for the simple case, _"if you answered question 2, show question 4,"_ a single if-this-then-that mapping, and it does not express compound, cross-question conditions cleanly. Forcing that kind of branching into metadata rows is where it becomes genuinely burdensome.
So our recommendation is firm: keep the logic out of Salesforce entirely. It is the combination of the two points, the logic is display rather than validation so it never needs to be server-side, and the multi-step conditional logic would be painful to model in Custom Metadata, that makes React the right home for it.
Our Belief
Having actually dug into the org, here's our read:
We cannot cleanly embed the LWR/LWC site into the marketing site.
An LWR site only embeds through an <iframe>, and the cross-domain auth and the styling seams (Section 1) make that a poor fit for the inline, flashy intake we want, so "just drop the Experience Cloud site onto the page" is simply off the table.
That narrows the real choice to two: Lightning Out 2.0 or React + API.
Both of them keep Salesforce as the system of record for the answers and any server-side validation, they really only differ in where the front end runs and who ends up maintaining it.
Our recommendation is React + API.
We're more comfortable building it and running it, we believe we can hit the timeline better with it (it reuses the existing web and React skills and it ships on our own pipeline, decoupled from the Salesforce release cadence), and we can support it better over the long term.
The logic is a deliberate React commitment.
The display logic lives in the React app, so this is a deliberate commitment to React rather than a hedge, and a future Lightning Out path would mean porting those rules, not inheriting them. We are making that trade on purpose, because React is the front end we are most confident building, running, and testing. Salesforce stays the system of record for the answers and for any server-side validation, and the low-cost concurrency moves (make the intake save lean, push the rollups async) pay off no matter which front end you end up choosing.
Scorecard: your calls
These ratings are yours. Set Importance, Urgency, and Effort in each numbered section (#1–#5); this view updates live. Everything is saved only in your browser, nothing leaves your machine.
| Section | Importance | Urgency | Effort | Priority |
|---|
Set some scores to generate a suggested order.
#2 front-end decision: Not set
Sources
1. Can an LWR be embedded on the site?
- Embed Experience Cloud in iframe · https://crsinfosolutions.com/embed-experience-cloud-iframe/
- Salesforce Developers, LWR Sites · https://developer.salesforce.com/docs/atlas.en-us.exp_cloud_lwr.meta/exp_cloud_lwr/intro.htm
- Salesforce Ben, Single Source of Truth · https://www.salesforceben.com/mastering-single-source-of-truth-with-crm-cdps-and-your-tech-stack/
2. Embedded + interactive + flashy: is it possible?
- Lightning Out 2.0 GA · https://developer.salesforce.com/blogs/2025/10/lightning-out-2-0-is-now-generally-available-in-winter-26
- LWC Dev Guide, Lightning Out 2.0 · https://developer.salesforce.com/docs/platform/lwc/guide/lightning-out-intro.html
3. Concurrency & the buffer question
- hundreds of thousands to millions of concurrent users · https://www.salesforce.com/blog/scale-salesforce-concurrent-users/
- Salesforce, Execution Governors and Limits · https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm
- the dual-write problem · https://authzed.com/blog/the-dual-write-problem
- SF integration patterns / External IDs · https://architect.salesforce.com/docs/architect/fundamentals/guide/data360_integration_patterns_and_practices
- Salesforce, Event-Driven Architecture guide · https://architect.salesforce.com/docs/architect/decision-guides/guide/event-driven.html
- scaling platform event subscribers · https://www.salesforceben.com/how-to-scale-apex-platform-event-triggers-with-parallel-subscribers-in-salesforce/