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/auth
with Password provider@convex-vue/core
for 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 values
The .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-here
Important: 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-keys
This will:
- Generate a
JWT_PRIVATE_KEY
andJWKS
(JSON Web Key Set) - Automatically add them to your Convex deployment
- Update your
.env.local
file with the private key
- Run Convex once to generate types and deploy functions
npx convex dev --once
- Start dev servers
npm run dev
Open the app, sign up/sign in, verify state persists across refresh, and sign out.
convex/schema.ts
: useauthTables
, optionally overrideusers
andauthAccounts
indexes.convex/auth.ts
: configureconvexAuth({ providers: [Password] })
, exportauth
,signIn
,signOut
,isAuthenticated
, and agetUser
query.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 http
src/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/core
reactive 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 protected]
EMAIL_SERVER_PASSWORD=your-app-password
[email protected]
# 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:signIn
withrefreshToken
), update storage andsetAuth
, then retry once.
- Make sure
CONVEX_SITE_URL
andVITE_CONVEX_URL
point to the same Convex deployment URL reachable by the browser. - JWT_PRIVATE_KEY is REQUIRED - Generate it with
npx convex auth generate-keys
before 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-keys
to 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_KEY
is 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
AuthStorage
interface
npm run dev # Vite + Convex in parallel (check package.json)
npx convex dev # Start Convex backend
npx convex dev --once # One-shot compile/deploy
MIT