Protect data
Give each todo an owner, check the user in Convex, and protect the page.
Sign-in alone does not protect data. Any client can still call todos.list and todos.create. This page makes each todo belong to one user and checks that user inside Convex.
Clear the old todos
The existing todos have no owner. Convex rejects the new schema while they exist.
Open the Convex dashboard, go to Data, then todos, select all documents, and delete them. Do not assign old todos to the first user who signs in.
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']),
})ownerId stores the Better Auth user ID.
Check the user in Convex
import { ConvexError, v } from 'convex/values'
import { mutation, query } from './_generated/server'
import { auth } from './auth'
export const list = query({
args: {},
handler: async (ctx) => {
const user = await auth.requireUser(ctx)
return await ctx.db
.query('todos')
.withIndex('by_owner_created', (q) => q.eq('ownerId', user.id))
.order('desc')
.take(50)
},
})
export const create = mutation({
args: { text: v.string() },
handler: async (ctx, args) => {
const user = await auth.requireUser(ctx)
const text = args.text.trim()
if (!text || text.length > 200) {
throw new ConvexError({
code: 'INVALID_TODO_TEXT',
message: 'Todo text must contain 1 to 200 characters',
})
}
return await ctx.db.insert('todos', {
ownerId: user.id,
text,
completed: false,
createdAt: Date.now(),
})
},
})auth.requireUser(ctx) returns the signed-in user. It checks that the session is still valid. When nobody is signed in, it throws a ConvexError with code UNAUTHENTICATED.
listreads only the caller's todos.createtakes the owner from the session, never from the client.
Use auth.getUser(ctx) instead when a function also works for anonymous callers. It returns null when nobody is signed in. See backend authorization.
Wait for sign-in before the query runs
In app/pages/index.vue, change the query line:
const { data: todos, status, error } = useConvexQuery(api.todos.list, {}, { auth: 'required' })With auth: 'required', the query does not run while nobody is signed in. Its status stays 'idle'. Without it, an anonymous visit would call list and get an UNAUTHENTICATED error.
This option only controls when the query runs. The check in Convex is what protects the data. Keep both.
Protect the page
Add this line to the top of the <script setup> block in app/pages/index.vue:
definePageMeta({ convexAuth: true })An anonymous visitor to / now goes to /auth/signin?redirect=/. After sign-in, the sign-in page sends them back to /.
To send visitors to another sign-in page, set convex.auth.redirectTo in nuxt.config.ts. To protect every page by default, set convex.auth.routes: 'protected' and mark public pages with convexAuth: false. See route protection.
Test it
- Sign out and open
/. You land on the sign-in page. - Sign in as user A and add two todos.
- Sign out. Create user B and sign in. The list is empty.
Step 3 shows that Convex keeps each user's data apart. Step 1 only shows that the page redirects.
Next, see the project structure or go to next steps.