npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

use-convex

v1.3.0

Published

First-class Convex integration for Nuxt

Readme

use-convex

First-class Convex integration for Nuxt 4.

SSR snapshots hydrate through the Nuxt payload, then the browser overlays a live WebSocket subscription.

Install

use-convex on npm

npm i use-convex convex
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['use-convex'],
  convex: {
    url: process.env.NUXT_PUBLIC_CONVEX_URL,
  },
})
<script setup lang="ts">
import { api } from '~~/convex/_generated/api'

const { data, pending, error } = await useConvexQuery(api.tasks.list, {}, { authenticated: true })
</script>

With Nuxt 4's app/ directory, ~~ is the project root (where convex/ lives).

How it works

                 useConvexQuery()
                       │
              ┌────────┴────────┐
              │                 │
           Nuxt SSR          Browser
              │                 │
     ConvexHttpClient      ConvexClient
              │                 │
        useAsyncData        onUpdate
              │                 │
        Nuxt payload ──► live ?? payload

Server and client share the same useAsyncData key so hydration reuses the payload. The browser then overlays ConvexClient.onUpdate.

Set convex.server: false to skip SSR snapshots globally, or pass { server: false } on a single query.

Queries

const { data, pending, error, refresh } = await useConvexQuery(
  api.tasks.list,
  {}, // args, a ref / getter, or 'skip'
  { authenticated: true }, // wait for Convex auth before live subscribe
)

| Option | Default | Purpose | | --------------- | ------------------------ | ------------------------------------------ | | key | function name + args | Nuxt payload / cache key | | server | convex.server (true) | SSR HttpClient snapshot | | lazy | false | Non-blocking on client navigation | | live | true | Subscribe after hydration | | authenticated | false | Wait for Convex auth before live subscribe | | token | cookie / none | Per-request JWT for SSR |

Args accept the query's FunctionArgs, 'skip', or a MaybeRefOrGetter of either.

  • 'skip' — gate on missing ids, feature flags, etc. Keeps the same payload key as empty args so SSR HTML survives.
  • authenticated: true — skip live subscribe until Convex confirms auth. SSR still snapshots when the JWT cookie is present. Prefer this over wrapping args yourself.
  • token — authenticated SSR for this call only. Never put JWTs in runtimeConfig.public. When convex.auth.cookie / HttpOnly auth is set, queries fall back to that cookie.

Warm a subscription before a screen mounts:

prewarmQuery(api.tasks.list, {})

Several queries

Browser-only live map (no SSR). Each value is data | undefined (loading) | Error. Combine with useConvexQuery when you need an SSR snapshot for known queries.

const results = useConvexQueries(
  () => ({
    tasks: { query: api.tasks.list, args: {} },
    files: showFiles.value ? { query: api.files.list, args: {} } : 'skip',
    // Per-entry gate (overrides the options bag when set):
    // admin: { query: api.admin.stats, args: {}, authenticated: true },
  }),
  { authenticated: true }, // wait for Convex auth before any subscribe
)
// results.value.tasks

'skip' still wins for missing ids / feature flags. Prefer { authenticated: true } over wrapping every private entry yourself.

Pagination

const { results, status, isLoading, loadMore } = await useConvexPaginatedQuery(
  api.tasks.listPaginated,
  {},
  { initialNumItems: 20, authenticated: true },
)
// loadMore() when status === 'CanLoadMore'

The first page is SSR'd. On the browser, every loaded page stays live. loadMore fetches the next page.

Mutations and actions

const { mutate, pending, error } = useConvexMutation(api.tasks.create, {
  optimisticUpdate: (localStore, args) => {
    const existing = localStore.getQuery(api.tasks.list, {}) ?? []
    localStore.setQuery(api.tasks.list, {}, [
      { _id: 'tmp', text: args.text, completed: false },
      ...existing,
    ])
  },
})
await mutate({ text: 'Ship it' }) // browser-only

const { run, pending, error } = useConvexAction(api.tasks.shout)
const shouted = await run({ text: 'hello' }) // browser-only

For paginated lists:

insertAtTop({
  paginatedQuery: api.tasks.listPaginated,
  localQueryStore: localStore,
  item: { _id: 'tmp', text: args.text, completed: false },
})

Also available: insertAtBottomIfLoaded, insertAtPosition, optimisticallyUpdateValueInPaginatedQuery.

useConvex() returns the browser ConvexClient when you need an escape hatch. useConvexConnectionState() is a reactive ShallowRef<ConnectionState | null> (WebSocket status: isWebSocketConnected, hasInflightRequests, …).

File uploads

Convex file storage is a three-step client flow: generate a short-lived upload URL, POST the file bytes, then save the returned storageId in a mutation. useConvexFileUpload wraps that for Nuxt (browser-only, with pending / error / progress).

const { upload, pending, error, progress } = useConvexFileUpload({
  generateUploadUrl: api.files.generateUploadUrl,
  saveFile: api.files.save, // ({ storageId, name, contentType, size, ... }) => Id<"files">
})

await upload(file) // progress: 0..1 while bytes fly
// optional extra save args: await upload(file, { caption: '…' })

Your Convex mutations must enforce auth — never expose an unauthenticated generateUploadUrl. Prefer storing storageId (and resolving URLs with ctx.storage.getUrl) over persisting raw public URLs. See the playground files module and the /files page for a full list/upload/delete example.

Cloudflare R2 (@convex-dev/r2)

For larger objects or R2-backed apps, use useConvexR2Upload — the Vue counterpart of @convex-dev/r2/react's useUploadFile. It does not depend on the R2 package at runtime; pass the clientApi() exports from your Convex app:

// convex/r2.ts
import { R2 } from '@convex-dev/r2'
import { components } from './_generated/api'

const r2 = new R2(components.r2)
export const { generateUploadUrl, syncMetadata } = r2.clientApi({
  checkUpload: async (ctx) => {
    /* auth */
  },
})
// Pass r2.clientApi() exports: { generateUploadUrl, syncMetadata }
const { upload, pending, error, progress } = useConvexR2Upload(api.r2)
const key = await upload(file) // R2 object key

That runs generateUploadUrlPUT to the signed URL → syncMetadata({ key }), with XHR progress. Use built-in useConvexFileUpload for Convex storage; use this helper when you adopt the R2 component.

Auth

Convex Auth

Opt in with provider: 'convex-auth'. The module hydrates tokens, finishes OAuth ?code= callbacks, and wires setAuth. Forms only call signIn / signOut.

convex: {
  url: process.env.NUXT_PUBLIC_CONVEX_URL,
  auth: {
    provider: 'convex-auth',
    // cookie defaults to 'convex_jwt'
    // storageNamespace: 'myapp',       // optional; default = deployment URL
    // storage: 'localStorage',         // or 'inMemory' (verifier uses a short-lived cookie)
    // shouldHandleCode: true,          // set false to ignore ?code=
  },
}

For non-default OAuth / callback flows (JS router replaceURL, function shouldHandleCode, custom TokenStorage), call configureConvexAuth from a client plugin that runs before the auth plugin:

// plugins/convex-auth-options.client.ts
export default defineNuxtPlugin({
  name: 'convex-auth-options',
  enforce: 'pre',
  setup() {
    const route = useRoute()
    configureConvexAuth({
      replaceURL: (url) => navigateTo(url, { replace: true }),
      shouldHandleCode: () => route.path === '/auth/callback',
      // storage: window.sessionStorage, // may be async; signIn waits before redirect
      // storageNamespace: 'myapp',
    })
  },
})

storage: 'inMemory' keeps JWTs in memory. The OAuth verifier is a short-lived cookie so the ?code= callback still works after the IdP redirect. Custom storage may return Promises; signIn waits for writes before navigating.

<script setup lang="ts">
const { signIn, signOut } = useAuth()
</script>

<template>
  <AuthLoading>Resolving…</AuthLoading>
  <AuthRefreshing>Refreshing session…</AuthRefreshing>
  <Authenticated>
    <!-- signed-in shell -->
  </Authenticated>
  <Unauthenticated>
    <!-- sign-in form -->
  </Unauthenticated>
</template>

Prefer <Authenticated> (or showAuthedUi) so SSR HTML does not flash the sign-in form. Gate private queries with { authenticated: true }. <AuthRefreshing> shows only while an authenticated session is refreshing a rejected token (same as React).

await signIn('password', { email, password, flow: 'signIn' })
await signIn('github') // OAuth redirect; the plugin finishes on ?code=
await signOut()

You still own the Convex backend:

  • convex/auth.tsconvexAuth({ providers }) exporting auth, signIn, signOut
  • convex/schema.ts — spread authTables
  • convex/http.tsauth.addHttpRoutes(http) (required for OAuth / magic links)
  • convex/auth.config.ts — JWT issuer (CONVEX_SITE_URL)
  • Env: JWT_PRIVATE_KEY + JWKS (npx @convex-dev/auth), plus SITE_URL for OAuth

By default the JWT is a readable cookie (convex_jwt) and the refresh token lives in localStorage. For production Convex Auth apps, prefer HttpOnly dual cookies:

auth: {
  provider: 'convex-auth',
  httpOnly: true, // JWT + refresh via Nitro `/api/convex/auth/session`
}

A readable convex_auth_present marker drives hasSsrSession / showAuthedUi (UI shell only — not authorization). Always gate private live queries with { authenticated: true } (or equivalent). Tokens are HttpOnly cookies; the session API returns the JWT only via same-origin POST { getToken: true } for ConvexClient.setAuth. That mitigates cookie theft, not XSS (any XSS that can call your origin can still obtain a token once the client needs it in memory).

SSR query budget

Defaults are convex.server: true and per-query live: true (HttpClient snapshot and WebSocket). On list-heavy pages, skip SSR or live selectively:

await useConvexQuery(api.tasks.list, {}, { server: false }) // client-only
await useConvexQuery(api.stats.get, {}, { live: false }) // SSR / one-shot only

Or set convex: { server: false } globally.

Protect a route:

// middleware/auth.ts
export default defineNuxtRouteMiddleware(() => {
  return requireConvexAuthMiddleware({ redirectTo: '/login' })
})

useAuthToken() returns the current JWT for authenticated HTTP calls. useConvexGate() exposes { showAuthedUi, showLoading, showSignedOut, showRefreshing } if you prefer flags over layout components (Authenticated / Unauthenticated / AuthLoading / AuthRefreshing).

Bring your own (Clerk, Auth0, custom)

Omit provider and wire useConvexAuth yourself:

// plugins/my-auth.client.ts
useConvexAuth({
  fetchToken: ({ forceRefreshToken }) => auth.getToken({ forceRefreshToken }),
  isLoading: auth.isLoading,
  isAuthenticated: auth.hasSession,
})
const { data } = await useConvexQuery(api.tasks.list, {}, { authenticated: true })

Optional SSR cookie (you must write the JWT after sign-in):

auth: {
  cookie: 'convex_jwt'
}

Server routes

Nitro helpers are auto-imported in server/. Pass the H3 event so they resolve the deployment URL and, when a cookie is configured, the JWT:

// server/api/tasks.get.ts
export default defineEventHandler(async (event) => {
  requireConvexAuth(event)
  return await fetchQuery(api.tasks.list, {}, { event })
})
// server/api/tasks.post.ts
export default defineEventHandler(async (event) => {
  requireConvexAuth(event)
  const { text } = await readBody(event)
  return await fetchMutation(api.tasks.create, { text }, { event })
})
// server/api/shout.post.ts
export default defineEventHandler(async (event) => {
  requireConvexAuth(event)
  const { text } = await readBody(event)
  return await fetchAction(api.tasks.shout, { text }, { event })
})

fetchAction uses the same options. Each helper builds a fresh ConvexHttpClient. Override with { token } when you already have a JWT. Pass { adminToken } (deploy key / admin key) for privileged server tooling — same as Next.js fetchQuery (user token is ignored when adminToken is set). getConvexToken(event) reads the cookie without throwing.

Config

convex: {
  url: process.env.NUXT_PUBLIC_CONVEX_URL,
  server: true, // SSR snapshots; override per query with { server }
  client: { unsavedChangesWarning: false }, // forwarded to new ConvexClient()
  auth: {
    provider: 'convex-auth',
    cookie: 'convex_jwt',
    httpOnly: false,
    presentCookie: 'convex_auth_present',
    storage: 'localStorage', // or 'inMemory'
    // storageNamespace: 'myapp',
    // shouldHandleCode: true,
  },
}

url also reads NUXT_PUBLIC_CONVEX_URL via runtimeConfig.public.convex.url, so you can change the deployment without rebuilding.

Nuxt DevTools

In development, a Convex tab appears in Nuxt DevTools. It embeds the official hosted dashboard for *.convex.cloud URLs and shows module config / tips.

  • Open dashboard deep-links to dashboard.convex.dev (uses your existing Convex login).
  • Auto-login in the embed needs CONVEX_DEPLOY_KEY in the Nuxt process environment (e.g. .env.local). Without it, the iframe shows Convex’s credential form.
  • A deploy key inlined into the DevTools page is visible to anyone who can reach your local nuxt dev server — only set it for trusted local machines.
  • Local / self-hosted backends are not embedded; use the Open dashboard link or the CLI dashboard instead.

Contributing

See CONTRIBUTING.md. Lint/format/test use Vite+ (vp) — Oxlint, Oxfmt, and Vitest — while Nuxt module build/dev stay on nuxt-module-build / nuxi.

Playground

playground/ is a normal Nuxt app that consumes this module (workspace-linked).

pnpm install

# Terminal 1 — Convex backend (writes CONVEX_URL to playground/.env.local)
pnpm run dev:backend
# first time: npx @convex-dev/auth   # JWT_PRIVATE_KEY + JWKS

# Terminal 2 — Nuxt
pnpm run dev

| Route | What it shows | | --------- | ----------------------------------------------------- | | / | Shell session: features + composable call-shape demos | | /live | SSR snapshot + live overlay (sign up, CRUD todos) | | /server | Nitro fetchQuery / fetchMutation / fetchAction | | /files | useConvexFileUpload (upload, list, preview, delete) | | /extras | live: false, pagination, action, connection state |

On /server: GET /api/health is public; GET/POST /api/tasks use the cookie JWT; POST /api/shout is a public fetchAction demo.

Releasing

Releases run from .github/workflows/release.yml when commits that touch src/ land on main (or when the workflow is run manually). Version bumps follow conventional commits:

| Commit | Release | | ------------------------------ | ------- | | feat!: or BREAKING CHANGE: | major | | feat: | minor | | anything else under src/ | patch |

semantic-release publishes use-convex to npm, tags vX.Y.Z, and opens a GitHub Release. The repo package.json version is not committed back.

Publishing uses npm trusted publishing (OIDC), not an NPM_TOKEN. On the use-convex package settings add a GitHub Actions trusted publisher:

  • Organization or user: jrmybtlr
  • Repository: convex-nuxt
  • Workflow filename: release.yml
  • Environment: leave empty
  • Allowed actions: include npm publish (new publishers default to staged publish only)

License

MIT