Documentation
Build withFramework quickstartsVue

Vue

One composable every component shares, and guards that ask about permissions.

This is the shape the nulvless console itself uses — Vue 3 with the same SDK, running entirely in the browser. Route guards decide what renders; your API still decides what is allowed.

Install

npm install @nulvless/nulvless-js

Create the client

src/lib/nulvless.ts
import { createClient, Storage } from '@nulvless/nulvless-js';

export const nulvless = createClient(
  import.meta.env.VITE_NULVLESS_URL,
  import.meta.env.VITE_NULVLESS_KEY,
  {
    storage: Storage.Local,
    trackUser: true,
    language: () => navigator.language,
  },
);

language decides which template language the platform renders for any message this client triggers — a verification email, for example.

A session composable

src/composables/useSession.ts
import { onUnmounted, ref } from 'vue';
import type { Session } from '@nulvless/nulvless-js';
import { nulvless } from '@/lib/nulvless';

const session = ref<Session | null>(null);
const loading = ref(true);

nulvless.auth.getSession().then(({ data }) => {
  session.value = data;
  loading.value = false;
});

export function useSession() {
  const offIn = nulvless.auth.on('signed-in', async () => {
    const { data } = await nulvless.auth.getSession();
    session.value = data;
  });
  const offOut = nulvless.auth.on('signed-out', () => (session.value = null));
  const offDead = nulvless.auth.on('unauthorized', () => (session.value = null));

  onUnmounted(() => {
    offIn();
    offOut();
    offDead();
  });

  return { session, loading };
}

The refs live at module scope, so every component shares one session rather than each fetching its own.

Sign in

src/views/SignIn.vue
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { nulvless } from '@/lib/nulvless';

const router = useRouter();
const email = ref('');
const password = ref('');
const error = ref<string | null>(null);

async function signIn() {
  error.value = null;

  const { data, error: signInError } = await nulvless.auth.signIn({
    email: email.value,
    password: password.value,
  });

  if (signInError) {
    error.value = 'Check your email and password.';
    return;
  }

  if (data.challenge) {
    router.push({ name: 'verify', state: { challenge: data.challenge } });
    return;
  }

  router.push('/');
}
</script>

<template>
  <form @submit.prevent="signIn">
    <input v-model="email" type="email" />
    <input v-model="password" type="password" />
    <p v-if="error" role="alert">{{ error }}</p>
    <button type="submit">Sign in</button>
  </form>
</template>

Sign up

Sign-up branches on the challenge, because whether confirmation is a link or a code is an environment setting.

const { data, error } = await nulvless.auth.signUp({
  email: email.value,
  password: password.value,
  profile: { name: name.value },
});

if (error) return;

if (data.challenge) {
  if (data.challenge.preferred === 'link') {
    checkYourEmail.value = true;
    return;
  }
  router.push({ name: 'verify', state: { challenge: data.challenge } });
  return;
}

router.push('/');

Guard the routes

src/router/guards.ts
import type { NavigationGuard } from 'vue-router';
import { nulvless } from '@/lib/nulvless';

export const requireAuth: NavigationGuard = async (to) => {
  const { data } = await nulvless.auth.getSession();
  if (!data) return { name: 'sign-in', query: { next: to.fullPath } };
  return true;
};

For a route that also needs a permission, ask in the same call:

export const requireOwner: NavigationGuard = async (to) => {
  const { error } = await nulvless.auth.verify({
    resource: String(to.params.org),
    roles: ['owner', 'admin'],
  });

  return error ? { name: 'forbidden' } : true;
};

Next

On this page