Retour aux skills
clerk/skillsContrôle réussi

SKILL DETAIL

clerk-vue-patterns

clerk/skills/clerk-vue-patterns

Vue 3 patterns with Clerk — composables (useAuth, useUser,

Installations · 97Voir la source

Installation

npx skills add https://github.com/clerk/skills --skill clerk-vue-patterns

Fichiers du skill

SKILL.md

Dernière synchronisation · 6 sept. 2026

evals/evals.json›
{
  "skill_name": "clerk-vue-patterns",
  "evals": [
    {
      "id": 1,
      "prompt": "show me how to use clerk composables in a vue 3 component to display the user's name and a sign-out button.",
      "expected_output": "Uses useAuth and useUser composables from @clerk/vue, shows user.firstName, calls signOut()",
      "scaffold": "vue-basic-auth",
      "expectations": [
        "Imports useAuth and useUser from @clerk/vue",
        "Uses isSignedIn from useAuth to conditionally render",
        "Accesses user.firstName or user.fullName from useUser",
        "Calls signOut() from useAuth on button click",
        "Handles the case where user is not signed in"
      ]
    },
    {
      "id": 2,
      "prompt": "i want to build a custom sign-in form in vue using clerk's useSignIn composable. show the email/password flow.",
      "expected_output": "Uses useSignIn composable, calls signIn.create with identifier and password, sets active session after success",
      "scaffold": "vue-basic-auth",
      "expectations": [
        "Imports useSignIn from @clerk/vue",
        "Destructures signIn and setActive from useSignIn",
        "Calls signIn.create({ identifier, password }) on form submit",
        "Calls setActive({ session: signIn.createdSessionId }) after successful sign-in",
        "Handles errors from the sign-in attempt"
      ]
    },
    {
      "id": 3,
      "prompt": "i want a pinia store that syncs with clerk auth state. the store should expose isAuthenticated and userId, and update reactively when the user signs in or out.",
      "expected_output": "Creates a Pinia store that uses useAuth composable, exposes isSignedIn and userId as computed values",
      "scaffold": "vue-basic-auth",
      "expectations": [
        "Creates a Pinia store using defineStore",
        "Calls useAuth inside the store setup function",
        "Exposes isSignedIn (or renamed isAuthenticated) from useAuth",
        "Exposes userId from useAuth",
        "Does NOT duplicate auth state — wraps useAuth reactivity rather than copying it"
      ]
    }
  ]
}
references/composables.md›
# Composables

All composables come from `@clerk/vue`.

## useAuth

```ts
import { useAuth } from '@clerk/vue'

const { isSignedIn, isLoaded, userId, sessionId, signOut, getToken } = useAuth()
```

| Property | Type | Description |
|----------|------|-------------|
| `isSignedIn` | `Ref<boolean>` | True when user is authenticated |
| `isLoaded` | `Ref<boolean>` | True when Clerk has initialized |
| `userId` | `Ref<string \| null>` | Current user ID |
| `sessionId` | `Ref<string \| null>` | Current session ID |
| `signOut()` | `function` | Sign out and clear session |
| `getToken()` | `async function` | Get JWT for external APIs |

## useUser

```ts
import { useUser } from '@clerk/vue'

const { user, isLoaded } = useUser()
```

`user` is `Ref<UserResource | null>` with `.firstName`, `.lastName`, `.fullName`, `.imageUrl`, `.primaryEmailAddress`, etc.

## useClerk

```ts
import { useClerk } from '@clerk/vue'

const clerk = useClerk()
await clerk.value.openSignIn()
await clerk.value.openUserProfile()
```

Use for programmatic UI control (open modals, redirect, etc.).

## useOrganization

```ts
import { useOrganization } from '@clerk/vue'

const { organization, membership, isLoaded } = useOrganization()
```

`organization` is `Ref<Organization | null>`. `membership` includes `role`.

## useOrganizationList

```ts
import { useOrganizationList } from '@clerk/vue'

const { userMemberships, setActive } = useOrganizationList()
```

`userMemberships.data` is an array of memberships. Call `setActive({ organization: id })` to switch.

## useSignIn / useSignUp

```ts
import { useSignIn } from '@clerk/vue'

const { signIn, setActive, isLoaded } = useSignIn()

async function submit(email: string, password: string) {
  const result = await signIn.value!.create({
    identifier: email,
    password,
  })
  if (result.status === 'complete') {
    await setActive.value!({ session: result.createdSessionId })
  }
}
```

## CRITICAL

- All composables return refs — access values with `.value` outside of templates
- Check `isLoaded` before rendering auth-gated UI to prevent flashes
- `useOrganizationList` returns `userMemberships` as a paginated resource — access `.data` for the array
references/pinia-integration.md›
# Pinia Integration

## Auth Store (Thin Wrapper)

Wrap `useAuth` in a Pinia store for app-wide access without prop drilling:

```ts
// stores/auth.ts
import { defineStore } from 'pinia'
import { useAuth, useUser } from '@clerk/vue'

export const useAuthStore = defineStore('auth', () => {
  const { isSignedIn, isLoaded, userId, signOut, getToken } = useAuth()
  const { user } = useUser()

  return {
    isSignedIn,
    isLoaded,
    userId,
    user,
    signOut,
    getToken,
  }
})
```

Usage in components:

```vue
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth'

const auth = useAuthStore()
</script>

<template>
  <p v-if="auth.isSignedIn">{{ auth.user?.firstName }}</p>
</template>
```

## Org Store

```ts
// stores/org.ts
import { defineStore } from 'pinia'
import { useOrganization, useOrganizationList } from '@clerk/vue'

export const useOrgStore = defineStore('org', () => {
  const { organization, membership } = useOrganization()
  const { userMemberships, setActive } = useOrganizationList()

  function switchOrg(orgId: string) {
    return setActive.value!({ organization: orgId })
  }

  return {
    organization,
    membership,
    userMemberships,
    switchOrg,
  }
})
```

## CRITICAL

- Do NOT copy `userId` into Pinia state manually — it creates a stale copy. Always return refs directly from composables
- Pinia store `setup()` function runs inside Vue's setup context — composables work correctly here
references/vue-router-guards.md›
# Vue Router Guards

For plain Vue (without Nuxt), protect routes using navigation guards.

## Global Before Guard

```ts
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/sign-in', component: SignIn },
    { path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } },
  ],
})

router.beforeEach(async (to) => {
  if (!to.meta.requiresAuth) return true

  const { userId } = useAuth()

  if (!userId.value) return '/sign-in'
  return true
})
```

## Per-Route Guard with In-Component Guard

```vue
<script setup lang="ts">
import { useAuth } from '@clerk/vue'
import { useRouter } from 'vue-router'
import { watchEffect } from 'vue'

const { isSignedIn, isLoaded } = useAuth()
const router = useRouter()

watchEffect(() => {
  if (isLoaded.value && !isSignedIn.value) {
    router.replace('/sign-in')
  }
})
</script>
```

## Org-Gated Route

```ts
router.beforeEach(async (to) => {
  if (!to.meta.requiresOrg) return true

  const { userId } = useAuth()
  const { organization } = useOrganization()

  if (!userId.value) return '/sign-in'
  if (!organization.value) return '/select-org'
  return true
})
```

## CRITICAL

- Call composables outside of the guard function body if possible — `useAuth()` is reactive and works outside components in Vue 3 (when called in setup context or at module level after plugin install)
- `isLoaded` must be true before trusting `isSignedIn` — guard may fire before Clerk initializes
- For Nuxt, prefer `middleware/` instead of manual router guards
SKILL.md›
---
name: clerk-vue-patterns
description: 'Vue 3 patterns with Clerk — composables (useAuth, useUser,
  useClerk, useOrganization), Vue Router guards, Pinia auth store
  integration. Triggers on: vue clerk, useAuth vue, clerk composables,
  vue router clerk guard, pinia auth clerk. For Nuxt, use clerk-nuxt-patterns instead.'
license: MIT
allowed-tools: WebFetch
metadata:
  author: clerk
  version: 1.0.0
---

# Vue Patterns

SDK: `@clerk/vue` v2+ (Vue 3). For Nuxt, use `clerk-nuxt-patterns`.

## What Do You Need?

| Task | Reference |
|------|-----------|
| Composables: useAuth, useUser, useOrganization | references/composables.md |
| Vue Router navigation guards | references/vue-router-guards.md |
| Pinia store with auth state | references/pinia-integration.md |

## Mental Model

Vue uses composables from `@clerk/vue`:

- **`useAuth()`** — reactive `isSignedIn`, `userId`, `signOut`
- **`useUser()`** — reactive `user` object
- **`useClerk()`** — full Clerk instance for advanced operations
- **`useOrganization()`** — reactive `organization`, `membership`

## Setup

### Vue (Plain)

```ts
// main.ts
import { clerkPlugin } from '@clerk/vue'
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.use(clerkPlugin, {
  publishableKey: import.meta.env.VITE_CLERK_PUBLISHABLE_KEY,
})
app.mount('#app')
```

## Composables Usage

```vue
<script setup lang="ts">
import { useAuth, useUser } from '@clerk/vue'

const { isSignedIn, userId, signOut } = useAuth()
const { user } = useUser()
</script>

<template>
  <div v-if="isSignedIn">
    <p>Hello {{ user?.firstName }}</p>
    <button @click="signOut()">Sign Out</button>
  </div>
  <SignInButton v-else />
</template>
```

## Org Switching

```vue
<script setup lang="ts">
import { useOrganizationList } from '@clerk/vue'

const { userMemberships, setActive } = useOrganizationList()
</script>

<template>
  <button
    v-for="mem in userMemberships.data ?? []"
    :key="mem.organization.id"
    @click="setActive({ organization: mem.organization.id })"
  >
    {{ mem.organization.name }}
  </button>
</template>
```

## Common Pitfalls

| Symptom | Cause | Fix |
|---------|-------|-----|
| Composables return `undefined` | Not inside `ClerkProvider` tree | Ensure `app.use(clerkPlugin, { publishableKey })` is called |
| `userId` reactive but not updating | Destructuring loses reactivity | Use `const { userId } = useAuth()` (toRefs-style composable, reactive) |
## Import Map

| What | Import |
|------|--------|
| Composables | `@clerk/vue` |
| Plugin setup | `@clerk/vue` |
| Components | `@clerk/vue` |

## See Also

- `clerk-setup` - Initial Clerk install
- `clerk-custom-ui` - Custom flows & appearance
- `clerk-orgs` - B2B organizations

## Docs

- [Vue SDK](https://clerk.com/docs/vue/getting-started/quickstart)
templates/vue-basic-auth/index.html›
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Clerk Vue</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
templates/vue-basic-auth/package.json›
{
  "name": "vue-basic-auth",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build"
  },
  "dependencies": {
    "vue": "latest",
    "@clerk/vue": "latest"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "latest",
    "typescript": "latest",
    "vite": "latest",
    "vue-tsc": "latest"
  }
}
templates/vue-basic-auth/src/App.vue›
<script setup lang="ts">
import { Show, SignInButton, SignUpButton, UserButton } from '@clerk/vue'
</script>

<template>
  <header>
    <Show when="signed-out">
      <SignInButton />
      <SignUpButton />
    </Show>
    <Show when="signed-in">
      <UserButton />
    </Show>
  </header>
</template>
templates/vue-basic-auth/src/main.ts›
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { clerkPlugin } from '@clerk/vue'

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY

if (!PUBLISHABLE_KEY) {
  throw new Error('Add your Clerk Publishable Key to the .env file')
}

const app = createApp(App)
app.use(clerkPlugin, { publishableKey: PUBLISHABLE_KEY })
app.mount('#app')
templates/vue-basic-auth/src/style.css›
templates/vue-basic-auth/vite.config.ts›
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
})