Protect Data
Enforce ownership in Convex and add Nuxt route protection for navigation.
Authentication does not secure a function by itself. Update the todo app so every document belongs to the authenticated Convex identity.
Add an owner to the schema
import { defineSchema, defineTable } from 'convex/server'
import { v } from 'convex/values'
export default defineSchema({
todos: defineTable({
ownerId: v.string(),
text: v.string(),
completed: v.boolean(),
createdAt: v.number(),
}).index('by_owner_created', ['ownerId', 'createdAt']),
})Delete or migrate public development records before deploying this schema change. Do not assign existing records to the first user who reads them.
Require identity in the backend
import { mutation, query } from './_generated/server'
import { ConvexError, v } from 'convex/values'
async function requireIdentity(ctx: {
auth: { getUserIdentity(): Promise<{ subject: string } | null> }
}) {
const identity = await ctx.auth.getUserIdentity()
if (!identity) throw new ConvexError({ code: 'UNAUTHENTICATED' })
return identity
}
export const list = query({
args: {},
handler: async (ctx) => {
const identity = await requireIdentity(ctx)
return await ctx.db
.query('todos')
.withIndex('by_owner_created', (q) => q.eq('ownerId', identity.subject))
.order('desc')
.take(50)
},
})
export const create = mutation({
args: { text: v.string() },
handler: async (ctx, args) => {
const identity = await requireIdentity(ctx)
const text = args.text.trim()
if (!text || text.length > 200) {
throw new ConvexError({ code: 'INVALID_TODO_TEXT' })
}
return await ctx.db.insert('todos', {
ownerId: identity.subject,
text,
completed: false,
createdAt: Date.now(),
})
},
})The query only reads the caller's indexed documents. The mutation ignores any client-supplied owner and writes the validated identity.
Mark the query as required
const {
data: todos,
status,
error,
} = await useConvexQuery(
api.todos.list,
{},
{
auth: 'required',
},
)An anonymous required query stays idle instead of executing anonymously.
This is execution policy, not the backend check. Keep both.
Protect navigation
Configure the default sign-in route:
export default defineNuxtConfig({
modules: ['better-convex-nuxt'],
convex: {
auth: {
routeProtection: {
redirectTo: '/auth',
preserveReturnTo: true,
},
},
},
})Mark the todo page:
definePageMeta({ convexAuth: true })The middleware waits for auth settlement and redirects anonymous navigation. It preserves a local return path in the redirect query parameter.
Verify the boundary
Test all three cases:
- Anonymous navigation redirects to
/auth. - An authenticated user can create and list their todos.
- A different authenticated user cannot list the first user's todos.
The third test proves backend isolation. The first proves route UX.