├── .npmrc ├── demo.png ├── server ├── tsconfig.json └── api │ └── auth.ts ├── app ├── app.vue ├── pages │ ├── sign-in │ │ └── [...slug].vue │ ├── sign-up │ │ └── [...slug].vue │ ├── dashboard.vue │ └── index.vue ├── middleware │ └── auth.global.ts ├── components │ ├── NuxtLogo.vue │ ├── ClerkLogo.vue │ ├── LearnMore.vue │ ├── CodeSwitcher.vue │ ├── UserDetails.tsx │ └── Footer.vue ├── composables │ └── useCodeHighlighter.ts ├── layouts │ └── default.vue └── consts │ └── cards.ts ├── public ├── favicon.ico ├── images │ ├── logo.png │ ├── dark-logo.png │ ├── light-logo.png │ ├── sign-in@2xrl.webp │ ├── sign-up@2xrl.webp │ ├── verify@2xrl.webp │ ├── user-button@2xrl.webp │ ├── user-button-2@2xrl.webp │ ├── nuxt.svg │ └── clerk.svg └── fonts │ ├── GeistVF.woff │ └── GeistMonoVF.woff ├── tsconfig.json ├── pnpm-workspace.yaml ├── eslint.config.mjs ├── .env.example ├── .gitignore ├── tailwind.config.js ├── package.json ├── nuxt.config.ts └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/nuxt-clerk-template/HEAD/demo.png -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /app/app.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/nuxt-clerk-template/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/nuxt-clerk-template/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /public/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/nuxt-clerk-template/HEAD/public/fonts/GeistVF.woff -------------------------------------------------------------------------------- /public/images/dark-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/nuxt-clerk-template/HEAD/public/images/dark-logo.png -------------------------------------------------------------------------------- /public/images/light-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/nuxt-clerk-template/HEAD/public/images/light-logo.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - '@clerk/shared' 3 | - '@parcel/watcher' 4 | - esbuild 5 | - sharp 6 | -------------------------------------------------------------------------------- /public/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/nuxt-clerk-template/HEAD/public/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /public/images/sign-in@2xrl.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/nuxt-clerk-template/HEAD/public/images/sign-in@2xrl.webp -------------------------------------------------------------------------------- /public/images/sign-up@2xrl.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/nuxt-clerk-template/HEAD/public/images/sign-up@2xrl.webp -------------------------------------------------------------------------------- /public/images/verify@2xrl.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/nuxt-clerk-template/HEAD/public/images/verify@2xrl.webp -------------------------------------------------------------------------------- /public/images/user-button@2xrl.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/nuxt-clerk-template/HEAD/public/images/user-button@2xrl.webp -------------------------------------------------------------------------------- /public/images/user-button-2@2xrl.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wobsoriano/nuxt-clerk-template/HEAD/public/images/user-button-2@2xrl.webp -------------------------------------------------------------------------------- /app/pages/sign-in/[...slug].vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/pages/sign-up/[...slug].vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({}, { 4 | rules: { 5 | 'node/prefer-global/process': 'off', 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key 2 | NUXT_CLERK_SECRET_KEY=your_secret_key 3 | NUXT_PUBLIC_CLERK_SIGN_IN_FORCE_REDIRECT_URL=/dashboard 4 | NUXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL=/dashboard 5 | NUXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 6 | NUXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .nuxt 4 | .nitro 5 | .cache 6 | dist 7 | 8 | # Node dependencies 9 | node_modules 10 | 11 | # Logs 12 | logs 13 | *.log 14 | 15 | # Misc 16 | .DS_Store 17 | .fleet 18 | .idea 19 | 20 | # Local env files 21 | .env 22 | .env.* 23 | !.env.example 24 | 25 | .vscode 26 | 27 | .data 28 | .wrangler 29 | -------------------------------------------------------------------------------- /server/api/auth.ts: -------------------------------------------------------------------------------- 1 | import { clerkClient } from '@clerk/nuxt/server' 2 | 3 | export default eventHandler(async (event) => { 4 | const { userId } = event.context.auth() 5 | 6 | if (!userId) { 7 | throw createError({ 8 | statusCode: 401, 9 | statusMessage: 'Unauthorized', 10 | }) 11 | } 12 | 13 | const user = await clerkClient(event).users.getUser(userId) 14 | 15 | return { user } 16 | }) 17 | -------------------------------------------------------------------------------- /app/middleware/auth.global.ts: -------------------------------------------------------------------------------- 1 | const isProtectedPage = createRouteMatcher(['/dashboard']) 2 | const isGuestPage = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)']) 3 | 4 | export default defineNuxtRouteMiddleware((to) => { 5 | const { userId } = useAuth() 6 | 7 | if (userId.value && isGuestPage(to)) { 8 | return navigateTo('/dashboard') 9 | } 10 | 11 | if (!userId.value && isProtectedPage(to)) { 12 | return navigateTo('/sign-in') 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | theme: { 4 | extend: { 5 | colors: { 6 | 'primary-600': '#6C47FF', 7 | 'primary-700': '#5639CC', 8 | 'primary-50': '#F4F2FF', 9 | 'success-700': '#027A48', 10 | 'success-50': '#ECFDF3', 11 | }, 12 | fontFamily: { 13 | sans: ['var(--font-geist-sans)'], 14 | mono: ['var(--font-geist-mono)'], 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-clerk-template", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare", 11 | "lint": "eslint .", 12 | "lint:fix": "eslint . --fix" 13 | }, 14 | "dependencies": { 15 | "@clerk/nuxt": "^1.13.5", 16 | "@nuxt/image": "^2.0.0", 17 | "@oxc-parser/binding-linux-x64-gnu": "^0.81.0", 18 | "nuxt": "^4.2.1", 19 | "vue": "^3.5.25", 20 | "vue-router": "^4.6.3" 21 | }, 22 | "devDependencies": { 23 | "@antfu/eslint-config": "^4.19.0", 24 | "@nuxtjs/tailwindcss": "^6.14.0", 25 | "@types/node": "^20.19.25", 26 | "eslint": "^9.39.1", 27 | "typescript": "latest" 28 | }, 29 | "packageManager": "pnpm@10.24.0" 30 | } 31 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | devtools: { enabled: false }, 3 | modules: ['@nuxtjs/tailwindcss', '@nuxt/image', '@clerk/nuxt'], 4 | 5 | clerk: { 6 | appearance: { 7 | variables: { colorPrimary: '#000000' }, 8 | elements: { 9 | formButtonPrimary: 10 | 'bg-black border border-black border-solid hover:bg-white hover:text-black', 11 | socialButtonsBlockButton: 12 | 'bg-white border-gray-200 hover:bg-transparent hover:border-black text-gray-600 hover:text-black', 13 | socialButtonsBlockButtonText: 'font-semibold', 14 | formButtonReset: 15 | 'bg-white border border-solid border-gray-200 hover:bg-transparent hover:border-black text-gray-500 hover:text-black', 16 | membersPageInviteButton: 17 | 'bg-black border border-black border-solid hover:bg-white hover:text-black', 18 | card: 'bg-[#fafafa]', 19 | }, 20 | }, 21 | }, 22 | 23 | compatibilityDate: '2025-05-15', 24 | }) 25 | -------------------------------------------------------------------------------- /app/components/NuxtLogo.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /app/pages/dashboard.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 43 | -------------------------------------------------------------------------------- /app/composables/useCodeHighlighter.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | Prism: { 4 | highlight: (...args: any) => string 5 | languages: Record 6 | plugins: { 7 | autoloader: { 8 | loadLanguages: (languages: string[], callback: () => void) => void 9 | } 10 | } 11 | } 12 | } 13 | } 14 | 15 | export function useCodeHighlighter(code: Ref) { 16 | const renderedCode = ref('') 17 | const isLanguageLoaded = ref(false) 18 | 19 | onMounted(() => { 20 | const loadLanguage = () => { 21 | if (window.Prism.languages.javascript) { 22 | isLanguageLoaded.value = true 23 | } 24 | else { 25 | window.Prism.plugins.autoloader.loadLanguages(['javascript'], () => { 26 | isLanguageLoaded.value = true 27 | }) 28 | } 29 | } 30 | 31 | loadLanguage() 32 | 33 | watch( 34 | [code, isLanguageLoaded], 35 | ([value, loaded]) => { 36 | if (!value || !loaded) { 37 | return 38 | } 39 | renderedCode.value = window.Prism.highlight( 40 | value, 41 | window.Prism.languages.javascript, 42 | 'javascript', 43 | ) 44 | }, 45 | { immediate: true }, 46 | ) 47 | }) 48 | 49 | return { 50 | renderedCode, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/components/ClerkLogo.vue: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /app/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | 24 | 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clerk and Nuxt template 2 | 3 | This template shows how to use [Clerk](https://www.clerk.com) in a Nuxt application. It is a direct port of the [Next.js starter template](https://github.com/clerk/clerk-nextjs-demo-app-router). 4 | 5 | Clerk is a developer-first authentication and user management solution. This starter uses [Clerk Nuxt SDK](https://clerk.com/docs/references/nuxt/overview), which provides pre-built Vue components and composables for login, signup, user profile, and organization management. Clerk is designed to be easy to use and customize, and can be dropped into any Vue or Nuxt application. 6 | 7 | This template allows you to get started with Clerk and Nuxt in a matter of minutes and includes: 8 | 9 | - Fully functional auth flow with login, signup, and a protected page 10 | - Customized Clerk components with Tailwind CSS 11 | - Composables for accessing user data and authentication state 12 | - Organizations for multi-tenant applications 13 | 14 | ## Demo 15 | 16 | A hosted demo of this example is available at: 17 | 18 | - https://nuxt-clerk-template.vercel.app 19 | - https://nuxt-clerk-template.pages.dev 20 | - https://nuxt-clerk-template.netlify.app 21 | 22 | ## Running the template 23 | 24 | ```bash 25 | git clone https://github.com/wobsoriano/nuxt-clerk-template 26 | ``` 27 | 28 | To run the example locally you need to: 29 | 30 | 1. Sign up for a Clerk account at [https://clerk.com](https://dashboard.clerk.com/sign-up). 31 | 2. Go to [Clerk's dashboard](https://dashboard.clerk.com) and create an application. 32 | 3. Set the required Clerk environment variables as shown in [the example env file](./.env.example). 33 | 4. `npm install` the required dependencies. 34 | 5. `npm run dev` to launch the development server. 35 | 36 | ## Learn more 37 | 38 | To learn more about Clerk and Nuxt, check out the following resources: 39 | 40 | - [Clerk Documentation](https://clerk.com/docs) 41 | - [Vue Clerk](https://vue-clerk.com) 42 | - [Nuxt Documentation](https://nuxt.com/docs) 43 | -------------------------------------------------------------------------------- /app/consts/cards.ts: -------------------------------------------------------------------------------- 1 | export const CARDS = [ 2 | { 3 | title: 'Customizable Components', 4 | description: 5 | 'Prebuilt components to handle essential functionality like user sign-in, sign-up, and account management.', 6 | href: 'https://clerk.com/docs/components/overview?utm_source=vercel-template&utm_medium=partner&utm_term=component_reference', 7 | linkText: 'Component Reference', 8 | }, 9 | { 10 | title: 'Vue Composables', 11 | description: `Build custom functionality by accessing auth state, user and session data, and more with Vue Clerk's composables.`, 12 | href: 'https://www.vue-clerk.com/composables/use-auth', 13 | linkText: 'Vue Composables', 14 | }, 15 | { 16 | title: 'Organizations', 17 | description: 18 | 'Built for B2B SaaS: create and switch between orgs, manage and invite members, and assign custom roles.', 19 | href: 'https://clerk.com/docs/organizations/overview?utm_source=vercel-template&utm_medium=partner&utm_term=component_reference', 20 | linkText: 'Organizations', 21 | }, 22 | ] 23 | 24 | export const DASHBOARD_CARDS = [ 25 | { 26 | title: 'Authenticate requests with JWT\'s', 27 | description: 28 | 'Clerk empowers you to authenticate same and cross origin requests using a Clerk generated JWT', 29 | href: 'https://clerk.com/docs/backend-requests/overview?utm_source=vercel-template&utm_medium=partner&utm_term=JWT', 30 | linkText: 'Request authentication', 31 | }, 32 | { 33 | title: 'Build an onboarding flow', 34 | description: `Leverage customizable session tokens, public metadata, and Middleware to create a custom onboarding experience.`, 35 | href: 'https://clerk.com/docs/guides/add-onboarding-flow?utm_source=vercel-template&utm_medium=partner&utm_term=onboarding', 36 | linkText: 'Onboarding flow', 37 | }, 38 | { 39 | title: 'Deploy to Production', 40 | description: 41 | 'Production instances are meant to support high volumes of traffic and by default, have a more strict security posture.', 42 | href: 'https://clerk.com/docs/deployments/overview?utm_source=vercel-template&utm_medium=partner&utm_term=deploy-to-prod', 43 | linkText: 'Production', 44 | }, 45 | ] 46 | -------------------------------------------------------------------------------- /app/components/LearnMore.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 69 | -------------------------------------------------------------------------------- /public/images/nuxt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/components/CodeSwitcher.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 73 | 74 | 207 | -------------------------------------------------------------------------------- /public/images/clerk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 101 | 102 | 130 | -------------------------------------------------------------------------------- /app/components/UserDetails.tsx: -------------------------------------------------------------------------------- 1 | const Row = defineComponent((props: { desc: string, value: string }, { slots }) => { 2 | return () => ( 3 |
4 | {props.desc} 5 | 6 | {props.value} 7 | {slots.default?.()} 8 | 9 |
10 | ) 11 | }, { 12 | props: ['desc', 'value'], 13 | }) 14 | 15 | const PointerC = defineComponent((props: { label: string }) => { 16 | return () => ( 17 |
18 |
19 |
20 |
21 |
22 |
23 | {props.label} 24 |
25 |
26 | ) 27 | }, { 28 | props: ['label'], 29 | }) 30 | 31 | function formatDate(date: Date) { 32 | return date.toLocaleDateString('en-US', { 33 | month: 'short', 34 | day: 'numeric', 35 | year: 'numeric', 36 | }) 37 | } 38 | 39 | function formatDateWithNumbers(date: Date): string { 40 | return date.toLocaleString('en-US', { 41 | month: 'numeric', 42 | day: 'numeric', 43 | year: 'numeric', 44 | hour: 'numeric', 45 | minute: '2-digit', 46 | second: '2-digit', 47 | hour12: true, 48 | }) 49 | } 50 | 51 | const UserDetails = defineComponent(() => { 52 | const { user } = useUser() 53 | const { session } = useSession() 54 | const { organization } = useOrganization() 55 | 56 | return () => ( 57 | user.value && session.value 58 | ? ( 59 |
60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 |
68 |
69 |
70 | user.imageUrl 71 |
72 |
73 |
74 | {user.value.firstName && user.value.lastName 75 | ? ( 76 |

77 | {user.value.firstName} 78 | {' '} 79 | {user.value.lastName} 80 |
81 |
82 |
83 |
84 |
85 |
86 | user.firstName 87 |
88 |
89 | user.lastName 90 |
91 |
92 |

93 | ) 94 | : ( 95 |
96 | )} 97 |
98 | 99 |
100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 |
113 |

114 | Session details 115 |

116 |
117 | 118 | 119 | 120 | 121 | 122 | 123 | 127 | 128 | 129 | 133 | 134 | 135 |
136 | {organization.value 137 | ? ( 138 | <> 139 |

140 | Organization detail 141 |

142 |
143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 156 | 157 | 158 |
159 | 160 | ) 161 | : null} 162 |
163 |
164 | ) 165 | : null 166 | ) 167 | }) 168 | 169 | export default UserDetails 170 | -------------------------------------------------------------------------------- /app/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 147 | --------------------------------------------------------------------------------