nuxt-convex 0.0.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,508 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/onmax/nuxt-convex/main/.github/og.webp" alt="Nuxt Convex" width="100%">
3
+ </p>
4
+
5
+ <h1 align="center">nuxt-convex</h1>
6
+
7
+ <p align="center">Nuxt module for <a href="https://convex.dev">Convex</a> - reactive backend with real-time sync, file storage, and auto-imports.</p>
8
+
9
+ <p align="center">
10
+ <a href="https://npmjs.com/package/nuxt-convex"><img src="https://img.shields.io/npm/v/nuxt-convex/latest.svg?style=flat&colorA=020420&colorB=00DC82" alt="npm version"></a>
11
+ <a href="https://npm.chart.dev/nuxt-convex"><img src="https://img.shields.io/npm/dm/nuxt-convex.svg?style=flat&colorA=020420&colorB=00DC82" alt="npm downloads"></a>
12
+ <a href="https://npmjs.com/package/nuxt-convex"><img src="https://img.shields.io/npm/l/nuxt-convex.svg?style=flat&colorA=020420&colorB=00DC82" alt="License"></a>
13
+ <a href="https://nuxt.com"><img src="https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js" alt="Nuxt"></a>
14
+ </p>
15
+
16
+ > **[Live Demo](https://nuxt-convex-playground.workers.dev)** · [Convex Docs](https://docs.convex.dev)
17
+
18
+ ## Features
19
+
20
+ - **Real-time by default** - Queries auto-update when data changes
21
+ - **Virtual modules** - Import from `#convex`, `#convex/api`, and `#convex/storage`
22
+ - **Auto-imports** - Composables available without manual imports
23
+ - **File storage** - Upload API with auto-scaffolded Convex functions
24
+ - **DevTools** - Convex dashboard tab in Nuxt DevTools
25
+
26
+ ## Requirements
27
+
28
+ - Nuxt 3.0.0 or higher
29
+ - A Convex project (run `npx convex dev` to create one)
30
+
31
+ ## Setup
32
+
33
+ Install the module and its peer dependencies:
34
+
35
+ ```bash
36
+ pnpm add nuxt-convex convex @convex-vue/core
37
+ ```
38
+
39
+ Add the module to your Nuxt configuration:
40
+
41
+ ```ts [nuxt.config.ts]
42
+ export default defineNuxtConfig({
43
+ modules: ['nuxt-convex'],
44
+ convex: {
45
+ storage: true, // Enable file storage (optional)
46
+ },
47
+ })
48
+ ```
49
+
50
+ The module reads `CONVEX_URL` or `NUXT_PUBLIC_CONVEX_URL` environment variables automatically. Run `npx convex dev` to start the Convex development server.
51
+
52
+ ## Creating Your Backend
53
+
54
+ Convex functions live in the `convex/` directory at your project root. See [Convex docs](https://docs.convex.dev/functions) for full reference.
55
+
56
+ ### Schema
57
+
58
+ Define your database tables with [schemas](https://docs.convex.dev/database/schemas). Tables are created automatically when you push to Convex.
59
+
60
+ ```ts [convex/schema.ts]
61
+ import { defineSchema, defineTable } from 'convex/server'
62
+ import { v } from 'convex/values'
63
+
64
+ export default defineSchema({
65
+ tasks: defineTable({
66
+ title: v.string(),
67
+ userId: v.optional(v.string()),
68
+ isCompleted: v.boolean(),
69
+ createdAt: v.number(),
70
+ }).index('by_user', ['userId']),
71
+ })
72
+ ```
73
+
74
+ The `v` object provides [validators](https://docs.convex.dev/database/schemas#validators) for all Convex types: `v.string()`, `v.number()`, `v.boolean()`, `v.id('tableName')`, `v.array()`, `v.object()`, `v.optional()`, `v.union()`, etc.
75
+
76
+ ### Queries
77
+
78
+ [Queries](https://docs.convex.dev/functions/query-functions) read data from the database. They are reactive - the UI updates automatically when underlying data changes.
79
+
80
+ ```ts [convex/tasks.ts]
81
+ import { v } from 'convex/values'
82
+ import { query } from './_generated/server'
83
+
84
+ export const list = query({
85
+ args: { userId: v.string() },
86
+ handler: async (ctx, { userId }) => {
87
+ return await ctx.db
88
+ .query('tasks')
89
+ .withIndex('by_user', q => q.eq('userId', userId))
90
+ .order('desc')
91
+ .collect()
92
+ },
93
+ })
94
+
95
+ export const get = query({
96
+ args: { id: v.id('tasks') },
97
+ handler: async (ctx, { id }) => {
98
+ return await ctx.db.get(id)
99
+ },
100
+ })
101
+ ```
102
+
103
+ ### Mutations
104
+
105
+ [Mutations](https://docs.convex.dev/functions/mutation-functions) write data to the database. They are transactional and run on the server.
106
+
107
+ ```ts [convex/tasks.ts]
108
+ import { v } from 'convex/values'
109
+ import { mutation } from './_generated/server'
110
+
111
+ export const add = mutation({
112
+ args: { title: v.string(), userId: v.string() },
113
+ handler: async (ctx, { title, userId }) => {
114
+ return await ctx.db.insert('tasks', {
115
+ title,
116
+ userId,
117
+ isCompleted: false,
118
+ createdAt: Date.now(),
119
+ })
120
+ },
121
+ })
122
+
123
+ export const toggle = mutation({
124
+ args: { id: v.id('tasks') },
125
+ handler: async (ctx, { id }) => {
126
+ const task = await ctx.db.get(id)
127
+ if (task) {
128
+ await ctx.db.patch(id, { isCompleted: !task.isCompleted })
129
+ }
130
+ },
131
+ })
132
+
133
+ export const remove = mutation({
134
+ args: { id: v.id('tasks') },
135
+ handler: async (ctx, { id }) => {
136
+ await ctx.db.delete(id)
137
+ },
138
+ })
139
+ ```
140
+
141
+ ### Actions
142
+
143
+ [Actions](https://docs.convex.dev/functions/actions) run arbitrary code including external API calls, but cannot directly access the database. Use them for third-party integrations.
144
+
145
+ ```ts [convex/ai.ts]
146
+ import { v } from 'convex/values'
147
+ import { action } from './_generated/server'
148
+
149
+ export const summarize = action({
150
+ args: { text: v.string() },
151
+ handler: async (ctx, { text }) => {
152
+ const response = await fetch('https://api.openai.com/v1/...', {
153
+ method: 'POST',
154
+ headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
155
+ body: JSON.stringify({ prompt: text }),
156
+ })
157
+ return await response.json()
158
+ },
159
+ })
160
+ ```
161
+
162
+ Actions can call mutations/queries via `ctx.runMutation()` and `ctx.runQuery()`.
163
+
164
+ ## Usage
165
+
166
+ Composables are auto-imported. They wrap [@convex-vue/core](https://github.com/niconiahi/convex-vue).
167
+
168
+ ### useConvexQuery
169
+
170
+ Subscribe to a [query](https://docs.convex.dev/functions/query-functions). Returns reactive data that auto-updates when the database changes.
171
+
172
+ ```vue [app/pages/tasks.vue]
173
+ <script setup lang="ts">
174
+ import { api } from '#convex/api'
175
+
176
+ const { data: tasks, isLoading, error } = useConvexQuery(api.tasks.list, { userId: 'user_123' })
177
+ </script>
178
+
179
+ <template>
180
+ <div v-if="isLoading">
181
+ Loading...
182
+ </div>
183
+ <div v-else-if="error">
184
+ Error: {{ error.message }}
185
+ </div>
186
+ <ul v-else>
187
+ <li v-for="task in tasks" :key="task._id">
188
+ {{ task.title }}
189
+ </li>
190
+ </ul>
191
+ </template>
192
+ ```
193
+
194
+ **Return values:**
195
+
196
+ | Property | Type | Description |
197
+ | ----------- | --------------------- | ----------------------- |
198
+ | `data` | `Ref<T \| undefined>` | Query result, reactive |
199
+ | `isLoading` | `Ref<boolean>` | True while initial load |
200
+ | `error` | `Ref<Error \| null>` | Error if query failed |
201
+
202
+ ### useConvexMutation
203
+
204
+ Call a [mutation](https://docs.convex.dev/functions/mutation-functions) to write data.
205
+
206
+ ```vue [app/components/AddTask.vue]
207
+ <script setup lang="ts">
208
+ import { api } from '#convex/api'
209
+
210
+ const { mutate: addTask, isLoading, error } = useConvexMutation(api.tasks.add)
211
+
212
+ const title = ref('')
213
+
214
+ async function handleSubmit() {
215
+ await addTask({ title: title.value, userId: 'user_123' })
216
+ title.value = ''
217
+ }
218
+ </script>
219
+
220
+ <template>
221
+ <form @submit.prevent="handleSubmit">
222
+ <input v-model="title" :disabled="isLoading" placeholder="New task...">
223
+ <button :disabled="isLoading">
224
+ Add
225
+ </button>
226
+ <p v-if="error">
227
+ {{ error.message }}
228
+ </p>
229
+ </form>
230
+ </template>
231
+ ```
232
+
233
+ **Return values:**
234
+
235
+ | Property | Type | Description |
236
+ | ----------- | ---------------------- | ------------------------ |
237
+ | `mutate` | `(args) => Promise<T>` | Call the mutation |
238
+ | `isLoading` | `Ref<boolean>` | True while running |
239
+ | `error` | `Ref<Error \| null>` | Error if mutation failed |
240
+
241
+ ### useConvexAction
242
+
243
+ Call an [action](https://docs.convex.dev/functions/actions) for external API calls or long-running tasks.
244
+
245
+ ```vue [app/components/Summarize.vue]
246
+ <script setup lang="ts">
247
+ import { api } from '#convex/api'
248
+
249
+ const { execute: summarize, isLoading, error } = useConvexAction(api.ai.summarize)
250
+
251
+ const result = ref('')
252
+
253
+ async function handleSummarize(text: string) {
254
+ result.value = await summarize({ text })
255
+ }
256
+ </script>
257
+ ```
258
+
259
+ **Return values:**
260
+
261
+ | Property | Type | Description |
262
+ | ----------- | ---------------------- | ---------------------- |
263
+ | `execute` | `(args) => Promise<T>` | Call the action |
264
+ | `isLoading` | `Ref<boolean>` | True while running |
265
+ | `error` | `Ref<Error \| null>` | Error if action failed |
266
+
267
+ ### useConvex
268
+
269
+ Get the raw [ConvexClient](https://docs.convex.dev/api/classes/browser.ConvexClient) for advanced usage.
270
+
271
+ ```vue [app/components/Advanced.vue]
272
+ <script setup lang="ts">
273
+ import { api } from '#convex/api'
274
+
275
+ const client = useConvex()
276
+
277
+ // Direct client access for advanced patterns
278
+ const result = await client.query(api.tasks.get, { id: 'abc123' })
279
+ </script>
280
+ ```
281
+
282
+ ## File Storage
283
+
284
+ Convex provides built-in [file storage](https://docs.convex.dev/file-storage). Enable it in your config to auto-scaffold the required functions.
285
+
286
+ ### Setup
287
+
288
+ ```ts [nuxt.config.ts]
289
+ export default defineNuxtConfig({
290
+ modules: ['nuxt-convex'],
291
+ convex: {
292
+ storage: true,
293
+ },
294
+ })
295
+ ```
296
+
297
+ This creates `convex/_hub/storage.ts` with `generateUploadUrl`, `getUrl`, and `remove` functions. Customize as needed.
298
+
299
+ Add the `uploads` table to your schema:
300
+
301
+ ```ts [convex/schema.ts]
302
+ import { defineSchema, defineTable } from 'convex/server'
303
+ import { v } from 'convex/values'
304
+
305
+ export default defineSchema({
306
+ uploads: defineTable({
307
+ storageId: v.id('_storage'),
308
+ name: v.string(),
309
+ type: v.string(),
310
+ url: v.optional(v.string()),
311
+ userId: v.string(),
312
+ createdAt: v.number(),
313
+ }).index('by_user', ['userId']),
314
+ })
315
+ ```
316
+
317
+ ### useConvexStorage
318
+
319
+ Low-level composable for storage operations. Auto-imported when storage is enabled.
320
+
321
+ ```vue [app/components/Storage.vue]
322
+ <script setup lang="ts">
323
+ import { api } from '#convex/api'
324
+
325
+ const { generateUploadUrl, getUrl, remove } = useConvexStorage(api)
326
+
327
+ // Get a reactive URL for a stored file
328
+ const fileUrl = getUrl('storage_id_here')
329
+
330
+ // Generate upload URL for direct upload
331
+ const uploadUrl = await generateUploadUrl.mutate()
332
+
333
+ // Delete a file
334
+ await remove.mutate({ storageId: 'storage_id_here' })
335
+ </script>
336
+ ```
337
+
338
+ **Return values:**
339
+
340
+ | Property | Type | Description |
341
+ | ------------------- | -------------------------------------------- | --------------------- |
342
+ | `generateUploadUrl` | `{ mutate: () => Promise<string> }` | Get URL for uploading |
343
+ | `getUrl` | `(storageId: string) => Ref<string \| null>` | Reactive file URL |
344
+ | `remove` | `{ mutate: (args) => Promise<void> }` | Delete a file |
345
+
346
+ ### useConvexUpload
347
+
348
+ High-level composable for file uploads with progress tracking. Auto-imported.
349
+
350
+ ```vue [app/pages/upload.vue]
351
+ <script setup lang="ts">
352
+ import { api } from '#convex/api'
353
+
354
+ const { generateUploadUrl } = useConvexStorage(api)
355
+ const saveFile = useConvexMutation(api._hub.storage.saveFile)
356
+
357
+ const { upload, isUploading, progress, error } = useConvexUpload({
358
+ generateUploadUrl,
359
+ onSuccess: async (storageId, file) => {
360
+ await saveFile.mutate({
361
+ storageId,
362
+ name: file.name,
363
+ type: file.type,
364
+ userId: 'user_123',
365
+ })
366
+ },
367
+ onError: err => console.error('Upload failed:', err),
368
+ })
369
+
370
+ async function handleFileChange(event: Event) {
371
+ const file = (event.target as HTMLInputElement).files?.[0]
372
+ if (file) {
373
+ const storageId = await upload(file)
374
+ console.log('Uploaded:', storageId)
375
+ }
376
+ }
377
+ </script>
378
+
379
+ <template>
380
+ <div>
381
+ <input type="file" :disabled="isUploading" @change="handleFileChange">
382
+ <p v-if="isUploading">
383
+ Uploading... {{ progress }}%
384
+ </p>
385
+ <p v-if="error">
386
+ {{ error.message }}
387
+ </p>
388
+ </div>
389
+ </template>
390
+ ```
391
+
392
+ **Options:**
393
+
394
+ | Option | Type | Description |
395
+ | ------------------- | ----------------------------------------- | --------------------------------- |
396
+ | `generateUploadUrl` | `{ mutate: () => Promise<string> }` | Required. From `useConvexStorage` |
397
+ | `onSuccess` | `(storageId: string, file: File) => void` | Called after successful upload |
398
+ | `onError` | `(error: Error) => void` | Called on upload error |
399
+
400
+ **Return values:**
401
+
402
+ | Property | Type | Description |
403
+ | ------------- | ----------------------------------------- | -------------------------------- |
404
+ | `upload` | `(file: File) => Promise<string \| null>` | Upload a file, returns storageId |
405
+ | `isUploading` | `Ref<boolean>` | True while uploading |
406
+ | `progress` | `Ref<number>` | Upload progress 0-100 |
407
+ | `error` | `Ref<Error \| null>` | Error if upload failed |
408
+
409
+ ## Configuration
410
+
411
+ | Option | Type | Default | Description |
412
+ | --------- | --------- | ------------------------ | ------------------------------- |
413
+ | `url` | `string` | `process.env.CONVEX_URL` | Convex deployment URL |
414
+ | `storage` | `boolean` | `false` | Enable file storage integration |
415
+
416
+ **Environment variables:**
417
+
418
+ | Variable | Description |
419
+ | ------------------------ | ----------------------------------------------- |
420
+ | `CONVEX_URL` | Convex deployment URL (set by `npx convex dev`) |
421
+ | `NUXT_PUBLIC_CONVEX_URL` | Alternative, follows Nuxt convention |
422
+
423
+ ## DevTools
424
+
425
+ When running in development, the module adds a **Convex** tab to Nuxt DevTools with an embedded Convex dashboard. Access your data, run functions, and view logs directly from DevTools.
426
+
427
+ ## Better Auth Integration
428
+
429
+ Use [nuxt-better-auth](https://nuxt-better-auth.onmax.me/) for authentication alongside Convex for your app data.
430
+
431
+ ### 1. Install dependencies
432
+
433
+ ```bash
434
+ pnpm add @onmax/nuxt-better-auth
435
+ ```
436
+
437
+ ### 2. Configure nuxt-better-auth
438
+
439
+ ```ts [nuxt.config.ts]
440
+ export default defineNuxtConfig({
441
+ modules: ['nuxt-convex', '@onmax/nuxt-better-auth'],
442
+ convex: { storage: true },
443
+ })
444
+ ```
445
+
446
+ ### 3. Create server auth config
447
+
448
+ ```ts [server/auth.config.ts]
449
+ import { defineServerAuth } from '@onmax/nuxt-better-auth/config'
450
+
451
+ export default defineServerAuth(() => ({
452
+ emailAndPassword: { enabled: true },
453
+ }))
454
+ ```
455
+
456
+ See [nuxt-better-auth docs](https://nuxt-better-auth.onmax.me/) for database adapters, social providers, and plugins.
457
+
458
+ ### Using Convex as Auth Database (Advanced)
459
+
460
+ To store auth data directly in Convex instead of a SQL database:
461
+
462
+ ```bash
463
+ pnpm add @convex-dev/better-auth better-auth --save-exact
464
+ ```
465
+
466
+ ```ts [convex/convex.config.ts]
467
+ import betterAuth from '@convex-dev/better-auth/convex.config'
468
+ import { defineApp } from 'convex/server'
469
+
470
+ const app = defineApp()
471
+ app.use(betterAuth)
472
+ export default app
473
+ ```
474
+
475
+ ```ts [server/auth.config.ts]
476
+ import { components } from '#convex/api'
477
+ import { createClient } from '@convex-dev/better-auth'
478
+ import { convex } from '@convex-dev/better-auth/server/plugins'
479
+ import { defineServerAuth } from '@onmax/nuxt-better-auth/config'
480
+
481
+ const authComponent = createClient(components.betterAuth)
482
+
483
+ export default defineServerAuth(() => ({
484
+ database: authComponent.adapter(),
485
+ plugins: [convex()],
486
+ emailAndPassword: { enabled: true },
487
+ }))
488
+ ```
489
+
490
+ This stores users, sessions, and accounts in Convex. See [@convex-dev/better-auth](https://github.com/get-convex/convex-better-auth) for details.
491
+
492
+ ## Development
493
+
494
+ ```bash
495
+ pnpm install
496
+ pnpm dev:prepare
497
+ pnpm dev
498
+ ```
499
+
500
+ Run tests:
501
+
502
+ ```bash
503
+ pnpm test
504
+ ```
505
+
506
+ ## License
507
+
508
+ MIT
@@ -0,0 +1,17 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ConvexConfig {
4
+ /** Convex deployment URL. Auto-detected from CONVEX_URL or NUXT_PUBLIC_CONVEX_URL env vars */
5
+ url?: string;
6
+ /** Enable file storage integration. When true, scaffolds convex/_hub/storage.ts */
7
+ storage?: boolean;
8
+ }
9
+ interface ResolvedConvexConfig {
10
+ url: string;
11
+ storage: boolean;
12
+ }
13
+
14
+ declare const _default: _nuxt_schema.NuxtModule<ConvexConfig, ConvexConfig, false>;
15
+
16
+ export { _default as default };
17
+ export type { ConvexConfig, ResolvedConvexConfig };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "nuxt-convex",
3
+ "version": "0.0.1-alpha.0",
4
+ "configKey": "convex",
5
+ "compatibility": {
6
+ "nuxt": ">=3.0.0"
7
+ },
8
+ "builder": {
9
+ "@nuxt/module-builder": "1.0.2",
10
+ "unbuild": "unknown"
11
+ }
12
+ }
@@ -0,0 +1,181 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import process from 'node:process';
4
+ import { defineNuxtModule, createResolver, addPlugin, addImports, addTemplate, addTypeTemplate } from '@nuxt/kit';
5
+ import { consola } from 'consola';
6
+ import { defu } from 'defu';
7
+ import { join } from 'pathe';
8
+
9
+ const version = "0.0.1-alpha.0";
10
+
11
+ const log = consola.withTag("nuxt:convex");
12
+ const module$1 = defineNuxtModule({
13
+ meta: {
14
+ name: "nuxt-convex",
15
+ version,
16
+ configKey: "convex",
17
+ compatibility: { nuxt: ">=3.0.0" }
18
+ },
19
+ defaults: {
20
+ storage: false
21
+ },
22
+ async onInstall(nuxt) {
23
+ const options = nuxt.options.convex;
24
+ if (options?.storage) {
25
+ await scaffoldStorageFunctions(nuxt);
26
+ }
27
+ },
28
+ async setup(options, nuxt) {
29
+ const { resolve } = createResolver(import.meta.url);
30
+ const config = resolveConfig(options);
31
+ nuxt.options.runtimeConfig.public.convex = defu(
32
+ nuxt.options.runtimeConfig.public.convex,
33
+ { url: config.url }
34
+ );
35
+ addPlugin({ src: resolve("./runtime/plugin.client"), mode: "client" });
36
+ addImports({ name: "useConvexUpload", from: resolve("./runtime/composables/useConvexUpload") });
37
+ setupConvexApiAlias(nuxt);
38
+ setupConvexClient(nuxt);
39
+ if (config.storage) {
40
+ await setupConvexStorage(nuxt, resolve);
41
+ }
42
+ if (nuxt.options.dev) {
43
+ addConvexDevTools(nuxt, config);
44
+ }
45
+ nuxt.hook("ready", () => {
46
+ if (config.url) {
47
+ log.info(`Convex connected: ${config.url}`);
48
+ } else {
49
+ log.warn("Convex URL not configured. Set CONVEX_URL or NUXT_PUBLIC_CONVEX_URL");
50
+ }
51
+ });
52
+ }
53
+ });
54
+ function resolveConfig(options) {
55
+ const url = options.url || process.env.CONVEX_URL || process.env.NUXT_PUBLIC_CONVEX_URL || "";
56
+ return defu(options, { url, storage: false });
57
+ }
58
+ function setupConvexClient(nuxt, _resolve) {
59
+ const template = addTemplate({
60
+ filename: "convex/client.mjs",
61
+ getContents: () => `// Auto-generated by nuxt-convex
62
+ import { inject } from 'vue'
63
+ import { CONVEX_INJECTION_KEY } from '@convex-vue/core'
64
+ export { useConvexQuery, useConvexMutation, useConvexAction } from '@convex-vue/core'
65
+
66
+ // Get the Convex client - MUST be called during component setup
67
+ export function useConvex() {
68
+ const client = inject(CONVEX_INJECTION_KEY)
69
+ if (!client) throw new Error('[nuxt-convex] Convex client not found. Is the plugin installed?')
70
+ return client
71
+ }
72
+ `,
73
+ write: true
74
+ });
75
+ addTypeTemplate({
76
+ filename: "types/convex.d.ts",
77
+ getContents: () => `// Auto-generated by nuxt-convex
78
+ import type { ConvexClient } from 'convex/browser'
79
+ declare module '#convex' {
80
+ export { useConvexQuery, useConvexMutation, useConvexAction } from '@convex-vue/core'
81
+ export function useConvex(): ConvexClient
82
+ }
83
+ `
84
+ }, { nitro: true, nuxt: true });
85
+ nuxt.options.alias["#convex"] = template.dst;
86
+ addImports([
87
+ { name: "useConvexQuery", from: "#convex" },
88
+ { name: "useConvexMutation", from: "#convex" },
89
+ { name: "useConvexAction", from: "#convex" },
90
+ { name: "useConvex", from: "#convex" }
91
+ ]);
92
+ }
93
+ function setupConvexApiAlias(nuxt) {
94
+ const generatedDir = join(nuxt.options.rootDir, "convex/_generated");
95
+ if (!existsSync(generatedDir)) {
96
+ log.warn("convex/_generated not found. Run `npx convex dev` to generate types.");
97
+ }
98
+ nuxt.options.alias["#convex/api"] = join(generatedDir, "api");
99
+ addTypeTemplate({
100
+ filename: "types/convex-api.d.ts",
101
+ getContents: () => `// Auto-generated by nuxt-convex
102
+ declare module '#convex/api' {
103
+ export * from '../../convex/_generated/api'
104
+ }
105
+ `
106
+ }, { nitro: true, nuxt: true });
107
+ }
108
+ async function setupConvexStorage(nuxt, resolve) {
109
+ const template = addTemplate({
110
+ filename: "convex/storage.mjs",
111
+ getContents: () => `// Auto-generated by nuxt-convex
112
+ export { useConvexStorage } from '${resolve("./runtime/composables/useConvexStorage")}'
113
+ `,
114
+ write: true
115
+ });
116
+ addTypeTemplate({
117
+ filename: "types/convex-storage.d.ts",
118
+ getContents: () => `// Auto-generated by nuxt-convex
119
+ declare module '#convex/storage' {
120
+ import type { Ref } from 'vue'
121
+
122
+ export interface ConvexStorageReturn {
123
+ generateUploadUrl: () => Promise<string>
124
+ getUrl: (storageId: string) => Ref<string | null>
125
+ remove: (storageId: string) => Promise<void>
126
+ }
127
+
128
+ export function useConvexStorage(): ConvexStorageReturn
129
+ }
130
+ `
131
+ }, { nitro: true, nuxt: true });
132
+ nuxt.options.alias["#convex/storage"] = template.dst;
133
+ addImports({ name: "useConvexStorage", from: "#convex/storage" });
134
+ await scaffoldStorageFunctions(nuxt);
135
+ }
136
+ async function scaffoldStorageFunctions(nuxt) {
137
+ const storageDir = join(nuxt.options.rootDir, "convex", "_hub");
138
+ const storagePath = join(storageDir, "storage.ts");
139
+ if (existsSync(storagePath))
140
+ return;
141
+ await mkdir(storageDir, { recursive: true });
142
+ await writeFile(storagePath, `// Auto-generated by nuxt-convex - feel free to customize
143
+ import { mutation, query } from '../_generated/server'
144
+ import { v } from 'convex/values'
145
+
146
+ export const generateUploadUrl = mutation(async (ctx) => {
147
+ return await ctx.storage.generateUploadUrl()
148
+ })
149
+
150
+ export const getUrl = query({
151
+ args: { storageId: v.id('_storage') },
152
+ handler: async (ctx, { storageId }) => {
153
+ return await ctx.storage.getUrl(storageId)
154
+ },
155
+ })
156
+
157
+ export const remove = mutation({
158
+ args: { storageId: v.id('_storage') },
159
+ handler: async (ctx, { storageId }) => {
160
+ await ctx.storage.delete(storageId)
161
+ },
162
+ })
163
+ `);
164
+ log.success("Created convex/_hub/storage.ts");
165
+ }
166
+ function addConvexDevTools(nuxt, config) {
167
+ nuxt.hook("devtools:customTabs", (tabs) => {
168
+ if (!config.url)
169
+ return;
170
+ const dashboardUrl = config.url.replace(".convex.cloud", ".convex.dev");
171
+ tabs.push({
172
+ category: "server",
173
+ name: "convex",
174
+ title: "Convex",
175
+ icon: "i-simple-icons-convex",
176
+ view: { type: "iframe", src: dashboardUrl }
177
+ });
178
+ });
179
+ }
180
+
181
+ export { module$1 as default };
@@ -0,0 +1,28 @@
1
+ import type { Ref } from '#imports';
2
+ import type { FunctionReference } from 'convex/server';
3
+ export interface ConvexStorageApi {
4
+ _hub?: {
5
+ storage?: {
6
+ generateUploadUrl: FunctionReference<'mutation'>;
7
+ getUrl: FunctionReference<'query'>;
8
+ remove: FunctionReference<'mutation'>;
9
+ };
10
+ };
11
+ }
12
+ export interface ConvexStorageReturn {
13
+ generateUploadUrl: {
14
+ mutate: () => Promise<string>;
15
+ };
16
+ getUrl: (storageId: string) => Ref<string | null>;
17
+ remove: {
18
+ mutate: (args: {
19
+ storageId: string;
20
+ }) => Promise<void>;
21
+ };
22
+ }
23
+ /**
24
+ * Composable for Convex file storage operations.
25
+ * MUST be called during component setup (not in onMounted/callbacks).
26
+ * @param api - The Convex API from `#convex/api`
27
+ */
28
+ export declare function useConvexStorage(api: ConvexStorageApi): ConvexStorageReturn;
@@ -0,0 +1,34 @@
1
+ import { inject, readonly, ref } from "#imports";
2
+ import { CONVEX_INJECTION_KEY, useConvexQuery } from "@convex-vue/core";
3
+ export function useConvexStorage(api) {
4
+ const storage = api?._hub?.storage;
5
+ if (!storage) {
6
+ console.warn("[nuxt-convex] Storage API not found. Ensure convex/_hub/storage.ts exists and `npx convex dev` is running.");
7
+ }
8
+ const client = inject(CONVEX_INJECTION_KEY);
9
+ if (!client) {
10
+ console.warn("[nuxt-convex] Convex client not found. Is the plugin installed?");
11
+ }
12
+ return {
13
+ generateUploadUrl: {
14
+ async mutate() {
15
+ if (!client || !storage?.generateUploadUrl)
16
+ throw new Error("[nuxt-convex] Storage API not configured");
17
+ return await client.mutation(storage.generateUploadUrl, {});
18
+ }
19
+ },
20
+ getUrl: (storageId) => {
21
+ if (!storage?.getUrl)
22
+ return readonly(ref(null));
23
+ const { data } = useConvexQuery(storage.getUrl, { storageId });
24
+ return readonly(data);
25
+ },
26
+ remove: {
27
+ async mutate(args) {
28
+ if (!client || !storage?.remove)
29
+ throw new Error("[nuxt-convex] Storage API not configured");
30
+ return await client.mutation(storage.remove, args);
31
+ }
32
+ }
33
+ };
34
+ }
@@ -0,0 +1,32 @@
1
+ import type { DeepReadonly, Ref } from 'vue';
2
+ export interface UseConvexUploadOptions {
3
+ /** Mutation to generate upload URL (from useConvexStorage or custom) */
4
+ generateUploadUrl: {
5
+ mutate: (args?: any) => Promise<string | undefined>;
6
+ };
7
+ /** Called on successful upload with storageId and file */
8
+ onSuccess?: (storageId: string, file: File) => void;
9
+ /** Called on upload error */
10
+ onError?: (error: Error) => void;
11
+ }
12
+ export interface UseConvexUploadReturn {
13
+ upload: (file: File) => Promise<string | null>;
14
+ isUploading: DeepReadonly<Ref<boolean>>;
15
+ progress: DeepReadonly<Ref<number>>;
16
+ error: DeepReadonly<Ref<Error | null>>;
17
+ }
18
+ /**
19
+ * Composable for uploading files to Convex storage.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const { generateUploadUrl } = useConvexStorage()
24
+ * const { upload, isUploading, error } = useConvexUpload({
25
+ * generateUploadUrl,
26
+ * onSuccess: (id) => console.log('Uploaded:', id)
27
+ * })
28
+ *
29
+ * const handleFile = (file: File) => upload(file)
30
+ * ```
31
+ */
32
+ export declare function useConvexUpload(options: UseConvexUploadOptions): UseConvexUploadReturn;
@@ -0,0 +1,45 @@
1
+ import { readonly, ref } from "#imports";
2
+ export function useConvexUpload(options) {
3
+ const _isUploading = ref(false);
4
+ const _progress = ref(0);
5
+ const _error = ref(null);
6
+ async function upload(file) {
7
+ if (import.meta.server) {
8
+ console.warn("[nuxt-convex] useConvexUpload can only be used on client-side");
9
+ return null;
10
+ }
11
+ _isUploading.value = true;
12
+ _progress.value = 0;
13
+ _error.value = null;
14
+ try {
15
+ const uploadUrl = await options.generateUploadUrl.mutate({});
16
+ if (!uploadUrl)
17
+ throw new Error("[nuxt-convex] Failed to generate upload URL");
18
+ const response = await fetch(uploadUrl, {
19
+ method: "POST",
20
+ headers: { "Content-Type": file.type },
21
+ body: file
22
+ });
23
+ if (!response.ok) {
24
+ throw new Error(`[nuxt-convex] Upload failed: ${response.statusText}`);
25
+ }
26
+ const { storageId } = await response.json();
27
+ _progress.value = 100;
28
+ options.onSuccess?.(storageId, file);
29
+ return storageId;
30
+ } catch (e) {
31
+ const error = e;
32
+ _error.value = error;
33
+ options.onError?.(error);
34
+ return null;
35
+ } finally {
36
+ _isUploading.value = false;
37
+ }
38
+ }
39
+ return {
40
+ upload,
41
+ isUploading: readonly(_isUploading),
42
+ progress: readonly(_progress),
43
+ error: readonly(_error)
44
+ };
45
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,13 @@
1
+ import { defineNuxtPlugin, useRuntimeConfig } from "#imports";
2
+ import { createConvexVue } from "@convex-vue/core";
3
+ export default defineNuxtPlugin((nuxtApp) => {
4
+ const config = useRuntimeConfig();
5
+ const convexUrl = config.public.convex?.url;
6
+ if (!convexUrl) {
7
+ console.warn("[nuxt-convex] No Convex URL configured. Set CONVEX_URL env var or run `npx convex dev`");
8
+ return;
9
+ }
10
+ const convex = createConvexVue({ convexUrl });
11
+ nuxtApp.vueApp.use(convex);
12
+ return { provide: { convex } };
13
+ });
@@ -0,0 +1,9 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module.mjs'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module.mjs'
8
+
9
+ export { type ConvexConfig, type ResolvedConvexConfig } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "nuxt-convex",
3
+ "type": "module",
4
+ "version": "0.0.1-alpha.0",
5
+ "packageManager": "pnpm@10.15.1",
6
+ "description": "Nuxt module for Convex - reactive backend with real-time sync, file storage, and auto-imports",
7
+ "author": "onmax",
8
+ "license": "MIT",
9
+ "homepage": "https://github.com/onmax/nuxt-convex#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/onmax/nuxt-convex.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/onmax/nuxt-convex/issues"
16
+ },
17
+ "keywords": [
18
+ "nuxt",
19
+ "nuxt-module",
20
+ "convex",
21
+ "realtime",
22
+ "database",
23
+ "storage",
24
+ "vue"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/types.d.mts",
29
+ "import": "./dist/module.mjs"
30
+ }
31
+ },
32
+ "main": "./dist/module.mjs",
33
+ "types": "./dist/types.d.mts",
34
+ "typesVersions": {
35
+ "*": {
36
+ ".": [
37
+ "./dist/types.d.mts"
38
+ ]
39
+ }
40
+ },
41
+ "files": [
42
+ "dist"
43
+ ],
44
+ "scripts": {
45
+ "prepack": "nuxt-module-build build",
46
+ "dev": "nuxi dev playground",
47
+ "dev:build": "nuxi build playground",
48
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare",
49
+ "playground:prepare": "nuxi prepare playground",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "typecheck": "nuxt-module-build build",
53
+ "lint": "eslint .",
54
+ "lint:fix": "eslint . --fix",
55
+ "release": "bumpp && git push --follow-tags"
56
+ },
57
+ "peerDependencies": {
58
+ "@convex-vue/core": ">=0.0.4",
59
+ "convex": ">=1.0.0"
60
+ },
61
+ "dependencies": {
62
+ "@nuxt/kit": "catalog:prod",
63
+ "consola": "catalog:prod",
64
+ "defu": "catalog:prod",
65
+ "pathe": "catalog:prod"
66
+ },
67
+ "devDependencies": {
68
+ "@antfu/eslint-config": "catalog:lint",
69
+ "@convex-vue/core": "catalog:convex",
70
+ "@nuxt/devtools": "catalog:nuxt",
71
+ "@nuxt/eslint-config": "catalog:lint",
72
+ "@nuxt/module-builder": "catalog:build",
73
+ "@nuxt/test-utils": "catalog:test",
74
+ "@types/node": "catalog:types",
75
+ "bumpp": "^10.3.2",
76
+ "convex": "catalog:convex",
77
+ "eslint": "catalog:lint",
78
+ "eslint-plugin-format": "catalog:lint",
79
+ "nuxt": "catalog:nuxt",
80
+ "typescript": "catalog:build",
81
+ "vitest": "catalog:test",
82
+ "vitest-package-exports": "catalog:test",
83
+ "vue": "catalog:nuxt",
84
+ "yaml": "catalog:"
85
+ }
86
+ }