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