Skip to content

aaronksaunders/vue-convex-auth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Vue + Convex Auth (Password) – Full Integration Guide

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.

Stack

  • 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

Features

  • 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

Quick Start

  1. 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
  1. 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
  1. 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 and JWKS (JSON Web Key Set)
  • Automatically add them to your Convex deployment
  • Update your .env.local file with the private key
  1. Run Convex once to generate types and deploy functions
npx convex dev --once
  1. Start dev servers
npm run dev

Open the app, sign up/sign in, verify state persists across refresh, and sign out.

Server Changes (Convex)

  • convex/schema.ts: use authTables, optionally override users and authAccounts indexes.
  • convex/auth.ts: configure convexAuth({ providers: [Password] }), export auth, signIn, signOut, isAuthenticated, and a getUser query.
  • convex/http.ts: call auth.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

Client Changes (Vue)

Core Architecture

  • src/composables/useConvexAuth.ts - Main auth composable with JWT management
  • src/composables/useConvexVue.ts - Reactive query integration with @convex-vue/core
  • src/composables/useCapacitorAuth.ts - Capacitor secure storage for mobile apps
  • src/App.vue - Root component with auth state management
  • src/components/Content.vue - Main content with reactive queries

Key Features

  • 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

Project Structure

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

Storage Configuration

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

Reactive Queries

This project uses @convex-vue/core for reactive data management, providing real-time synchronization across devices.

Usage Example

// 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 }
  },
}

Key Benefits

  • 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

Architecture Diagram

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
Loading

Component Breakdown

🟣 Custom Components (What We Built)

Vue Components

  • App.vue - Root component with auth state management
  • Content.vue - Main content with reactive queries and real-time updates
  • SignInForm.vue - Authentication form with email/password inputs
  • SignOutButton.vue - Sign out functionality

Custom Composables

  • useConvexAuth.ts - JWT authentication management, token storage, refresh logic
  • useConvexVue.ts - Integration layer for @convex-vue/core reactive queries
  • useCapacitorAuth.ts - Mobile device detection and secure storage integration

Configuration Files

  • env.example.txt - Environment template for easy setup
  • main.ts - Convex Vue plugin initialization

πŸ”΅ Convex-Provided Components

Authentication Infrastructure

  • @convex-dev/auth - Password provider, JWT signing, session management
  • convex/auth.ts - Server-side auth functions (signIn, signOut, getUser)
  • convex/http.ts - HTTP routes for authentication endpoints
  • convex/auth.config.ts - JWT configuration

Database & Real-time

  • convex/schema.ts - Database schema with auth tables
  • convex/myFunctions.ts - Custom queries and mutations
  • Convex Database - Real-time synchronized database
  • @convex-vue/core - Reactive query system for Vue

JWT Infrastructure

  • JWT Token Signing - Server-side token generation
  • JWKS Endpoint - Public key verification
  • Token Validation - Automatic token verification

🟠 Third-Party Integrations

Capacitor (Mobile)

  • @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 Ecosystem

  • Vue 3 - Frontend framework
  • Vite - Build tool and dev server
  • TypeScript - Type safety

Data Flow Diagram

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
Loading

Sign-in Sequence (Password)

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
Loading

Available Authentication Providers

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.

πŸ” OAuth Providers (Social Login)

// 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!,
    }),
  ],
})

πŸ“§ Magic Links (Passwordless)

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,
    }),
  ],
})

πŸ”‘ WebAuthn (Passkeys/Biometric)

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,
    }),
  ],
})

πŸ›‘οΈ Advanced Features

Role-Based Access Control (RBAC)

// 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
  },
})

Multi-Factor Authentication (MFA)

import { TOTP } from '@convex-dev/auth/providers/TOTP'

export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
  providers: [
    Password,
    TOTP({
      issuer: 'Your App Name',
    }),
  ],
})

Custom Providers

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,
})

πŸ”§ Configuration Examples

Environment Variables for OAuth

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

Frontend Integration

// 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 Comparison

Provider Type Setup Complexity User Experience Security Level
Password Credentials ⭐ Easy ⭐⭐ Good ⭐⭐ Good
Google 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

Token Refresh Strategy

  • 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 with refreshToken), update storage and setAuth, then retry once.

Development Tips

  • Make sure CONVEX_SITE_URL and VITE_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.

Troubleshooting

Authentication Issues

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

Storage Options

localStorage (Default)

  • Platform: Web browsers
  • Security: Standard web storage
  • Use case: Web applications, development

Capacitor Secure Storage (Mobile)

  • 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

Custom Storage

  • Platform: Any
  • Security: Depends on implementation
  • Use case: Special requirements, custom backends
  • Implementation: Implement AuthStorage interface

Scripts

npm run dev        # Vite + Convex in parallel (check package.json)
npx convex dev     # Start Convex backend
npx convex dev --once  # One-shot compile/deploy

License

MIT

About

No description or website provided.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published