Skip to main content

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.

app/pages/articles/[slug].vue
<script setup lang="ts">
import { api } from '#convex/api'

const route = useRoute()
const slug = computed(() => String(route.params.slug))

const { data: article } = await useConvexQuery(
  api.articles.bySlug,
  computed(() => ({ slug: slug.value })),
  { auth: 'none' },
)

const bookmarkArgs = computed(() => (article.value ? { articleId: article.value._id } : 'skip'))
const { data: bookmark, status: bookmarkStatus } = await useConvexQuery(
  api.bookmarks.forArticle,
  bookmarkArgs,
  { auth: 'required', server: false },
)
</script>

<template>
  <article v-if="article">
    <h1>{{ article.title }}</h1>
    <ArticleBody :content="article.body" />

    <ConvexAuthenticated>
      <BookmarkButton :bookmarked="Boolean(bookmark)" :loading="bookmarkStatus === 'pending'" />
    </ConvexAuthenticated>
  </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. <ConvexAuthenticated> only controls rendering.

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.