Embedded Reporting Architecture: Patterns for Secure Integration

Learn the architectural patterns behind secure embedded reporting, including server-side APIs, iframe previews, token authentication, and multi-tenant isolation.

Published Mar 02, 2026

When you embed reporting inside a software product, you're not just adding a feature. You're connecting your application's identity and data model to an external system—and doing it in a way that feels seamless to the user. Done well, embedded reports behave as if they were built into your product. Done poorly, they expose credentials, break on browser updates, or require users to authenticate twice.

This article walks through the core architecture of embedded reporting: the server-side API boundary, client-side preview embedding, authentication design, and the role of official client libraries. The patterns here apply broadly; the second half maps them to a concrete implementation with CxReports.


What embedded reporting actually means

Embedded reporting means reports are generated and displayed inside your application, not in a separate tool the user has to navigate to. The user stays in your product. The report feels native.

Most embedded reporting systems have two modes of operation:

  • Server-side generation: your backend calls the reporting engine, retrieves a PDF or data file, and handles it—returning it to the user, storing it, or emailing it. The user never sees the reporting system directly.
  • Client-side preview: the reporting engine's UI is loaded inside your frontend, typically via an iframe. The user interacts with the report (paging, zooming) directly.

Both modes use the same reporting engine and the same report definitions. They differ in where the boundary sits between your application and the engine—and that difference drives the entire authentication design.


The architectural mental model

Think of your application and the reporting engine as two separate systems that need to cooperate around a trust boundary.

Your Application
  ├── Frontend (browser)
  │     └── loads iframes, makes API calls to your backend
  └── Backend (server)
        └── holds credentials, calls reporting engine API
              └── Reporting Engine
                    ├── REST API (server-to-server)
                    └── Preview UI (loaded in browser via iframe)

Three rules follow from this model:

  1. Long-lived credentials live on your server. Your backend has an API key or token that proves its identity to the reporting engine. This credential never touches the browser.

  2. The browser gets short-lived, scoped tokens. When you need the browser to authenticate with the reporting engine directly (for an iframe preview), your backend mints a temporary token and passes it to the frontend. The token is valid once, or for a short window, and scoped to a specific report or workspace.

  3. Authorization runs in your application, not the reporting engine. Before your backend generates any token or calls any API, it should have already verified that the current user has the right to access that report. The reporting engine enforces its own access controls, but your application layer is the first line.

This is the architecture that makes embedded reporting secure and maintainable. Everything else—API design, iframe setup, client libraries, cookie behavior—follows from it.


Server-side API access

Your backend communicates with the reporting engine over a REST API authenticated with a long-lived credential (typically a personal access token or API key stored in your secrets configuration).

The core operations your backend will use:

  • List available reports — to build a report browser or dynamically populate a UI.
  • Export a report to PDF — the primary workhorse; returns a binary you can store, email, or return directly to the client.
  • Push temporary data — for reports whose data is computed at request time (aggregated from multiple sources, not stored in a single database).
  • Create short-lived tokens — to authorize iframe previews for the frontend.

Every call goes over HTTPS with the credential in the Authorization: Bearer header. The credential never appears in URLs, query strings, or response bodies—only in the header, where it's encrypted by TLS and not logged by proxies.

// All server-side API calls follow this pattern
const response = await fetch(
  `${REPORTING_ENGINE_URL}/api/v1/ws/${workspaceId}/reports/${reportId}/pdf`,
  {
    headers: {
      Authorization: `Bearer ${process.env.REPORTING_API_TOKEN}`,
    },
  },
);
const pdfBuffer = await response.arrayBuffer();

For multi-tenant applications, your backend maps each authenticated user to their workspace before making API calls. Workspace scoping is the natural isolation boundary: reports, templates, and data sources within a workspace are accessible only through that workspace's API path.


Token-based authentication: the two-token model

Because embedded reporting spans both server and client contexts, a single credential type is not sufficient. You need two:

Long-lived API credentials (personal access tokens or equivalent) are used exclusively by your server. They're created once, stored securely in environment variables or a secrets manager, and rotated periodically. They grant broad access and should be treated like passwords. They must never appear in client-side code, HTML, browser storage, or logs.

Short-lived nonce tokens are created by your server and handed to the browser for a specific purpose: authenticating an iframe embed. They are single-use and expire quickly (typically minutes). On first use, the reporting engine converts the nonce token into a session cookie that handles subsequent requests within the iframe. If the nonce is intercepted before use, it buys an attacker nothing after its expiry.

The server-to-client handoff is the critical moment:

User requests page → backend generates nonce token → renders page with token embedded
→ browser loads iframe with nonce in URL → engine converts nonce to session cookie
→ all subsequent iframe requests use the cookie

The nonce token appears in the iframe URL as a query parameter because that's how the iframe's initial request is made—there's no other channel. This is a known pattern and acceptable because the nonce is single-use and short-lived. What must never appear in a URL is a long-lived API token.


Iframe-based preview embedding

An iframe loads the reporting engine's preview UI inside your page. The user can page through reports, zoom, and interact—all without leaving your application and without your frontend needing to handle PDF rendering.

The setup:

  1. Your backend generates a nonce token (using the long-lived API credential).
  2. Your backend passes the nonce to the frontend—via a page render, API response, or server component.
  3. The frontend embeds the iframe with the nonce as a query parameter.
<iframe
  src="https://reports.yourdomain.com/preview/{reportId}?nonce={nonceToken}"
  width="100%"
  height="700"
  frameborder="0"
  allowfullscreen
>
</iframe>

Browser cookie behavior is the main operational concern. Browsers increasingly restrict third-party cookies—cookies set by a domain different from the one in the address bar. If your application is app.yourdomain.com and your reporting engine is reporting.otherdomain.com, session cookies set by the engine will likely be blocked.

The standard solution: host the reporting engine on a subdomain of your own domain (reports.yourdomain.com). Cookies from a subdomain are treated as first-party by all major browsers. If you're using a managed SaaS reporting service, look for a custom domain option that allows this configuration.

Additional headers to set in your application:

Content-Security-Policy: frame-src 'self' https://reports.yourdomain.com;

And ensure CORS on the reporting engine allows your application's origin.


Client libraries: why they matter

You can call any REST API with a raw HTTP client. Official client libraries add three things worth having:

  1. Type safety. Request and response shapes are known at compile time. Mismatches fail at build, not in production.
  2. Error normalization. HTTP error codes, network errors, and application-level errors are surfaced consistently rather than requiring custom parsing.
  3. Lifecycle management. Connection pooling, timeouts, and retry logic are handled by the library, not implemented per-call by your team.

For embedded reporting specifically, the two operations you'll call most often—PDF export and nonce token creation—benefit from having a typed, dependency-injected client instance initialized once and reused across your application:

// Initialize once, at application startup
import { CxReportsClient } from "@cx-reports/api-client";

export const reportingClient = new CxReportsClient({
  baseUrl: process.env.CXREPORTS_URL,
  token: process.env.CXREPORTS_PAT,
  timeout: 60_000,
});
// Use anywhere in your backend
async function serveReport(
  workspaceId: string,
  reportId: string,
  res: Response,
) {
  try {
    const pdf = await reportingClient.reports.exportToPdf(
      workspaceId,
      reportId,
    );
    res.setHeader("Content-Type", "application/pdf");
    res.setHeader("Content-Disposition", `attachment; filename="report.pdf"`);
    res.send(pdf);
  } catch (err) {
    if (err.statusCode === 404) res.status(404).send("Report not found");
    else res.status(500).send("Report generation failed");
  }
}

The pattern is the same whether you're in a request handler, a background job, or a serverless function.


How this architecture maps to CxReports

CxReports implements the four-pillar model described above:

API access — A versioned REST API under /api/v1/ provides endpoints for listing reports, exporting PDFs, pushing temporary data, and managing workspaces and report types. All calls use Bearer token authentication.

API clients — Official clients are available for Node.js/TypeScript, C#/.NET, and Python. Each follows the same conceptual model: instantiate once with base URL and token, then call typed methods for reports and authentication. The TypeScript client (@cx-reports/api-client) is the most commonly used for web backend and serverless scenarios:

const nonce = await reportingClient.auth.createNonceToken({ workspaceId });

C# and Python clients follow equivalent patterns, named and structured idiomatically for their respective ecosystems. If you're in a stack not covered by an official client, the raw API is straightforward—the clients are convenience wrappers, not required.

Iframe preview — The nonce token workflow described above is CxReports' standard embedding path. Your server creates a nonce token via the API client, passes it to the frontend, and the iframe URL includes it as ?nonce={token}. CxReports converts it to a session cookie on first request. Host CxReports under your own subdomain (reports.yourdomain.com) to avoid third-party cookie issues.

Token-based authentication — CxReports uses personal access tokens (PATs) for server-side API access and nonce tokens for client-side iframe embedding. PATs are generated from the user settings panel, stored as secrets in your environment configuration, and never exposed beyond your server. Nonce tokens are generated on demand by your backend and valid for a single use.

Multi-tenant deployment — CxReports supports workspaces as the isolation boundary. Each tenant or customer can have a dedicated workspace; your backend maps the authenticated user's tenant ID to the appropriate workspace before making API calls.


Security practices

A few rules that apply regardless of which reporting engine you're using:

  • Store API credentials in a secrets manager or environment variables. Never commit them to source control.
  • Rotate long-lived tokens on a schedule (quarterly is a common baseline) and immediately on suspected compromise.
  • Generate nonce tokens on demand—never cache or reuse them.
  • Use HTTPS everywhere, including internal service communication. Mixed content (HTTPS host loading HTTP iframe) is blocked by browsers.
  • Validate authorization in your application before generating tokens. The reporting engine is not your only access control layer.
  • Keep CORS configuration specific: list exact allowed origins, never use wildcards in production.
  • Log token creation events but sanitize token values from logs.

Getting started with CxReports

The architecture described above maps directly to CxReports' developer primitives:

Concept CxReports primitive
Server-side API access REST API, PAT authentication
PDF export GET /api/v1/ws/{workspaceId}/reports/{id}/pdf
Temporary data push POST /api/v1/ws/{workspaceId}/temporary-data
Client library @cx-reports/api-client (Node), CxReports.ApiClient (.NET), cxreports-api-client (Python)
Iframe authentication Nonce token → session cookie
Tenant isolation Workspace per tenant

Documentation: docs.cx-reports.com
API reference: docs.cx-reports.com/getting-started/api
Demo application (Next.js): github.com/cx-reports/consumer-demo-app-nextjs
Developer resources: docs.cx-reports.com/getting-started/developer-resources

If you want to see the architecture in action with your own data, request a demo or reach out directly.

Modal Fallback