DocsGuides

Guides

Build personalized dashboards

2 min readReviewed July 2026

Personalized dashboards use one reusable chart definition and apply an authenticated user or account filter on the server.

The critical design rule is simple: the browser never receives your GraphJSON API key and never chooses its own unrestricted query.

1. Instrument account-aware events#

Log a durable authorization key with every relevant event:

{
  "event": "order_completed",
  "user_id": "usr_42",
  "account_id": "acct_7",
  "order_id": "ord_93",
  "amount": 4900,
  "currency": "usd"
}

Use the same account_id value your application uses to authorize the dashboard. Do not rely on display names or email addresses.

2. Design a base chart#

In the GraphJSON visualizer:

  1. select the collection
  2. build the metric without a user filter
  3. add a test account_id filter
  4. verify the result against your source system
  5. select Export → As embed API call

The exported payload is your base chart definition.

3. Generate the URL on your server#

The example below uses a framework-neutral handler shape:

export async function accountAnalyticsHandler(request, response) {
  const session = await requireSession(request);
  const accountId = session.accountId;

  // The server owns every query parameter except the derived account ID.
  const graphjsonResponse = await fetch(
    "https://api.graphjson.com/api/visualize/iframe",
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        api_key: process.env.GRAPHJSON_API_KEY,
        collection: "orders",
        IANA_time_zone: session.reportingTimeZone || "UTC",
        graph_type: "Single Value",
        start: "7 days ago",
        end: "now",
        filters: [
          ["event", "=", "order_completed"],
          ["account_id", "=", accountId],
          ["currency", "=", "usd"]
        ],
        metric: "amount",
        aggregation: "Sum",
        compare: "7 days ago",
        customizations: {
          title: "Revenue",
          value_prefix: "$"
        }
      })
    }
  );

  const body = await graphjsonResponse.json();

  if (!graphjsonResponse.ok) {
    return response.status(502).json({ error: "Analytics unavailable" });
  }

  return response.json({ url: body.url });
}

The application session—not a cookie value read without verification and not a request body field—determines accountId.

4. Render it#

function RevenueCard({ url }) {
  return (
    <iframe
      src={url}
      title="Revenue for the last seven days"
      width="100%"
      height="280"
      loading="lazy"
      style={{ border: 0 }}
    />
  );
}

Use a loading state and a clear fallback when your server or GraphJSON is unavailable. Analytics should not block the rest of the product.

5. Add more metrics#

Reuse the same authorization pattern for:

Metric Graph type Aggregation Metric field
Orders Single Value Count None
Revenue Single Value Sum amount
Average order value Single Value Avg amount
Orders over time Single Line Count None
Revenue by product Bar Chart Sum amount, split by product

Keep currency-separated metrics separate unless you convert values into a documented reporting currency before ingestion.

Prevent cross-tenant access#

The iframe URL is a capability. Send it only to the authenticated user for whom it was generated.

If you cache URLs:

  • include the authorized account ID in the cache key
  • use a tenant-scoped cache namespace
  • invalidate on authorization changes
  • never cache a personalized response in a public CDN

For even tighter control or a fully native UI, call the data API from your server and return only the fields the customer needs.

Need a hand?

Tell us what you’re building and we’ll point you in the right direction.

Contact support