From b8440e9c9e2cffe18f1c44bc741f550311d7bd73 Mon Sep 17 00:00:00 2001 From: Moheeb Zara Date: Sun, 17 May 2026 01:36:54 -0700 Subject: [PATCH] perf(ux): request-cached permission checks (dedupe RPC fan-out) New React cache()-wrapped hasPermission(uid,sid,perm): identical checks resolve once per server render, so the (app) layout's nav-permission resolution and a page's checks for the same perm (door.manage, forms.manage) no longer double-round-trip user_has_permission. Pure dedupe -- semantics unchanged, permission logic stays in SQL/RLS. Adopted in the layout + members page. --- app/(app)/layout.tsx | 7 +++---- app/(app)/members/page.tsx | 23 ++++++++--------------- lib/permissions-cache.ts | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 lib/permissions-cache.ts diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx index 6bbab55..7f9a9ac 100644 --- a/app/(app)/layout.tsx +++ b/app/(app)/layout.tsx @@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server' import { redirect } from 'next/navigation' import { AppSidebar } from '@/components/app-sidebar' import { CommandPalette } from '@/components/command-palette' +import { hasPermission } from '@/lib/permissions-cache' import { ConfirmProvider } from '@/components/ui/confirm' import { getRoleLabelMap } from '@/lib/role-labels' @@ -69,11 +70,9 @@ export default async function AppLayout({ children }: { children: React.ReactNod const navPerms: Record = {} if (!isAdminRole) { const results = await Promise.all( - NAV_PERM_CODES.map(perm => - supabase.rpc('user_has_permission', { uid: user.id, sid: member.space_id, perm }), - ), + NAV_PERM_CODES.map(perm => hasPermission(user.id, member.space_id, perm)), ) - NAV_PERM_CODES.forEach((perm, i) => { navPerms[perm] = !!results[i].data }) + NAV_PERM_CODES.forEach((perm, i) => { navPerms[perm] = results[i] }) } return ( diff --git a/app/(app)/members/page.tsx b/app/(app)/members/page.tsx index e70a695..1b9e489 100644 --- a/app/(app)/members/page.tsx +++ b/app/(app)/members/page.tsx @@ -1,5 +1,6 @@ import { createClient } from '@/lib/supabase/server' import { PageHeader, PageTitle } from '@/components/ui/page-title' +import { hasPermission } from '@/lib/permissions-cache' import { MembersClient } from './members-client' import { InvitesPanel } from '../customize/panels/invites-panel' @@ -90,21 +91,13 @@ export default async function MembersPage() { // certifications.grant (the Instructor capability) is independent of // admin/board, so check it explicitly to decide whether to show the // per-member certifications panel. - const { data: canGrantCerts } = await supabase.rpc('user_has_permission', { - uid: user.id, - sid: self.space_id, - perm: 'certifications.grant', - }) - const { data: canManageCards } = await supabase.rpc('user_has_permission', { - uid: user.id, - sid: self.space_id, - perm: 'door.manage', - }) - const { data: canViewForms } = await supabase.rpc('user_has_permission', { - uid: user.id, - sid: self.space_id, - perm: 'forms.manage', - }) + // Request-cached: door.manage / forms.manage here dedupe with the layout's + // nav-permission resolution for the same render. + const [canGrantCerts, canManageCards, canViewForms] = await Promise.all([ + hasPermission(user.id, self.space_id, 'certifications.grant'), + hasPermission(user.id, self.space_id, 'door.manage'), + hasPermission(user.id, self.space_id, 'forms.manage'), + ]) return ( => { + const supabase = await createClient() + const { data } = await supabase.rpc('user_has_permission', { uid, sid, perm }) + return !!data + }, +)