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:
- select the collection
- build the metric without a user filter
- add a test
account_idfilter - verify the result against your source system
- 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.