Salesforce contains an enormous amount of useful operational data. It does not follow that every useful way to view that data must live inside Salesforce.

Maybe your team wants a focused pipeline screen, an underwriting workspace, a morning operating dashboard, or a small internal tool that combines Salesforce records with another system. An external dashboard can provide that experience without turning into a second CRM—if you preserve one crucial boundary:

Salesforce remains the system of record, and the dashboard only reads it.

This article walks through a user-authenticated architecture using a Salesforce External Client App, OAuth Authorization Code with PKCE, a backend-for-frontend, and read-only API operations. The result is an external dashboard that respects each user’s Salesforce permissions instead of sharing one all-powerful integration account.

Visual placeholder: Opening GIF. User signs in with Salesforce, lands on a polished pipeline dashboard, changes a filter, and clicks “Open in Salesforce” on one record. Use a developer org or fully synthetic data.

The architecture

The dashboard has four main pieces:

  1. A browser frontend displays charts, tables, and filters.
  2. Your backend owns the OAuth flow and calls Salesforce APIs.
  3. A Salesforce External Client App defines the OAuth trust relationship.
  4. Salesforce authorizes the signed-in user and returns only data that identity may access.

The browser should not become a permanent token wallet. A practical design stores Salesforce tokens in a server-side session store and gives the browser only an opaque, secure session cookie.

The browser asks your backend for /api/dashboard. The backend finds the user’s Salesforce token, runs bounded queries, shapes the results into dashboard-friendly JSON, and returns only the fields the UI needs.

Architecture diagram. Browser → application backend → Salesforce REST API. Draw the OAuth redirect through Salesforce login and show tokens stored only on the server.

Why use the signed-in user’s identity?

An external dashboard could authenticate to Salesforce using one shared service account. That is simple at first and expensive later.

With a shared identity, every dashboard user potentially sees whatever the integration account can see. Your application must recreate record-level authorization, field visibility, business-unit boundaries, and offboarding logic. It is easy to accidentally build a second, worse version of Salesforce security.

With per-user OAuth, Salesforce evaluates queries as the signed-in user. Existing profiles, permission sets, sharing rules, and record access continue to matter. When someone loses access in Salesforce, the dashboard naturally loses the same access.

This does not eliminate application authorization. Your app may still restrict who can use the dashboard at all. It does avoid giving the app broader Salesforce visibility than the user it serves.

Step 1: Create the External Client App

In Salesforce Setup, open External Client App Manager and create a local External Client App for the dashboard.

Enable OAuth and register the backend’s exact callback URL, for example:

https://dashboard.example.com/api/auth/salesforce/callback

For local development, add a separate HTTPS or loopback callback appropriate to your framework and Salesforce configuration. Keep development and production values explicit; do not rely on replacing random pieces of a URL at runtime.

Select the minimum scopes required. A typical user-delegated API dashboard may need:

  • Identity information
  • API access
  • Refresh-token access if server sessions should survive access-token expiry

Enable Authorization Code and require PKCE. Do not enable client credentials, username-password, or other flows merely because they are available.

After creating the app, configure its policy. Decide whether users can self-authorize or must be pre-approved. For an internal enterprise dashboard, pre-authorization through appropriate profiles or permission sets is often easier to govern.

Visual placeholder: Salesforce setup screenshot sequence. Show callback URL, selected scopes, PKCE requirement, and permitted-user policy. Consumer credentials must be blurred.

Step 2: Start the OAuth flow safely

When a user clicks Sign in with Salesforce, the backend should create two random values:

  • A state value to bind the callback to the browser session and prevent login CSRF
  • A PKCE verifier, from which it derives the code challenge sent to Salesforce

Store the state and verifier server-side or in a protected, short-lived mechanism tied to the login attempt. Then redirect the browser to the Salesforce authorization endpoint with the client ID, callback URL, requested scopes, state, and PKCE challenge.

After the user signs in, Salesforce redirects to your callback with an authorization code and the original state. Validate the state before exchanging anything. Then send the code and PKCE verifier to the token endpoint.

If successful, Salesforce returns an access token and related identity details. Store the token in a server-side session keyed by an opaque cookie. Set the cookie Secure, HttpOnly, and with an intentional SameSite policy.

The frontend never needs to read the Salesforce access token.

Visual placeholder: Animated OAuth sequence. Highlight the random state traveling out and back; keep the PKCE verifier inside the backend until token exchange.

Step 3: Define a read-only Salesforce boundary

“This dashboard is read-only” should be a property of the implementation, not a sentence in a prompt or README.

Start by exposing only query-oriented backend services. Do not build a generic Salesforce request proxy that accepts arbitrary methods and paths from the browser. A route like /api/salesforce?path=... eventually becomes an authorization incident with excellent flexibility.

Instead, create functions with narrow intent:

  • get_pipeline_summary(user_session, filters)
  • get_recent_opportunities(user_session, limit)
  • get_account_detail(user_session, account_id)
  • search_accounts(user_session, query, limit)

Keep SOQL in server-owned code. Parameterize or strictly validate filter values. Allowlist sortable fields and filter operators. Cap every result set and use pagination for detail tables.

At the HTTP layer, expose only GET or semantically read-only POST endpoints when a complex filter body is necessary. Do not include Salesforce create, update, upsert, delete, or composite-write operations in the application client.

For defense in depth, use Salesforce permissions that prevent the relevant users or integration context from writing the objects in scope where that matches the business requirement. Code restrictions are useful; platform permissions are harder to bypass accidentally.

Visual placeholder: “Read-only by construction” diagram showing four locks: no write routes, query-only service, SOQL allowlists, and Salesforce permissions.

Step 4: Shape data for the dashboard

Do not make the frontend understand raw Salesforce records if you can avoid it.

Suppose the dashboard needs pipeline totals by stage, upcoming maturities, and a list of recently modified opportunities. The backend can run a few deliberate queries and return a view model such as:

{
  "asOf": "2026-07-22T14:30:00Z",
  "pipeline": {
    "totalAmount": 42500000,
    "dealCount": 18,
    "byStage": [
      { "stage": "Underwriting", "count": 7, "amount": 18200000 }
    ]
  },
  "recentDeals": []
}

This creates a stable contract between backend and frontend. If a Salesforce field is renamed, calculated differently, or replaced, one mapping layer changes instead of every component.

Return an asOf timestamp and make freshness visible. Users trust dashboards more when the interface admits when the data was retrieved.

Step 5: Avoid the N+1 query trap

A dashboard often begins with one Opportunity query. Then each row needs an Account. Each Account needs an owner. Each deal needs its latest activity. Suddenly loading 25 rows triggers 76 API calls.

Design the query shape before polishing the skeleton loader.

Use Salesforce relationship queries and aggregate queries where appropriate. Batch related identifiers and fetch them in sets. Cache stable reference data briefly when permitted, but keep the user identity in the cache key whenever visibility can differ by user.

Never share a cached response across users unless you have proven the underlying data and permissions are identical. “It was faster” is not an acceptable explanation for cross-user record leakage.

Visual placeholder: Before/after waterfall. Before shows dozens of Salesforce calls; after shows three bounded calls followed by one dashboard response.

Step 6: Add “Open in Salesforce” before adding edits

Users will eventually want to act on what they see. The safest first action is a deep link back to the corresponding Salesforce record.

Add an Open in Salesforce link using the record ID and the org’s approved domain. This keeps updates in the system that already owns validation rules, approval flows, audit history, and permissions.

That small feature often delivers most of the workflow value people actually need: discover and prioritize work in the dashboard, then perform the official update in Salesforce.

If you later add write actions, treat them as a separate product and security phase. Define each allowed mutation, add confirmation and idempotency behavior, record an audit trail, and review field-level authorization. Do not let “just add a status dropdown” quietly dissolve the read-only boundary.

Step 7: Handle token expiry and revocation honestly

Access tokens expire. Refresh tokens can be revoked. Salesforce sessions can be restricted by policy. Your dashboard should treat those events as normal operating conditions.

When an API call fails because authorization is no longer valid, clear the server session and return an explicit reauthentication response. The frontend should offer Reconnect Salesforce, not display “Something went wrong” forever while retrying a doomed token.

Do not wrap every Salesforce error in the same generic exception. Distinguish at least:

  • Reauthentication required
  • User lacks access to a record or field
  • Invalid filter or query
  • Salesforce rate or availability problem
  • Internal application error

Useful errors reduce support load and stop developers from weakening security simply to make a message disappear.

Step 8: Verify the boundary

Test with at least two Salesforce users who have meaningfully different access.

Verify that:

  • Both can sign in through the ECA.
  • Each sees only records available to that Salesforce identity.
  • Restricted fields do not appear in API responses or logs.
  • A user without ECA approval cannot authorize.
  • State mismatch is rejected.
  • Expired or revoked tokens produce a reconnect path.
  • Search and filter inputs cannot alter the intended SOQL structure.
  • No frontend or backend route can create, modify, or delete Salesforce data.
  • Deep links open the correct record in the correct org.
  • Signing out clears the application session.

Include negative tests in the demo. Showing that the dashboard refuses an unauthorized record is more persuasive than showing the same bar chart for the sixth time.

Visual placeholder: Verification montage: two users with different record counts, a blocked record, a clean reconnect screen, and a successful deep link to Salesforce.

The result

An external Salesforce dashboard does not need to become a shadow CRM. It can be a focused lens: authenticate each person, query within their existing permissions, shape a few useful views, and send them back to Salesforce for official changes.

The External Client App establishes trust. OAuth and PKCE protect the sign-in flow. Server-side token storage keeps credentials out of browser code. Narrow query services preserve the read-only boundary. And the user’s own Salesforce identity prevents your app from pretending it knows more about authorization than Salesforce does.

That architecture is not as flashy as a dashboard with fifty editable widgets. It is much easier to explain to a security reviewer—and, more importantly, much harder to regret.

References