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-jsCreate the client
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
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
<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
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;
};