@providame/vue
v0.0.15
Published
Providame Vue 3 bindings — headless composables and prebuilt auth components.
Downloads
570
Readme
@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/coreand is configured on the client. See the Native apps guide.
Install
npm install @providame/vue vueUsage
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/reactand@providame/sdk-jsinject at runtime and need no import — this package is the exception, deliberately.In Nuxt, add it to
nuxt.config.tsinstead: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(defaulttrue) — toggles the first/last name fields.invite-token— required when your environment's signup-policy mode isinvite_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 toCreateSignUpParams.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-inroute. Passsign-up-urland 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.
SignInButtonno longer acceptssign-up-url(norSignUpButtonsign-in-url). They had no effect inmode="redirect"and produced a modal-destroying navigation inmode="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('[email protected]', '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('[email protected]')
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.appNameuseProvidame() 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