[{"data":1,"prerenderedAt":458},["ShallowReactive",2],{"\u002Fblog\u002Fnuxt-rbac-guide-roles-protected-pages-admin-controls-data":3},{"post":4,"surround":447},{"id":5,"title":6,"alternates":7,"authors":8,"badge":14,"body":16,"date":405,"dateModified":406,"description":26,"extension":407,"head":406,"hero_image_url":25,"json_ld":408,"meta":430,"navigation":431,"ogImage":406,"outbound_links":432,"path":433,"primary_keyword":434,"related_articles":435,"robots":406,"schemaOrg":406,"search_intent":436,"seo":437,"sitemap":438,"stem":439,"supporting_keywords":440,"tags":445,"__hash__":446},"blog_en\u002Fblog\u002Fnuxt-rbac-guide-roles-protected-pages-admin-controls.md","Nuxt RBAC guide: roles, protected pages, and admin controls",[],[9],{"name":10,"to":11,"avatar":12},"Tom Han","https:\u002F\u002Fx.com\u002Ftomhan245",{"src":13},"https:\u002F\u002Fcdn.shipahe.ad\u002Ftomhan.webp",{"label":15},"Ultimate guide",{"type":17,"value":18,"toc":369},"minimark",[19,28,32,37,40,45,65,68,72,75,79,82,94,97,101,104,108,115,129,132,136,139,144,149,152,156,159,164,171,175,178,183,186,190,193,197,200,208,212,218,222,225,230,233,237,240,244,251,255,258,263,266,270,284,288,291,295,298,301,305,322,325],[20,21,22],"figure",{},[23,24],"img",{"src":25,"alt":26,"style":27},"https:\u002F\u002Fshipahe.ad\u002Fimages\u002Fblog\u002Fnuxt-rbac-guide-roles-protected-pages-admin-controls\u002Fpost-859.webp","Practical Nuxt RBAC: model roles and permissions, guard routes and APIs, add admin guardrails and audits, and handle billing, auth, and export edge cases.","max-width:100%;border-radius:12px",[29,30,31],"p",{},"You cannot fake access control. If your Nuxt app handles paid tiers, partner portals, or a back office, one missing permission check can expose data or hurt revenue. The goal is simple: one model for who can do what, enforced the same way on pages, APIs, and jobs. Here is a practical RBAC setup for Nuxt that holds up under growth, audits, and messy real-world behavior.",[33,34,36],"h2",{"id":35},"model-roles-and-permissions","Model roles and permissions",[29,38,39],{},"Good RBAC starts with a small role set and an explicit permission catalog. Keep it boring and readable. If a teammate cannot reason about it in a code review, it will drift.",[41,42,44],"h3",{"id":43},"choose-clear-role-names","Choose clear role names",[46,47,48,49,48,53,48,56,48,59,48,62],"ul",{},"\n  ",[50,51,52],"li",{},"Owner. Top-level authority within a workspace or account. Can change billing and assign roles.",[50,54,55],{},"Admin. Manages users and settings but cannot change ownership.",[50,57,58],{},"Member or Staff. Uses core features. Limited settings access.",[50,60,61],{},"Viewer or Read-only. Can view data but not update or export.",[50,63,64],{},"Guest. Minimal access for invitations or trials.",[29,66,67],{},"Keep role names neutral. Actions belong in permissions, not labels.",[41,69,71],{"id":70},"write-the-permission-catalog-once","Write the permission catalog once",[29,73,74],{},"List the verbs that matter to your product, then pair them with the nouns in your domain. Typical verbs: read, create, update, delete, export, bill, invite, manage_roles. Pair with resources like project, dataset, invoice, report, model, file. This table is your source of truth for enforcement and tests.",[41,76,78],{"id":77},"represent-it-in-code-and-the-database","Represent it in code and the database",[29,80,81],{},"Store the catalog in the database so you can adjust without redeploys. Mirror it in TypeScript for type safety. One workable approach with Prisma:",[83,84,85,89],"pre",{},[86,87,88],"code",{},"\u002F\u002F prisma\u002Fschema.prisma\nmodel Role { id String @id @default(cuid()) name String @unique description String? scope Scope userRoles UserRole[] rolePerms RolePermission[] }\nmodel Permission { id String @id @default(cuid()) action Action resource Resource rolePerms RolePermission[] }\nmodel RolePermission { roleId String permissionId String role Role @relation(fields: [roleId], references: [id]) permission Permission @relation(fields: [permissionId], references: [id]) @@id([roleId, permissionId]) }\nmodel UserRole { userId String roleId String scopeId String? \u002F\u002F workspace\u002Faccount id role Role @relation(fields: [roleId], references: [id]) @@id([userId, roleId, scopeId]) }\n",[29,90,91],{},[86,92,93],{},"enum Scope { GLOBAL WORKSPACE }\nenum Action { READ CREATE UPDATE DELETE EXPORT BILL INVITE MANAGE_ROLES }\nenum Resource { PROJECT DATASET INVOICE REPORT MODEL FILE USER SETTINGS }\n",[29,95,96],{},"Seed the base catalog once. Add a migration when you introduce a new feature that needs a permission. My rule of thumb: if you add an admin toggle for something, add a permission for it too.",[33,98,100],{"id":99},"enforce-rbac-in-nuxt","Enforce RBAC in Nuxt",[29,102,103],{},"RBAC is only as strong as the checks on pages, APIs, and components. Apply the same decision function everywhere.",[41,105,107],{"id":106},"a-small-shared-decision-helper","A small, shared decision helper",[29,109,110,111,114],{},"Put a single ",[86,112,113],{},"can()"," helper in a composable so templates, middleware, and server code call the same logic:",[83,116,117,120],{},[86,118,119],{},"\u002F\u002F composables\u002FuseRBAC.ts\nimport { useAuth } from '#imports'\nexport type Action = 'READ'|'CREATE'|'UPDATE'|'DELETE'|'EXPORT'|'BILL'|'INVITE'|'MANAGE_ROLES'\nexport type Resource = 'PROJECT'|'DATASET'|'INVOICE'|'REPORT'|'MODEL'|'FILE'|'USER'|'SETTINGS'\n",[29,121,122],{},[86,123,124,125,128],{},"export function useRBAC() {\nconst { user } = useAuth() \u002F\u002F user roles preloaded on login\nconst roles = computed(() => user.value?.roles || ",[126,127],"span",{},")\nfunction can(action: Action, resource: Resource, scopeId?: string) {\n\u002F\u002F Check bans\u002Fsuspensions first\nif (user.value?.status === 'BANNED') return false\nreturn roles.value.some(r => (\n(!scopeId || r.scopeId === scopeId || r.scope === 'GLOBAL') &&\nr.permissions.some(p => p.action === action && p.resource === resource)\n))\n}\nreturn { can }\n}\n",[29,130,131],{},"Cache role membership in memory per request on the server so repeated checks are cheap, but always verify on the server before reading or writing sensitive data.",[41,133,135],{"id":134},"route-meta-and-global-middleware","Route meta and global middleware",[29,137,138],{},"Declare required permissions on pages and enforce them in a single middleware:",[83,140,141],{},[86,142,143],{},"\u002F\u002F pages\u002Fprojects\u002F[id]\u002Fedit.vue\n\u003Cscript setup lang=\"ts\">\ndefinePageMeta({ requires: [{ action: 'UPDATE', resource: 'PROJECT' }] })\n\u003C\u002Fscript>\n",[83,145,146],{},[86,147,148],{},"\u002F\u002F middleware\u002Frbac.global.ts\nexport default defineNuxtRouteMiddleware(async (to) => {\n  const { status } = useAuth()\n  if (status.value !== 'authenticated') return navigateTo('\u002Flogin?next=' + to.fullPath)\n  const { can } = useRBAC()\n  const reqs = (to.meta.requires as any[]) || []\n  const scopeId = to.params.workspaceId as string | undefined\n  const allowed = reqs.every(req => can(req.action, req.resource, scopeId))\n  if (!allowed) return navigateTo('\u002Ferror\u002Fforbidden')\n})\n",[29,150,151],{},"On SSR pages, this avoids flashing restricted content. Keep the meta terse and the middleware boring.",[41,153,155],{"id":154},"server-validation-every-time","Server validation every time",[29,157,158],{},"Never rely on client checks. Re-evaluate permissions in every server handler that touches sensitive data:",[83,160,161],{},[86,162,163],{},"\u002F\u002F server\u002Fapi\u002Fprojects\u002F[id].put.ts\nexport default defineEventHandler(async (event) => {\n  const user = await requireUser(event) \u002F\u002F throws if not logged in\n  const body = await readBody(event)\n  const workspaceId = await getWorkspaceIdForProject(event.context.params!.id)\n  if (!canServer(user, 'UPDATE', 'PROJECT', workspaceId)) {\n    throw createError({ statusCode: 403, statusMessage: 'Forbidden' })\n  }\n  return updateProject(event.context.params!.id, body)\n})\n",[29,165,166,167,170],{},"For file storage, check permission before generating signed URLs, and scope keys by tenant, for example ",[86,168,169],{},"workspaces\u002F{id}\u002Ffiles\u002F{uuid}",".",[41,172,174],{"id":173},"component-guards-without-ui-leaks","Component guards without UI leaks",[29,176,177],{},"Hide or disable affordances that would lead to denied actions, and keep templates readable:",[83,179,180],{},[86,181,182],{},"\u003Ctemplate>\n  \u003CUButton :disabled=\"!can('EXPORT','REPORT', workspaceId)\" @click=\"exportReport\">Export CSV\u003C\u002FUButton>\n  \u003CRouterLink v-if=\"can('UPDATE','PROJECT', workspaceId)\" :to=\"editUrl\">Edit Project\u003C\u002FRouterLink>\n\u003C\u002Ftemplate>\n\u003Cscript setup lang=\"ts\">\nconst { can } = useRBAC()\nconst workspaceId = useWorkspaceId()\n\u003C\u002Fscript>\n",[29,184,185],{},"Fetch role membership early in your auth flow so first paint matches the final state.",[33,187,189],{"id":188},"admin-operations-and-auditability","Admin operations and auditability",[29,191,192],{},"Human oversight is part of access control. You need a safe way to assign and revoke roles, respond to abuse, and explain what happened later.",[41,194,196],{"id":195},"assign-and-revoke-with-guardrails","Assign and revoke with guardrails",[29,198,199],{},"In the admin area, show current roles, the scope they apply to, and a change history. Add confirmation for risky actions like removing the last Owner from a workspace. Two simple protections stop most 2 a.m. tickets:",[46,201,48,202,48,205],{},[50,203,204],{},"Block self-demotion if it would leave no Owner.",[50,206,207],{},"Enforce “at least one Owner” with a transaction that checks count before removal.",[41,209,211],{"id":210},"handle-bans-and-suspensions","Handle bans and suspensions",[29,213,214,215,217],{},"Mix account state with RBAC. A banned user should be denied everywhere regardless of roles. Implement a fast pre-check in ",[86,216,113],{}," and mirror it on the server.",[41,219,221],{"id":220},"log-what-matters-not-everything","Log what matters, not everything",[29,223,224],{},"Log role changes, denied access, and sensitive operations like exports, deletes, billing changes, and permission edits. Keep payloads small and reference by ID to avoid personal data in logs. A compact event shape works well:",[83,226,227],{},[86,228,229],{},"{\n  type: 'role.removed',\n  actorId: 'usr_... ',\n  targetUserId: 'usr_... ',\n  role: 'ADMIN',\n  scopeId: 'ws_... ',\n  reason: 'requested by owner',\n  ts: 1713811200\n}\n",[29,231,232],{},"Schedule a daily report of RBAC changes. The simplest path is a cron job that hits a Nuxt endpoint that aggregates yesterday’s events and emails a summary to your security or ops channel.",[33,234,236],{"id":235},"edge-cases-you-must-design-for","Edge cases you must design for",[29,238,239],{},"Most RBAC bugs hide in edges. Decide these now so they do not turn into breaches or churn.",[41,241,243],{"id":242},"multi-tenant-scope-is-not-optional","Multi-tenant scope is not optional",[29,245,246,247,250],{},"Tie user roles to a scope like ",[86,248,249],{},"workspace_id",". A user can be an Admin in one workspace and a Viewer in another. In server queries, always filter by both the tenant and the user’s scoped role. If you use Postgres, row-level security is worth the setup so you cannot forget the WHERE clause.",[41,252,254],{"id":253},"payments-drive-access","Payments drive access",[29,256,257],{},"Subscriptions often map to feature permissions. When a payment succeeds, grant or update the role; when a subscription ends, downgrade gracefully. Use one webhook handler that translates provider events into role changes and audit events. Make it idempotent with an events table so retries do not double-apply.",[83,259,260],{},[86,261,262],{},"\u002F\u002F server\u002Fapi\u002Fwebhooks\u002Fbilling.post.ts\nexport default defineEventHandler(async (event) => {\n  const payload = await readRawBody(event)\n  const evt = verifySignature(payload, getHeader(event,'stripe-signature'))\n  if (seen(evt.id)) return 'ok'\n  const { userId, workspaceId, tier } = mapBillingEvent(evt)\n  await applyRoleForTier({ userId, workspaceId, tier })\n  await logEvent({ type: 'billing.role_sync', userId, workspaceId, tier })\n  markSeen(evt.id)\n  return 'ok'\n})\n",[29,264,265],{},"Add a short grace period to handle provider delays so upgrades feel instant and downgrades are polite.",[41,267,269],{"id":268},"authentication-quirks","Authentication quirks",[46,271,48,272,48,275,48,278,48,281],{},[50,273,274],{},"Confirm email before granting roles to invited users.",[50,276,277],{},"Invalidate old sessions after role changes so access shrinks immediately.",[50,279,280],{},"Make magic links single-use and short-lived to block replay.",[50,282,283],{},"On social logins, link to existing accounts by verified email to avoid parallel accounts with different roles.",[41,285,287],{"id":286},"files-and-exports","Files and exports",[29,289,290],{},"Exports and file downloads are sensitive. Check export permission when a job is created and again when the file is downloaded. Scope storage keys per-tenant and expire signed URLs quickly. Tools that process media for users, like SubtitlesFast, often gate higher-quality exports behind paid roles. Your Nuxt RBAC should mirror that pattern at both the job start and the download step.",[41,292,294],{"id":293},"owner-lockout-and-recovery","Owner lockout and recovery",[29,296,297],{},"Protect the Owner role from accidental removal. Keep at least one Owner per workspace and add a recovery path through support that can restore ownership after proof of control. Document the process so support staff do not improvise access changes.",[29,299,300],{},"If you want this all working on day one, start with a Nuxt\u002FVue boilerplate that already ships authentication, protected pages, admin, analytics, i18n, payments, email templates, and deployment tooling. Then your job is to fill the permission catalog and wire the checks, not reinvent sessions, webhooks, and cron wiring.",[33,302,304],{"id":303},"key-takeaways","Key takeaways",[46,306,48,307,48,310,48,313,48,316,48,319],{},[50,308,309],{},"Keep a single permission catalog stored in the database and mirrored in TypeScript for compile-time checks.",[50,311,312],{},"Use route meta and global middleware to guard pages, and re-check permissions on every server handler.",[50,314,315],{},"Manage roles in an admin area with guardrails, bans, and clear change history.",[50,317,318],{},"Log role changes, denials, and sensitive operations; review daily with a simple cron-triggered report.",[50,320,321],{},"Design for edges: tenant scope, billing-driven access, auth quirks, export checks, and owner recovery.",[29,323,324],{},"With a solid RBAC foundation, your Nuxt app grows cleanly, your team moves faster, and customers trust the boundaries you set. Whether you roll your own or start from a Nuxt SaaS boilerplate, keep the model simple, the checks consistent, and the audits boring.",[326,327,330,334,338,341,345,348,352,355,359,362,366],"section",{"className":328},[329],"post-faq",[33,331,333],{"id":332},"faq","FAQ",[41,335,337],{"id":336},"what-is-rbac-in-a-nuxt-app","What is RBAC in a Nuxt app?",[29,339,340],{},"RBAC assigns roles to users and grants permissions based on those roles. In Nuxt, you enforce it with route middleware, server checks, and an admin workflow.",[41,342,344],{"id":343},"should-i-store-roles-in-the-jwt-or-in-the-database","Should I store roles in the JWT or in the database?",[29,346,347],{},"Store roles and permissions in the database as the source of truth. You can cache role IDs in a session token, but always verify on the server before allowing access.",[41,349,351],{"id":350},"how-do-i-connect-subscriptions-to-rbac","How do I connect subscriptions to RBAC?",[29,353,354],{},"Map each plan to a role or set of permissions. On payment events, update the user’s role and log the change. On cancellation, downgrade after a brief grace period.",[41,356,358],{"id":357},"how-can-i-test-rbac-in-nuxt","How can I test RBAC in Nuxt?",[29,360,361],{},"Write unit tests for your can(action, resource) helper, middleware tests for protected routes, and integration tests for server endpoints that read or write sensitive data.",[41,363,365],{"id":364},"what-is-the-safest-way-to-handle-owner-transfers","What is the safest way to handle owner transfers?",[29,367,368],{},"Require confirmation from both parties, prevent removing the last owner, and record an audit entry. Provide a support-backed recovery path for edge cases.",{"title":370,"searchDepth":371,"depth":371,"links":372},"",2,[373,379,385,390,397,398],{"id":35,"depth":371,"text":36,"children":374},[375,377,378],{"id":43,"depth":376,"text":44},3,{"id":70,"depth":376,"text":71},{"id":77,"depth":376,"text":78},{"id":99,"depth":371,"text":100,"children":380},[381,382,383,384],{"id":106,"depth":376,"text":107},{"id":134,"depth":376,"text":135},{"id":154,"depth":376,"text":155},{"id":173,"depth":376,"text":174},{"id":188,"depth":371,"text":189,"children":386},[387,388,389],{"id":195,"depth":376,"text":196},{"id":210,"depth":376,"text":211},{"id":220,"depth":376,"text":221},{"id":235,"depth":371,"text":236,"children":391},[392,393,394,395,396],{"id":242,"depth":376,"text":243},{"id":253,"depth":376,"text":254},{"id":268,"depth":376,"text":269},{"id":286,"depth":376,"text":287},{"id":293,"depth":376,"text":294},{"id":303,"depth":371,"text":304},{"id":332,"depth":371,"text":333,"children":399},[400,401,402,403,404],{"id":336,"depth":376,"text":337},{"id":343,"depth":376,"text":344},{"id":350,"depth":376,"text":351},{"id":357,"depth":376,"text":358},{"id":364,"depth":376,"text":365},"2026-10-06",null,"md",{"@context":409,"@graph":410},"https:\u002F\u002Fschema.org",[411,415],{"@type":412,"headline":6,"description":26,"image":25,"inLanguage":413,"datePublished":414},"BlogPosting","en","2026-10-06 03:04:04",{"@type":416,"mainEntity":417},"FAQPage",[418,422,424,426,428],{"@type":419,"name":337,"acceptedAnswer":420},"Question",{"@type":421,"text":340},"Answer",{"@type":419,"name":344,"acceptedAnswer":423},{"@type":421,"text":347},{"@type":419,"name":351,"acceptedAnswer":425},{"@type":421,"text":354},{"@type":419,"name":358,"acceptedAnswer":427},{"@type":421,"text":361},{"@type":419,"name":365,"acceptedAnswer":429},{"@type":421,"text":368},{},true,[],"\u002Fblog\u002Fnuxt-rbac-guide-roles-protected-pages-admin-controls","nuxt",[],"Informational",{"title":6,"description":26},{"loc":433},"blog\u002Fnuxt-rbac-guide-roles-protected-pages-admin-controls",[434,441,442,443,444],"rbac","saas","security","vue",[434,441,442,443,444],"1hvH2rcMiuln20suc9kIzd93hEPUCD0d2DbLOXAZH0E",[448,453],{"title":449,"path":450,"stem":451,"description":452,"children":-1},"Nuxt Plugin Playbook: Ship Faster with 15 Real Patterns","\u002Fblog\u002Fnuxt-plugin-playbook-15-practical-uses-to-ship-faster","blog\u002Fnuxt-plugin-playbook-15-practical-uses-to-ship-faster","A practical Nuxt plugin playbook for SaaS: auth, payments, i18n, SEO, analytics, AI, marketplace hooks, plus when a Nuxt starter kit is the faster path.",{"title":454,"path":455,"stem":456,"description":457,"children":-1},"Nuxt SaaS case study: from idea to $1,120 MRR in 30 days","\u002Fblog\u002Fnuxt-saas-case-study-first-1k-mrr-in-30-days","blog\u002Fnuxt-saas-case-study-first-1k-mrr-in-30-days","Two engineers hit $1,120 MRR in 30 days using a Nuxt SaaS starter kit with auth, payments, i18n, admin, SEO, and AI features, then iterated pricing to grow.",1791273703955]