Public and Private Data
Combine identity-independent SSR content with authenticated user data on one page.
Result
Public article content renders for everyone and never waits for auth. A signed-in bookmark state uses identity and remains absent for anonymous users.
<script setup lang="ts">
import { api } from '#convex/api'
const route = useRoute()
const slug = computed(() => String(route.params.slug))
const { status } = useConvexAuth()
const { data: article } = useConvexQuery(
api.articles.bySlug,
computed(() => ({ slug: slug.value })),
{ auth: 'none' },
)
const bookmarkArgs = computed(() =>
article.value && status.value === 'authenticated' ? { articleId: article.value._id } : 'skip',
)
const { data: bookmark, status: bookmarkStatus } = useConvexQuery(
api.bookmarks.forArticle,
bookmarkArgs,
{ auth: 'required', server: false },
)
</script>
<template>
<article v-if="article">
<h1>{{ article.title }}</h1>
<ArticleBody :content="article.body" />
<template v-if="status === 'authenticated'">
<BookmarkButton :bookmarked="Boolean(bookmark)" :loading="bookmarkStatus === 'pending'" />
</template>
</article>
</template>Why the modes differ
articles.bySlug uses none: it never waits for auth and never receives identity. This makes its SSR result safe to reason about as public content.
bookmarks.forArticle uses required: it stays idle while anonymous and executes with identity after sign-in. server: false keeps private bookmark state out of the initial HTML for this product choice.
Security boundary
The bookmark query and mutation verify identity and article access. The status condition only controls rendering and whether this page supplies query arguments.
Caching
Public article HTML may be cacheable according to application policy because the public query is identity-independent. Do not include private bookmark data in that shared cache.