This project demonstrates a Vue application integrated with Convex as the backend, using @convex-dev/auth (Password provider) with reactive queries for real-time data synchronization. It provides a complete authentication solution with automatic device detection and secure storage.
- Vue 3 + Vite + Capacitor + TypeScript
- Convex backend (database, functions, HTTP router)
@convex-dev/authwith Password provider@convex-vue/corefor reactive queries and mutations- Capacitor for mobile app support
- Reactive Queries - Real-time data synchronization across devices
- JWT Authentication - Client-side JWT with
ConvexClient.setAuth - Configurable Storage - localStorage (web) or Capacitor secure storage (mobile)
- Automatic Device Detection - Platform-specific storage configuration
- Real-time Updates - UI automatically updates when data changes
- Mobile Support - iOS (Keychain) and Android (Encrypted SharedPreferences)
- TypeScript - Full type safety throughout the application
- Install
npm install npm install convex @convex-dev/auth @convex-vue/core # For mobile apps with Capacitor secure storage (optional) npm install @capacitor/preferences @capacitor/device- Environment
Copy the example environment file and configure it:
# Copy the example environment file cp env.example.txt .env.local # Edit .env.local with your actual valuesThe .env.local file should contain:
# Convex deployment URLs CONVEX_SITE_URL=https://<your-deployment>.convex.cloud VITE_CONVEX_URL=https://<your-deployment>.convex.cloud # JWT Private Key (REQUIRED for authentication) # Generate with: npx convex auth generate-keys JWT_PRIVATE_KEY=your-jwt-private-key-hereImportant: The JWT_PRIVATE_KEY is required for the Password provider to sign JWT tokens. Generate it using:
npx convex auth generate-keys- Generate JWT Keys (REQUIRED)
The Password provider requires JWT keys for token signing. Generate them:
npx convex auth generate-keysThis will:
- Generate a
JWT_PRIVATE_KEYandJWKS(JSON Web Key Set) - Automatically add them to your Convex deployment
- Update your
.env.localfile with the private key
- Run Convex once to generate types and deploy functions
npx convex dev --once- Start dev servers
npm run devOpen the app, sign up/sign in, verify state persists across refresh, and sign out.
convex/schema.ts: useauthTables, optionally overrideusersandauthAccountsindexes.convex/auth.ts: configureconvexAuth({ providers: [Password] }), exportauth,signIn,signOut,isAuthenticated, and agetUserquery.convex/http.ts: callauth.addHttpRoutes(http).
Core files:
// convex/auth.ts import { convexAuth } from '@convex-dev/auth/server' import { Password } from '@convex-dev/auth/providers/Password' import { query } from './_generated/server' import { getAuthUserId } from '@convex-dev/auth/server' export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ providers: [Password], }) export const getUser = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx) if (!userId) return null return await ctx.db.get(userId) }, })// convex/http.ts import { httpRouter } from 'convex/server' import { auth } from './auth' const http = httpRouter() auth.addHttpRoutes(http) export default httpsrc/composables/useConvexAuth.ts- Main auth composable with JWT managementsrc/composables/useConvexVue.ts- Reactive query integration with @convex-vue/coresrc/composables/useCapacitorAuth.ts- Capacitor secure storage for mobile appssrc/App.vue- Root component with auth state managementsrc/components/Content.vue- Main content with reactive queries
- Reactive Queries: Real-time data synchronization using
@convex-vue/core - JWT Authentication: Client-side JWT with
ConvexClient.setAuth() - Configurable Storage: localStorage (web) or Capacitor secure storage (mobile)
- Automatic Updates: UI updates automatically when data changes
- Device Detection: Platform-specific storage configuration
vue-convex-auth/ βββ env.example.txt # Example environment file (copy to .env.local) βββ src/ β βββ composables/ β β βββ useConvexAuth.ts # Main authentication composable β β βββ useConvexVue.ts # Reactive query integration β β βββ useCapacitorAuth.ts # Capacitor storage integration β βββ components/ β β βββ Content.vue # Main content with reactive queries β β βββ SignInForm.vue # Authentication form β β βββ SignOutButton.vue # Sign out button β βββ App.vue # Root component βββ convex/ β βββ auth.ts # Authentication configuration β βββ schema.ts # Database schema β βββ myFunctions.ts # Queries and mutations βββ README.md # This file Default (localStorage):
import { useConvexAuth } from './composables/useConvexAuth' const { initializeAuth } = useConvexAuth() onMounted(() => { initializeAuth() })Mobile apps with Capacitor secure storage:
import { setupCapacitorAuth } from './composables/useCapacitorAuth' onMounted(async () => { const { initializeAuth } = await setupCapacitorAuth() await initializeAuth() })Custom storage:
import { useConvexAuth } from './composables/useConvexAuth' const { configureAuthStorage, initializeAuth } = useConvexAuth() // Configure your custom storage configureAuthStorage({ async getItem(key) { /* your logic */ }, async setItem(key, value) { /* your logic */ }, async removeItem(key) { /* your logic */ }, }) onMounted(() => { initializeAuth() })Important keys (matching React provider):
__convexAuthJWT__convexAuthRefreshToken
This project uses @convex-vue/core for reactive data management, providing real-time synchronization across devices.
// In your Vue component import { useQuery, useMutation } from './composables/useConvexVue' import { api } from '../convex/_generated/api' export default { setup() { // Reactive query - automatically updates when data changes const { data: numbers, isLoading } = useQuery(api.myFunctions.listNumbers, { count: 10 }) // Reactive mutation - automatically updates related queries const addNumber = useMutation(api.myFunctions.addNumber) const handleAddNumber = async () => { await addNumber.mutate({ value: Math.random() * 10 }) // No manual refresh needed - the query updates automatically! } return { numbers, isLoading, handleAddNumber } }, }- Real-time Sync: Changes appear instantly across all connected devices
- Automatic Updates: No manual refresh needed after mutations
- Optimistic Updates: UI updates immediately for better UX
- Error Handling: Built-in error states and retry logic
- Type Safety: Full TypeScript support with generated types
flowchart TB subgraph "π Browser (Vue.js Frontend)" subgraph "π± Vue Components (Custom)" APP[App.vue<br/>Root Component] CONTENT[Content.vue<br/>Main Content with Reactive Queries] SIGNIN[SignInForm.vue<br/>Authentication Form] SIGNOUT[SignOutButton.vue<br/>Sign Out Button] end subgraph "π§ Custom Composables" AUTH[useConvexAuth.ts<br/>JWT Authentication Management] VUE[useConvexVue.ts<br/>Reactive Query Integration] CAPACITOR[useCapacitorAuth.ts<br/>Mobile Storage Integration] end subgraph "πΎ Storage Layer" LOCAL[localStorage<br/>Web Browser] SECURE[Capacitor Secure Storage<br/>iOS Keychain / Android Encrypted] CUSTOM[Custom Storage<br/>Implement AuthStorage Interface] end end subgraph "βοΈ Convex Backend" subgraph "π Authentication (Convex Provided)" AUTHFN[convex/auth.ts<br/>Password Provider] HTTP[convex/http.ts<br/>HTTP Routes] AUTHCONFIG[auth.config.ts<br/>JWT Configuration] end subgraph "π Data Layer (Convex Provided)" SCHEMA[convex/schema.ts<br/>Database Schema] QUERYFN[convex/myFunctions.ts<br/>Custom Queries & Mutations] DB[(Convex Database<br/>Real-time Sync)] end subgraph "π JWT Infrastructure (Convex Provided)" JWT[JWT Token Signing<br/>JWT_PRIVATE_KEY] JWKS[JWKS Endpoint<br/>Public Key Verification] end end subgraph "π± Mobile Platform (Capacitor)" DEVICE[Device Detection<br/>iOS/Android/Web] KEYCHAIN[iOS Keychain<br/>Secure Enclave] SHARED[Android SharedPreferences<br/>Encrypted Storage] end %% Component Relationships APP --> AUTH APP --> CONTENT CONTENT --> VUE SIGNIN --> AUTH SIGNOUT --> AUTH %% Storage Integration AUTH --> LOCAL AUTH --> SECURE AUTH --> CUSTOM CAPACITOR --> DEVICE DEVICE --> KEYCHAIN DEVICE --> SHARED %% Backend Communication AUTH <--> AUTHFN VUE <--> QUERYFN AUTHFN <--> HTTP AUTHFN <--> JWT QUERYFN <--> DB SCHEMA -.-> DB HTTP -.-> AUTHFN %% Styling classDef convexProvided fill:#e1f5fe,stroke:#01579b,stroke-width:2px classDef customBuilt fill:#f3e5f5,stroke:#4a148c,stroke-width:2px classDef storage fill:#fff3e0,stroke:#e65100,stroke-width:2px classDef mobile fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px class AUTHFN,HTTP,AUTHCONFIG,SCHEMA,QUERYFN,DB,JWT,JWKS convexProvided class APP,CONTENT,SIGNIN,SIGNOUT,AUTH,VUE,CAPACITOR customBuilt class LOCAL,SECURE,CUSTOM storage class DEVICE,KEYCHAIN,SHARED mobile App.vue- Root component with auth state managementContent.vue- Main content with reactive queries and real-time updatesSignInForm.vue- Authentication form with email/password inputsSignOutButton.vue- Sign out functionality
useConvexAuth.ts- JWT authentication management, token storage, refresh logicuseConvexVue.ts- Integration layer for@convex-vue/corereactive queriesuseCapacitorAuth.ts- Mobile device detection and secure storage integration
env.example.txt- Environment template for easy setupmain.ts- Convex Vue plugin initialization
@convex-dev/auth- Password provider, JWT signing, session managementconvex/auth.ts- Server-side auth functions (signIn, signOut, getUser)convex/http.ts- HTTP routes for authentication endpointsconvex/auth.config.ts- JWT configuration
convex/schema.ts- Database schema with auth tablesconvex/myFunctions.ts- Custom queries and mutations- Convex Database - Real-time synchronized database
@convex-vue/core- Reactive query system for Vue
- JWT Token Signing - Server-side token generation
- JWKS Endpoint - Public key verification
- Token Validation - Automatic token verification
@capacitor/device- Device detection (iOS/Android/Web)@capacitor/preferences- Secure storage APIs- iOS Keychain - Secure token storage on iOS
- Android SharedPreferences - Encrypted storage on Android
- Vue 3 - Frontend framework
- Vite - Build tool and dev server
- TypeScript - Type safety
sequenceDiagram participant U as User participant V as Vue Component participant A as useConvexAuth participant Q as useConvexVue participant C as ConvexClient participant S as Convex Server participant D as Database Note over U,D: Authentication Flow U->>V: Sign In V->>A: signIn(email, password) A->>C: action(api.auth.signIn) C->>S: HTTP Request S->>D: Create/Validate User S-->>C: JWT Tokens C-->>A: Tokens A->>A: setAuth(token) A->>A: Store in localStorage/Capacitor A-->>V: isAuthenticated = true Note over U,D: Reactive Query Flow V->>Q: useQuery(api.myFunctions.listNumbers) Q->>C: Subscribe to Query C->>S: WebSocket Connection S->>D: Query Data D-->>S: Initial Data S-->>C: Real-time Updates C-->>Q: Reactive Data Q-->>V: UI Updates Automatically Note over U,D: Mutation Flow U->>V: Add Number V->>Q: useMutation(api.myFunctions.addNumber) Q->>C: Execute Mutation C->>S: Mutation Request S->>D: Update Database D-->>S: Updated Data S-->>C: Real-time Update C-->>Q: Query Auto-Refreshes Q-->>V: UI Updates Instantly sequenceDiagram participant U as User participant VC as Vue (useConvexAuth) participant C as ConvexClient participant A as Convex auth:signIn participant DB as Convex DB U->>VC: signIn(email, password, flow) VC->>C: action(api.auth.signIn, { provider:"password", params }) C->>A: Execute action A->>DB: create/retrieve account, create session A-->>C: { tokens: { token, refreshToken } } C-->>VC: tokens VC->>VC: setAuth(() => token) VC->>VC: storage.setItem(JWT, token) and setItem(refresh) VC->>C: query(api.auth.getUser) C-->>VC: user doc VC-->>U: isAuthenticated=true, user populated Since we're using @convex-dev/auth (which is built on Auth.js), we have access to a comprehensive suite of authentication providers beyond the Password provider we're currently using.
// convex/auth.ts import { convexAuth } from '@convex-dev/auth/server' import { Password } from '@convex-dev/auth/providers/Password' import { Google } from '@convex-dev/auth/providers/Google' import { GitHub } from '@convex-dev/auth/providers/GitHub' import { Apple } from '@convex-dev/auth/providers/Apple' import { Discord } from '@convex-dev/auth/providers/Discord' import { Microsoft } from '@convex-dev/auth/providers/Microsoft' import { Facebook } from '@convex-dev/auth/providers/Facebook' import { Twitter } from '@convex-dev/auth/providers/Twitter' export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ providers: [ Password, Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }), GitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }), Apple({ clientId: process.env.APPLE_CLIENT_ID!, clientSecret: process.env.APPLE_CLIENT_SECRET!, }), Discord({ clientId: process.env.DISCORD_CLIENT_ID!, clientSecret: process.env.DISCORD_CLIENT_SECRET!, }), ], })import { Email } from '@convex-dev/auth/providers/Email' export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ providers: [ Password, Email({ server: { host: process.env.EMAIL_SERVER_HOST, port: process.env.EMAIL_SERVER_PORT, auth: { user: process.env.EMAIL_SERVER_USER, pass: process.env.EMAIL_SERVER_PASSWORD, }, }, from: process.env.EMAIL_FROM, }), ], })import { WebAuthn } from '@convex-dev/auth/providers/WebAuthn' export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ providers: [ Password, WebAuthn({ rpName: 'Your App Name', rpID: process.env.WEBAUTHN_RP_ID, origin: process.env.WEBAUTHN_ORIGIN, }), ], })// Add roles to your schema export default defineSchema({ users: defineTable({ name: v.string(), email: v.string(), role: v.union(v.literal('admin'), v.literal('user'), v.literal('moderator')), }), }) // Check roles in queries/mutations export const adminOnlyQuery = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx) if (!userId) throw new Error('Not authenticated') const user = await ctx.db.get(userId) if (user?.role !== 'admin') throw new Error('Admin access required') // Admin-only logic here }, })import { TOTP } from '@convex-dev/auth/providers/TOTP' export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ providers: [ Password, TOTP({ issuer: 'Your App Name', }), ], })import { CustomProvider } from '@convex-dev/auth/providers/Custom' const customProvider = CustomProvider({ id: 'custom', name: 'Custom Provider', type: 'oauth', authorization: 'https://your-provider.com/oauth/authorize', token: 'https://your-provider.com/oauth/token', userinfo: 'https://your-provider.com/oauth/userinfo', clientId: process.env.CUSTOM_CLIENT_ID, clientSecret: process.env.CUSTOM_CLIENT_SECRET, })# .env.local # Existing CONVEX_SITE_URL=https://your-deployment.convex.cloud VITE_CONVEX_URL=https://your-deployment.convex.cloud JWT_PRIVATE_KEY=your-jwt-private-key # OAuth Providers GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret GITHUB_CLIENT_ID=your-github-client-id GITHUB_CLIENT_SECRET=your-github-client-secret APPLE_CLIENT_ID=your-apple-client-id APPLE_CLIENT_SECRET=your-apple-client-secret DISCORD_CLIENT_ID=your-discord-client-id DISCORD_CLIENT_SECRET=your-discord-client-secret # Magic Links EMAIL_SERVER_HOST=smtp.gmail.com EMAIL_SERVER_PORT=587 EMAIL_SERVER_USER=your-email@gmail.com EMAIL_SERVER_PASSWORD=your-app-password EMAIL_FROM=noreply@yourapp.com # WebAuthn WEBAUTHN_RP_ID=yourdomain.com WEBAUTHN_ORIGIN=https://yourdomain.com// In your Vue components const { signIn } = useConvexAuth() // OAuth sign-in const signInWithGoogle = () => { signIn('google') } const signInWithGitHub = () => { signIn('github') } // Magic link sign-in const signInWithEmail = (email: string) => { signIn('email', { email }) } // WebAuthn sign-in const signInWithPasskey = () => { signIn('webauthn') }| Provider | Type | Setup Complexity | User Experience | Security Level |
|---|---|---|---|---|
| Password | Credentials | β Easy | ββ Good | ββ Good |
| OAuth | ββ Medium | βββ Excellent | βββ Excellent | |
| GitHub | OAuth | ββ Medium | βββ Excellent | βββ Excellent |
| Apple | OAuth | βββ Hard | βββ Excellent | βββ Excellent |
| Magic Links | Passwordless | ββ Medium | βββ Excellent | βββ Excellent |
| WebAuthn | Passwordless | βββ Hard | βββ Excellent | βββ Excellent |
- On app init: restore JWT/refresh from configured storage; set
setAuth(() => JWT). - On 401/Unauthorized: call a small refresh helper that exchanges the refresh token for a new JWT (via
auth:signInwithrefreshToken), update storage andsetAuth, then retry once.
- Make sure
CONVEX_SITE_URLandVITE_CONVEX_URLpoint to the same Convex deployment URL reachable by the browser. - JWT_PRIVATE_KEY is REQUIRED - Generate it with
npx convex auth generate-keysbefore running the app. - Password flows supported:
signUp,signIn(add reset flows if needed per provider docs). - If tokens aren't in storage after sign-in, check browser console logs from the composable.
- For mobile apps, install Capacitor packages and use
setupCapacitorAuth()for automatic device detection. - Storage is configurable - use
configureAuthStorage()for custom implementations.
Error: "JWT_PRIVATE_KEY is required"
- Run
npx convex auth generate-keysto generate the required JWT keys - Ensure the key is properly set in your Convex deployment environment
Error: "Unauthorized" or "Invalid token"
- Check that
JWT_PRIVATE_KEYis correctly configured in your Convex deployment - Verify the key hasn't been regenerated (regenerating invalidates existing tokens)
- Clear browser storage and try signing in again
Sign-in fails silently
- Check browser console for error messages
- Verify your Convex deployment is running (
npx convex dev) - Ensure all environment variables are correctly set
- Platform: Web browsers
- Security: Standard web storage
- Use case: Web applications, development
- Platform: iOS, Android, Web
- Security:
- iOS: Keychain (secure storage)
- Android: Encrypted SharedPreferences
- Web: localStorage fallback
- Use case: Mobile applications with Capacitor
- Installation:
npm install @capacitor/preferences @capacitor/device
- Platform: Any
- Security: Depends on implementation
- Use case: Special requirements, custom backends
- Implementation: Implement
AuthStorageinterface
npm run dev # Vite + Convex in parallel (check package.json) npx convex dev # Start Convex backend npx convex dev --once # One-shot compile/deployMIT