Protected Dashboard
Build an authenticated dashboard with route UX and backend-enforced data access.
Result
Anonymous navigation redirects to sign-in. Authenticated users receive server-rendered dashboard data. Convex rejects callers without access even if they bypass the page.
Backend
import { query } from './_generated/server'
import { ConvexError } from 'convex/values'
export const get = query({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) throw new ConvexError({ code: 'UNAUTHENTICATED' })
const projects = await ctx.db
.query('projects')
.withIndex('by_owner', (q) => q.eq('ownerId', identity.subject))
.take(20)
return {
projectCount: projects.length,
recentProjects: projects.slice(0, 5),
}
},
})Page
<script setup lang="ts">
import { api } from '#convex/api'
definePageMeta({ convexAuth: true })
const {
data: dashboard,
status,
error,
} = await useConvexQuery(
api.dashboard.get,
{},
{
auth: 'required',
},
)
</script>
<template>
<main>
<h1>Dashboard</h1>
<DashboardSkeleton v-if="status === 'pending'" />
<p v-else-if="error">Dashboard data is unavailable.</p>
<section v-else-if="dashboard">
<p>{{ dashboard.projectCount }} projects</p>
<ProjectList :projects="dashboard.recentProjects" />
</section>
</main>
</template>Security boundary
convexAuth controls navigation. auth: 'required' controls query execution. The identity and ownership check in dashboard.get controls data access.
Keep all three because they solve different problems.
Verify
- Anonymous page navigation redirects.
- Direct anonymous Convex query rejects.
- User A cannot receive user B's projects.
- Reloading as an authenticated user renders dashboard data without an anonymous flash.