Building with the SDK
Vue and Nuxt
The Vue plugin, composables, control components and drop-in UI, and how to use them with Nuxt.
npm install @zevauth/vue
The plugin
// main.ts
import { createApp } from 'vue';
import { createZevAuth } from '@zevauth/vue';
import App from './App.vue';
createApp(App)
.use(createZevAuth({ publishableKey: import.meta.env.VITE_ZEVAUTH_KEY }))
.mount('#app');
| Option | Meaning |
|---|---|
publishableKey | pk_test_… or pk_live_…. Required unless you pass client. |
client | A client from createZevAuthClient, for code outside components to share. |
apiUrl | Where the API lives. Defaults to https://api.zevauth.net. |
appearance | How every component looks. See Customising the components. |
localization | The words every component prints. |
A complete sign-in
<script setup lang="ts">
import { SignedIn, SignedOut, SignIn, UserButton } from '@zevauth/vue';
</script>
<template>
<SignedOut>
<SignIn after-sign-in-url="/dashboard" />
</SignedOut>
<SignedIn>
<UserButton after-sign-out-url="/" />
</SignedIn>
</template>
<SignIn /> shows whichever methods your environment has enabled, so turning
one on in the console makes it appear without a deploy.
Composables
They return the same fields as the React hooks, as refs.
const { isLoaded, isSignedIn, user } = useUser();
const { userId, organizationId, getToken, signOut } = useAuth();
const { organization, members } = useOrganization({ withMembers: true });
const { organizations, setActive, refetch } = useOrganizationList();
const client = useZevAuthClient(); // everything else
Outside a component, in a router guard or a store, composables can’t reach
the plugin. Create the client yourself with createZevAuthClient, pass it
to the plugin as client, and keep the reference:
import { createZevAuth, createZevAuthClient } from '@zevauth/vue';
export const zevauth = createZevAuthClient({ publishableKey: import.meta.env.VITE_ZEVAUTH_KEY });
app.use(createZevAuth({ client: zevauth }));
router.beforeEach(async (to) => {
if (to.meta.requiresAuth && !(await zevauth.getToken())) return '/sign-in';
});
isLoaded is separate from isSignedIn because there are three states, not
two. When a page first renders, nobody knows yet whether a session can be
restored. Treating that as “signed out” makes every reload flash your
signed-out page. Check isLoaded first:
<template>
<p v-if="!isLoaded">Loading…</p>
<p v-else-if="isSignedIn">Hello {{ user?.firstName }}</p>
<a v-else href="/sign-in">Sign in</a>
</template>
Control components
| Component | Renders its slot |
|---|---|
<SignedIn> | Once somebody is signed in |
<SignedOut> | Once it’s known that nobody is |
<ZevAuthLoading> | While the session is still being restored |
<Protect role="…" permission="…"> | For a member with that role or permission on the active organization; the #fallback slot otherwise |
<Protect permission="org:members:manage">
<InviteForm />
<template #fallback><p>Ask an admin.</p></template>
</Protect>
<Protect> hides UI. It doesn’t protect data: everything it hides is still
in your bundle. Make the same check on your server, against the verified
token.
Components
<SignIn>, <SignUp>, <SignInWithZevId>, <UserButton>, <UserProfile>,
<OrganizationSwitcher> and <OrganizationProfile> take the same props as
the React components. Write them as attributes, in
kebab-case or camelCase:
<SignIn
after-sign-in-url="/dashboard"
secondary-methods="collapsed"
layout="split-right"
:appearance="{ theme: 'soft', elements: { card: 'shadow-none' } }"
:localization="{ signIn: { submit: 'Continue' } }"
/>
class and style go on the element that holds the component, so you can
position it with your own CSS. aside and children take React elements
and aren’t available here.
The components render in the browser only. During server rendering they leave an empty element, and the browser fills it in.
Calling your own API
const { getToken } = useAuth();
const response = await fetch('/api/projects', {
headers: { Authorization: `Bearer ${await getToken()}` },
});
getToken refreshes the token first if it’s close to expiry. Verify it on
your server with @zevauth/backend/verify.
Nuxt
Install the same package and register it in a client-only plugin. The session lives in the browser, so there is nothing for the server to load.
// plugins/zevauth.client.ts
import { createZevAuth, createZevAuthClient } from '@zevauth/vue';
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig();
const client = createZevAuthClient({
publishableKey: config.public.zevauthPublishableKey,
});
nuxtApp.vueApp.use(createZevAuth({ client }));
// For route middleware and anything else outside a component.
return { provide: { zevauth: client } };
});
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
zevauthEnvironmentId: '', // NUXT_ZEVAUTH_ENVIRONMENT_ID
public: { zevauthPublishableKey: '' }, // NUXT_PUBLIC_ZEVAUTH_PUBLISHABLE_KEY
},
});
Because the plugin is client-only, a page that uses the composables or
components must render on the client too. Wrap them in <ClientOnly>:
<template>
<ClientOnly>
<SignedOut><SignIn after-sign-in-url="/dashboard" /></SignedOut>
<SignedIn><UserButton /></SignedIn>
</ClientOnly>
</template>
Server routes
Server-rendered page requests don’t carry the session: it’s sent as a bearer
token by your own fetch calls. Verify it in the server routes those calls
reach:
// server/api/projects.get.ts
import { getAuth } from '@zevauth/backend/verify';
export default defineEventHandler(async (event) => {
const auth = await getAuth(toWebRequest(event), {
environmentId: useRuntimeConfig().zevauthEnvironmentId,
});
if (!auth.isSignedIn) {
throw createError({ statusCode: 401, statusMessage: 'Sign in to continue.' });
}
return listProjects({ userId: auth.userId, organizationId: auth.organizationId });
});
getAuth never throws for a missing, expired or tampered token. It returns
isSignedIn: false, so a lapsed session is a 401, not a 500.
Protecting pages
Redirect in a client route middleware, once loading has finished:
// middleware/signed-in.ts
export default defineNuxtRouteMiddleware(async (to) => {
if (import.meta.server) return;
const token = await useNuxtApp().$zevauth.getToken();
if (!token) {
return navigateTo({ path: '/sign-in', query: { redirect_url: to.fullPath } });
}
});
Updated at, Sunday, November 1, 2026