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 { 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.