---
title: Vue and Nuxt
description: The Vue plugin, composables, control components and drop-in UI, and how to use them with Nuxt.
---

```bash
npm install @zevauth/vue
```

## The plugin

```ts
// 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](/concepts/appearance/). |
| `localization` | The words every component prints. |

## A complete sign-in

```vue
<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](/sdk/react/), as refs.

```ts
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:

```ts
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:

```vue
<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 |

```vue
<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](/sdk/components/). Write them as attributes, in
kebab-case or camelCase:

```vue
<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

```ts
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`](/concepts/verifying-tokens/).

## 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.

```ts
// 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 } };
});
```

```ts
// 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>`:

```vue
<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:

```ts
// 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:

```ts
// 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 } });
  }
});
```

> **Note:**
> `getToken()` is safe to call at any time, including before the components
> have loaded. Refreshes are serialised across tabs, so it never races the
> components for the session.
