Real-Time Feed
Build a paginated live feed with stable ordering and optimistic inserts.
Result
The first feed page renders during SSR. Users can load older entries and see new entries immediately.
Backend
import { paginationOptsValidator } from 'convex/server'
import { v } from 'convex/values'
import { mutation, query } from './_generated/server'
export const list = query({
args: {
channelId: v.id('channels'),
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, args) => {
return await ctx.db
.query('messages')
.withIndex('by_channel_created', (q) => q.eq('channelId', args.channelId))
.order('desc')
.paginate(args.paginationOpts)
},
})
export const send = mutation({
args: { channelId: v.id('channels'), body: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) throw new Error('Unauthenticated')
return await ctx.db.insert('messages', {
channelId: args.channelId,
authorId: identity.subject,
body: args.body.trim(),
})
},
})Page state
const feed = await useConvexPaginatedQuery(
api.messages.list,
{ channelId },
{ initialNumItems: 20, auth: 'required' },
)
const send = useConvexMutation(api.messages.send, {
optimisticUpdate: (store, args) => {
insertAtTop({
query: api.messages.list,
argsToMatch: { channelId: args.channelId },
store,
item: {
_id: crypto.randomUUID() as Id<'messages'>,
_creationTime: Date.now(),
channelId: args.channelId,
authorId: currentUserId.value,
body: args.body,
},
})
},
})Use the server result to reconcile the temporary item. Do not persist the temporary ID outside local rendering.
Important behavior
- Live subscriptions cover loaded pages.
- Unloaded history remains unloaded.
- Stable backend ordering prevents items from jumping unpredictably.
loadMore()is guarded byhasNextPageandstatus.
Verify
- Initial messages appear in SSR HTML.
- A second browser sees new messages live.
- A failed send rolls back the optimistic item.
- Loading more never starts two cursor requests concurrently.
- Anonymous callers cannot list or send private channel messages.