| name | gate-the-dashboard |
| description | Put the analytics dashboard and embedded BI behind login: app-shell login gate + session check + the handoff to data-platform's embed-JWT and RLS for per-user data isolation. The clearest expression of the auth-identity → data-platform seam. |
Skill: gate-the-dashboard
Invoked by: any agent building or securing an analytics dashboard or embedded BI feature; ravenclaude-core/security-reviewer for any embed-auth or dashboard-access change.
When to invoke: adding authentication to the analytics dashboard or any embedded BI component; designing the access-control handoff between the identity layer and the data isolation layer; auditing an existing dashboard that may be accessible without authentication.
Output: dashboard login gate implemented + server-side session check + embed JWT issuance endpoint created + data-platform RLS seam documented + cross-boundary isolation verified.
Two-layer contract (this skill's core job)
[User] -- Google SSO --> [auth-identity: authenticated session + auth.uid()]
|
Dashboard access gate
(this skill: is user logged in? do they have dashboard role?)
|
[App shell renders dashboard page]
|
[Server issues short-lived embed JWT]
(tenant_id from authenticated session — NOT from URL)
|
[data-platform: embed verifies JWT, RLS scopes rows]
(rls-policy-authoring + jwt-embed-issuance skills)
Layer 1 (this skill): prove who the person is and whether they can see the dashboard at all.
Layer 2 (data-platform): prove which data rows that person is allowed to see.
Do not duplicate the Layer 2 mechanics here. Reference them. They are owned by:
data-platform/skills/jwt-embed-issuance/SKILL.md — short-lived embed JWT with tenant_id
data-platform/skills/rls-policy-authoring/SKILL.md — Postgres RLS scoping
data-platform/best-practices/issue-short-lived-jwts-for-embeds.md — embed JWT absolute rule
data-platform/best-practices/embed-never-ship-the-service-key.md — secret handling rule
Step 1 — Protect the dashboard route
The dashboard page must check authentication server-side before rendering any content or loading any embed:
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const cookieStore = cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { getAll: () => cookieStore.getAll(), setAll: () => {} } },
);
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect("/login?next=/dashboard");
const role = user.app_metadata?.role ?? "viewer";
if (!["viewer", "editor", "admin"].(role)) {
();
}
;
}
If using Next.js middleware (recommended), the middleware already redirects unauthenticated users before the page renders. The Server Component check is defense-in-depth.
Step 2 — Issue the embed JWT server-side
The dashboard embed requires a short-lived JWT with the user's tenant_id to scope data. This token is issued by a server-side API route after authentication is verified. The signing secret never leaves the server.
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import { SignJWT } from "jose";
export async function POST(request: Request) {
const cookieStore = cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { getAll: () => cookieStore.getAll(), setAll: () => {} } },
);
const { data: { user }, error } = await supabase.auth.getUser();
if (!user || error) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
{ : membership } = supabase
.()
.()
.(, user.)
.();
(!membership?.) {
.({ : }, { : });
}
secret = ().(process..!);
token = ({
: user.,
: membership.,
: ,
: ,
})
.({ : })
.()
.()
.(secret);
.({ token });
}
Mandatory: route this code to ravenclaude-core/security-reviewer before production. The data-platform/skills/jwt-embed-issuance/SKILL.md owns the full claim-shape specification (required: sub, tenant_id, iat, exp, iss, aud; recommended: nonce, allowed_dashboards).
Step 3 — Client fetches the embed token and passes it to the embed
"use client";
import { useEffect, useState } from "react";
export function DashboardEmbed() {
const [embedToken, setEmbedToken] = useState<string | null>(null);
useEffect(() => {
fetch("/api/embed-token", { method: "POST" })
.then((r) => r.json())
.then(({ token }) => setEmbedToken(token));
}, []);
if (!embedToken) return <div>Loading dashboard…</div>;
return <YourEmbedComponent apiToken={embedToken} />;
}
Step 4 — Data isolation (data-platform's responsibility)
Once the embed token is in the embed component, data isolation is enforced by the data-platform plugin:
- The embed tool (Cube, Superset, Metabase) verifies the JWT signature.
- It extracts
tenant_id from the verified JWT.
- Postgres RLS policies or Cube
securityContext rules scope all queries to that tenant_id.
The auth-identity plugin's job ends at Step 3. Do not implement RLS policies or Cube securityContext rules here. Refer to:
data-platform/skills/rls-policy-authoring/SKILL.md
data-platform/skills/jwt-embed-issuance/SKILL.md
data-platform/skills/cube-schema-scaffolding/SKILL.md
The generated dashboard.html (marketplace analytics dashboard)
For the RavenClaude marketplace's own analytics dashboard (dashboard.html), the same two-layer pattern applies:
- The dashboard shell is only rendered after a successful Supabase Auth session check.
- Any BI embed within
dashboard.html receives a short-lived server-issued JWT scoped to the authenticated user's context.
- The embed's data queries are gated by Postgres RLS — the user only sees their own data.
Anti-patterns this skill flags
- Dashboard page renders without server-side authentication check (relying only on client-side redirect)
- Embed JWT issued without verifying the user's session first
tenant_id in the embed JWT derived from the URL or request body (not from the authenticated session)
- Signing secret for embed JWT in a
NEXT_PUBLIC_ env var or in the client bundle
- Embed JWT with
exp > 30 minutes (absolute rule — see data-platform embed JWT best-practice)
- Skipping the
data-platform RLS layer and relying on app-code row filtering as the data isolation control
- Dashboard accessible without authentication in any environment (including staging and preview)
- No cross-boundary denial test — required before shipping (see data-platform's
rls-cross-tenant-test.sql template)
Verification checklist
See also