Skip to Content
ExamplesBuild a Complete Member Dashboard
On this page

Build a Complete Member Dashboard

This example builds a member dashboard with a static frontend and a Site Members Function. Every member receives safe startup information; only Owners and Admins can request the private report records.

Create the resources

  1. Publish a Site named my-site or another available name. Choose Invite-only or Private Site Access and invite a Viewer for testing.
  2. Create a Table in the API tab named Reports with a Text field title. Add a sample record such as “September overview”.
  3. Configure the Table as Developer API and create a server key with read permission. Keep the Table’s reads private to that credential.
  4. Create a Site Members Function linked to the Site. Use my-app-api or another available slug; update the frontend URL if you choose a different slug.
  5. Add Function Secrets TABLE_API_URL (the Table’s copied base URL) and TABLE_API_KEY (the private server key).
  6. Paste the Function code below, enable GET, Test /bootstrap, and Publish.

Function: authorize before reading private data

export default { async fetch(request, env) { const user = env.MINIUP_USER; if (!user) return Response.json({ error: "Sign in required" }, { status: 401 }); const url = new URL(request.url); if (request.method !== "GET") { return Response.json({ error: "Method not allowed" }, { status: 405 }); } const canReadReports = user.role === "owner" || user.role === "admin"; if (url.pathname.endsWith("/bootstrap")) { return Response.json({ user: { name: user.name || "Member", role: user.role }, features: { dashboard: true, reports: canReadReports } }); } if (url.pathname.endsWith("/reports")) { if (!canReadReports) { return Response.json({ error: "Forbidden" }, { status: 403 }); } if (!env.TABLE_API_URL || !env.TABLE_API_KEY) { return Response.json({ error: "Reports not configured" }, { status: 503 }); } try { const source = new URL(env.TABLE_API_URL); source.searchParams.set("limit", "25"); const response = await fetch(source, { headers: { "x-miniup-api-key": env.TABLE_API_KEY } }); if (!response.ok) throw new Error("Report source failed"); const data = await response.json(); if (!Array.isArray(data.records)) throw new Error("Invalid report data"); return Response.json({ reports: data.records.map(record => ({ id: record.id, title: String(record.fields.title || "Untitled report") })) }); } catch { return Response.json({ error: "Reports unavailable" }, { status: 502 }); } } return Response.json({ error: "Not found" }, { status: 404 }); } };

The report route checks the trusted role on every request and returns only id and title. It does not pass the member’s browser-supplied URL or role to the private data source.

Frontend: index.html

Upload these three frontend files to the Site. The main page uses a responsive layout, accessible status, a retry action, and a component loaded after bootstrap.

<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Member dashboard</title> <style> body { font: 17px/1.6 system-ui; margin: auto; padding: 24px; max-width: 56rem; color: #14283e; } button { font: inherit; min-height: 44px; padding: 8px 16px; cursor: pointer; } button:focus-visible { outline: 3px solid #2875bc; outline-offset: 3px; } #dashboard { padding: 20px; border: 1px solid #ccd9e5; border-radius: 12px; } li { overflow-wrap: anywhere; } </style> </head> <body> <main> <h1>Member dashboard</h1> <p id="status" role="status" aria-live="polite">Loading your app…</p> <button id="retry" type="button" hidden>Retry</button> <section id="dashboard" aria-label="Your dashboard" hidden></section> </main> <script type="module" src="./app.js"></script> </body> </html>

Frontend: app.js

const status = document.querySelector("#status"); const retry = document.querySelector("#retry"); const dashboard = document.querySelector("#dashboard"); const base = "/api/functions/my-app-api"; async function load() { retry.hidden = true; dashboard.hidden = true; status.textContent = "Loading your app…"; try { const response = await fetch(`${base}/bootstrap`); if (!response.ok) throw new Error(`Unable to load your app (${response.status}). Check Site membership.`); const app = await response.json(); if (!app.features.dashboard) throw new Error("Dashboard unavailable."); const component = await import("./components/dashboard.js"); await component.render(app, base, dashboard); dashboard.hidden = false; status.textContent = ""; } catch (error) { status.textContent = error.message; retry.hidden = false; } } retry.addEventListener("click", load); load();

Frontend: components/dashboard.js

export async function render(app, base, container) { container.replaceChildren(); const greeting = document.createElement("h2"); greeting.textContent = `Welcome, ${app.user.name}`; container.append(greeting); if (!app.features.reports) { const note = document.createElement("p"); note.textContent = "Your membership does not include report access."; container.append(note); return; } const response = await fetch(`${base}/reports`); if (!response.ok) throw new Error(`Reports could not load (${response.status}).`); const { reports } = await response.json(); if (!reports.length) { const empty = document.createElement("p"); empty.textContent = "No reports yet."; container.append(empty); return; } const list = document.createElement("ul"); for (const report of reports) { const item = document.createElement("li"); item.textContent = report.title; list.append(item); } container.append(list); }

Validate the complete app

  1. As Owner, open the published Site. Expect the greeting and up to 25 real report records.
  2. With an empty Reports Table, expect “No reports yet.”
  3. As Viewer, expect the greeting and the no-report-access message.
  4. As Viewer, call /api/functions/my-app-api/reports directly. Expect 403 even if someone modifies frontend flags.
  5. While signed out, verify protected Site access and denied Function access.
  6. Temporarily use invalid test credentials in the Function preview to check the safe 502 error. Restore valid Secrets and publish before sharing.

This example intentionally provides a bounded read-only report list. Add pagination and record-specific rules before extending it to a larger business workflow. Do not assume a role check alone gives per-record isolation.

complete example · authenticated app · bootstrap · MINIUP_USER · role · private Table · secrets · dashboard