ZevAuth
Sign up

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');
OptionMeaning
publishableKeypk_test_… or pk_live_…. Required unless you pass client.
clientA client from createZevAuthClient, for code outside components to share.
apiUrlWhere the API lives. Defaults to https://api.zevauth.net.
appearanceHow every component looks. See Customising the components.
localizationThe 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

ComponentRenders 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