@providame/vue

Providame Vue 3 bindings — headless composables and prebuilt auth components.
2,532
Install
npm install @providame/vue
Latest Version:0.0.16
Vue:3
License:MIT
Last Updated:Oct 6, 2026
Links: GitLab  ·  npm
Maintainer: suraj.sudhera

@providame/vue

Vue 3 bindings for Providame — headless composables (build your own UI) and prebuilt components (drop-in), both over the same @providame/core engine.

Building a native or hybrid app (Capacitor or Ionic, or any app without a browser cookie jar)? Use a native key (nk_) instead of your publishable key — native mode comes from @providame/core and is configured on the client. See the Native apps guide.

Install

npm install @providame/vue vue

Usage

Install the plugin once, and import the stylesheet:

import { createApp } from 'vue'
import { ProvidamePlugin } from '@providame/vue'
import '@providame/vue/style.css' // required — see below

createApp(App).use(ProvidamePlugin, {
  publishableKey: 'pk_test_...',
})

The stylesheet import is required. Without it every prebuilt component renders completely unstyled — there is no runtime fallback, and no error to tell you what's wrong.

This package ships a real CSS file rather than injecting styles at runtime, so Nuxt and other SSR frameworks can inline it into the document head instead of flashing unstyled content on first paint. @providame/react and @providame/sdk-js inject at runtime and need no import — this package is the exception, deliberately.

In Nuxt, add it to nuxt.config.ts instead: css: ['@providame/vue/style.css'].

Prebuilt components

<script setup>
import { SignIn } from '@providame/vue'
</script>

<template>
  <SignIn sign-up-url="/sign-up" @complete="onComplete" />
</template>

Component props

SignIn, SignUp, and ResetPassword all accept title, subtitle, and logo. title defaults to a branding-aware heading (e.g. "Sign in to Acme") once your environment's app name loads; logo defaults to your configured branding logo.

SignUp additionally accepts:

  • collect-name (default true) — toggles the first/last name fields.
  • invite-token — required when your environment's signup-policy mode is invite_only: the single-use code from an admin-generated signup-invite email. Read it from your own app's invite-accept URL and pass it through — it threads directly to CreateSignUpParams.inviteToken. Every other signup-policy mode ignores it if set.
<SignUp :collect-name="false" invite-token="tok_..." />

Modal vs. page usage

These components serve two contexts, and the footer link behaves differently in each:

  • Page — you render <SignIn> at your own /sign-in route. Pass sign-up-url and the footer renders a normal link that navigates.
  • Modal — <SignInButton mode="modal"> handles this for you: the footer switches to sign-up in place, because navigating would tear the modal down.

Rendering these inside your own modal? Attach the switch listener instead of passing a URL, and hold the view state yourself:

<SignIn v-if="view === 'sign-in'" @switch-to-sign-up="view = 'sign-up'" />
<SignUp v-else @switch-to-sign-in="view = 'sign-in'" />

The listener takes precedence when both it and a URL are supplied.

SignInButton no longer accepts sign-up-url (nor SignUpButton sign-in-url). They had no effect in mode="redirect" and produced a modal-destroying navigation in mode="modal". Pass those URLs to <SignIn>/<SignUp> directly when rendering them as pages.

Headless

import { useSignIn } from '@providame/vue'

const signIn = useSignIn()

// Preferred — one guarded call for the common identifier + password case.
await signIn.signInWithPassword('user@example.com', 'password')

// Equivalent, longhand — useful if you need to do something between the two
// steps, e.g. show a separate password screen after the identifier is entered.
await signIn.create('user@example.com')
await signIn.attemptFirstFactor({ strategy: 'password', password: 'password' })

if (signIn.isComplete.value) {
  // signIn.session.value is set
}

Passwordless sign-in & MFA (useSignIn)

Beyond password sign-in, useSignIn() exposes passkey, email-OTP, SMS-OTP, and magic-link sign-in, plus mid-flow TOTP enrollment. The prebuilt <SignIn> above already wires password and passkey sign-in — email-OTP, SMS-OTP, and magic-link are headless-only escape hatches; build your own UI around them if you need one.

import { useSignIn } from '@providame/vue'
const signIn = useSignIn()

// Passkey — opens the flow and immediately prompts navigator.credentials.get().
await signIn.signInWithPasskey(identifier, window.location.hostname)

// Email one-time code (first factor)
await signIn.signInWithOTPEmail(identifier)
await signIn.attemptFirstFactor({ strategy: 'otp_email', code })
await signIn.resendOTPEmail()

// SMS one-time code (first factor) — identifier is still email; the code is
// texted to whatever phone number is on file for that account.
await signIn.signInWithOTPSMS(identifier)
await signIn.attemptFirstFactor({ strategy: 'otp_sms', code })
await signIn.resendOTPSMS()

// Magic link — emails a passwordless "click to sign in" link pointing at returnUrl.
await signIn.signInWithMagicLink(identifier, 'https://myapp.com/auth/callback')

// Mid-flow TOTP, reached from any of the strategies above.
if (signIn.status.value === 'needs_second_factor') {
  await signIn.attemptSecondFactor({ strategy: 'totp', code })
}
if (signIn.status.value === 'needs_mfa_enrollment') {
  await signIn.enrollTotp() // -> signIn.enrollment.value = { secret, otpauthUri }
  await signIn.verifyTotpEnrollment(code)
}

signIn.reset() // back to "idle"

firstFactors/secondFactors report what the backend will accept next; recoveryCodes is populated right after a mid-flow TOTP enrollment.

Sign-up (useSignUp)

useSignUp() mirrors useSignIn() — same reactive-state shape, for registration. The prebuilt <SignUp> already wires all of this; reach for the composable directly to build a fully custom sign-up UI.

import { useSignUp } from '@providame/vue'
const signUp = useSignUp()

await signUp.create({
  email,
  password,
  firstName, // optional
  lastName, // optional
  inviteToken, // required only when signup-policy mode is invite_only
  turnstileToken, // required only when bot-check is enabled for this environment
})

if (signUp.needsVerification.value) {
  await signUp.attemptVerification({ code })
}

// Post-registration second factor, if the environment's MFA policy requires one.
if (signUp.status.value === 'needs_second_factor') {
  await signUp.attemptSecondFactor({ strategy: 'totp', code })
}

// Forced enrollment on a brand-new account.
if (signUp.status.value === 'needs_mfa_enrollment') {
  await signUp.enrollTotp() // -> signUp.enrollment.value = { secret, otpauthUri }
  await signUp.verifyTotpEnrollment(code)
}

if (signUp.isComplete.value) {
  // signUp.session.value is set; signUp.recoveryCodes.value may carry backup codes
} else if (signUp.isWaitlisted.value) {
  // verified, but this environment's signup-policy mode is "waitlist" — no session issued yet
}

signUp.reset()

Password reset (useResetPassword, <ResetPassword>)

<script setup>
import { ResetPassword } from '@providame/vue'
</script>

<template>
  <ResetPassword sign-in-url="/sign-in" @complete="onComplete" />
</template>

The prebuilt <SignIn> already embeds this whole flow behind its "Forgot password?" link — mount <ResetPassword> on its own route only if you want it reachable directly (e.g. from a password-reset email link).

import { useResetPassword } from '@providame/vue'
const reset = useResetPassword()

await reset.create({ email })
await reset.attempt({ code, password }) // sets the new password and signs in

// Mid-flow TOTP, same shape as useSignIn/useSignUp.
if (reset.status.value === 'needs_second_factor') {
  await reset.attemptSecondFactor({ strategy: 'totp', code })
}
if (reset.status.value === 'needs_mfa_enrollment') {
  await reset.enrollTotp() // -> reset.enrollment.value = { secret, otpauthUri }
  await reset.verifyTotpEnrollment(code)
}

await reset.resend() // re-send the reset code
reset.reset() // back to "idle"

attempt()/attemptSecondFactor()/enrollTotp()/verifyTotpEnrollment()/resend() all throw if called before create() — there's no flow yet to act on.

What's in the box

Components — SignIn, SignUp, ResetPassword, UserButton, UserProfile, SignInButton, SignUpButton, SignOutButton, SignedIn, SignedOut, Protect, RedirectToSignIn, OrganizationSwitcher, OrganizationProfile, CreateOrganization.

Composables — useAuth, useUser, useSession, useSignIn, useSignUp, useSignInIdp, useResetPassword, useOrganization, useOrganizationList, useBranding, usePasskeyRegistrationLink, useProvidame.

Every composable returns Refs — including the functions, matching Clerk's own Vue SDK. So it's await getToken.value(), not getToken().

Organizations

<script setup>
import { OrganizationSwitcher, OrganizationProfile, useOrganization } from '@providame/vue'

// Reads token claims — makes no request
const { organization, roles, permissions, has } = useOrganization()
</script>

<template>
  <OrganizationSwitcher />
  <OrganizationProfile />
</template>

useOrganization() covers the active organization only and never fetches — the token already carries org_id/org_slug/org_roles/org_permissions. useOrganizationList() does fetch, since the full membership list isn't in a token; it also exposes setActive(orgId).

setActive() is remembered across sign-ins, so you don't call it on every login: a returning user is put back in the organization they last used, a user who has never chosen starts in the one they joined first, and clearing it sticks too.

<OrganizationProfile> gates each action on the caller's own org:sys_* permissions rather than a role name, so it works with roles you define yourself. Pass organization-id to render a specific organization instead of the session's active one.

<OrganizationSwitcher> accepts hide-create (drop "Create organization", for apps that provision organizations centrally), personal-label (relabel the no-organization option — Clerk calls it "Personal account"), and hide-personal (drop that option entirely, for a B2B-only app where every user belongs to an organization).

Gating UI

<Protect role="admin">...</Protect>
<Protect org-permission="org:sys_memberships:manage">...</Protect>
<Protect role="admin" org-role="org:admin">...</Protect>   <!-- both must pass -->

role/permission gate on environment-level grants; org-role/org-permission gate on the active organization. Omitted props are not constraints. This is UI gating only — real enforcement is your backend verifying the token, via @providame/backend.

Object-scoped access

<Protect> reads claims already in the token — synchronous, free, never fails. <Can> asks a question about one specific resource — a network call, so it needs a loading state and can fail:

<Can permission="edit" resource="document:42">
  <template #loading><Spinner /></template>
  <template #fallback>You can't edit this document.</template>
  <button>Edit</button>
</Can>

Use <Protect role="admin"> for "is this user an admin"; use <Can permission="edit" resource="document:42"> for "can this user edit this document". <Can> fails closed — a network error renders #fallback, never the default slot. useCan({ permission, resource }) is the headless equivalent, returning { allowed, isLoading, error }.

Answers are cached per-question for the life of the session (cleared on sign-in/out and organization switch) — mounting <Can> for the same resource twice on one page costs one request, not two.

<Can> also accepts an optional context prop (a plain object) that passes extra values through for conditional/attribute-based permissions — e.g. checking a permission that's only granted below a certain amount or within a certain region.

Branding & the underlying client

useBranding() is called internally by every prebuilt component above — reach for it directly only if you're building a fully custom UI and want the same environment-configured colors/logo applied to it:

import { useBranding } from '@providame/vue'

// Fetched once and memoized on the client instance — mounting several
// components that call this on one page costs a single request, not one
// per component. Also applies the colors as global CSS custom properties
// (--providame-accent/-bg/-fg and their -dark counterparts).
const branding = useBranding() // Ref<Branding>

branding.value.logoUrl // string | undefined
branding.value.primaryColor
branding.value.appName

useProvidame() returns the underlying Providame client instance every composable in this package is built on — every other composable calls this internally, so you only need it directly to reach a client method this package doesn't already wrap:

import { useProvidame } from '@providame/vue'

const providame = useProvidame()

await providame.configuration() // { turnstileSiteKey, tosUrl, privacyUrl, helpUrl, supportEmail, ... }
await providame.branding() // same data useBranding() applies automatically
providame.organizations // OrganizationsApi — used internally by <OrganizationProfile>

Theming

Every component accepts an appearance prop:

<SignIn :appearance="{ variables: { colorPrimary: '#111827' } }" />

Components also fetch your environment's configured branding (colors, logo, light and dark) on mount and apply it automatically, so an unstyled appearance prop is usually unnecessary.

Docs

Full reference: /docs/sdk/vue in your Providame dashboard.

Build

npm run build

Related Packages

@authon/nuxt

Authon Nuxt 3 module — auto-imported composables and middleware

2,931 1

Version History

Version Released Vue License
0.0.16 ^3.5.0 MIT
0.0.15 ^3.5.0 MIT
0.0.14 ^3.5.0 MIT
0.0.13 ^3.5.0 MIT
0.0.12 ^3.5.0 MIT
Pre-releases (2)
Version Released Vue License
0.0.2-alpha.0 ^3.5.0 MIT
0.0.1-alpha.0 ^3.5.0 MIT