Guides

Vue Integration

SPA-specific patterns for Vue 3 and Nuxt

Vue Integration

This guide covers integration patterns for Vue 3 with Composition API and Nuxt 3. Vue’s reactivity system pairs naturally with BotSigged’s real-time score updates.

Installation

npm install @botsigged/sdk
# or
yarn add @botsigged/sdk
# or
pnpm add @botsigged/sdk

Vue 3 Setup

Composable

Create a composable to manage the SDK instance:

// composables/useBotSigged.ts
import { ref, onMounted, onUnmounted, readonly } from 'vue';
import { BotSigged, ScoreUpdate } from '@botsigged/sdk';

const sdk = ref<BotSigged | null>(null);
const score = ref<ScoreUpdate | null>(null);
const isConnected = ref(false);
const isReady = ref(false);

let initialized = false;

export function useBotSigged(apiKey?: string) {
  onMounted(() => {
    if (initialized || !apiKey) return;
    initialized = true;

    sdk.value = new BotSigged({
      apiKey,
      autoStart: true,
      onScoreUpdate: (newScore) => {
        score.value = newScore;
        isReady.value = true;
      },
      onConnectionChange: (connected) => {
        isConnected.value = connected;
      },
    });
  });

  onUnmounted(() => {
    // Keep SDK alive across component unmounts
    // Only stop on full app teardown
  });

  const waitUntilReady = async () => {
    return sdk.value?.waitUntilReady();
  };

  const getSessionId = () => sdk.value?.getSessionId();

  const canSubmit = () => sdk.value?.canSubmit();

  return {
    sdk: readonly(sdk),
    score: readonly(score),
    isConnected: readonly(isConnected),
    isReady: readonly(isReady),
    waitUntilReady,
    getSessionId,
    canSubmit,
  };
}

Plugin (Optional)

For global access, create a Vue plugin:

// plugins/botsigged.ts
import { App, ref } from 'vue';
import { BotSigged, ScoreUpdate } from '@botsigged/sdk';

export const botSiggedPlugin = {
  install(app: App, options: { apiKey: string }) {
    const sdk = new BotSigged({
      apiKey: options.apiKey,
      autoStart: true,
    });

    app.config.globalProperties.$botSigged = sdk;
    app.provide('botSigged', sdk);
  },
};

Register in your app:

// main.ts
import { createApp } from 'vue';
import { botSiggedPlugin } from './plugins/botsigged';
import App from './App.vue';

createApp(App)
  .use(botSiggedPlugin, { apiKey: import.meta.env.VITE_BOTSIGGED_KEY })
  .mount('#app');

App.vue Setup

Initialize the SDK at the app root:

<!-- App.vue -->

<script setup lang="ts">
import { useBotSigged } from '@/composables/useBotSigged';

// Initialize once at app root
useBotSigged(import.meta.env.VITE_BOTSIGGED_KEY);
</script>

<template>
  <RouterView />
</template>

Form Protection

Basic Protected Form

<script setup lang="ts">
import { ref } from 'vue';
import { useBotSigged } from '@/composables/useBotSigged';

const { waitUntilReady, score, isReady } = useBotSigged();

const email = ref('');
const isSubmitting = ref(false);
const error = ref<string | null>(null);

async function handleSubmit() {
  error.value = null;
  isSubmitting.value = true;

  try {
    const result = await waitUntilReady();

    if (result?.score && result.score > 70) {
      error.value = 'Unable to verify your request. Please try again.';
      return;
    }

    await fetch('/api/signup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: email.value }),
    });
  } finally {
    isSubmitting.value = false;
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="email" type="email" required />
    <button type="submit" :disabled="isSubmitting">
      {{ isSubmitting ? 'Submitting...' : 'Sign Up' }}
    </button>
    <p v-if="error" class="error">{{ error }}</p>
  </form>
</template>

Conditional Submit Button

<script setup lang="ts">
import { computed } from 'vue';
import { useBotSigged } from '@/composables/useBotSigged';

const { canSubmit, isReady, score } = useBotSigged();

const submitState = computed(() => {
  if (!isReady.value) return { disabled: true, text: 'Verifying...' };
  if (score.value && score.value.bot_score > 70) return { disabled: true, text: 'Blocked' };
  return { disabled: false, text: 'Submit' };
});
</script>

<template>
  <button type="submit" :disabled="submitState.disabled">
    {{ submitState.text }}
  </button>
</template>

Reusable Form Wrapper

Create a component that wraps any form with protection:

<!-- ProtectedForm.vue -->

<script setup lang="ts">
import { ref } from 'vue';
import { useBotSigged } from '@/composables/useBotSigged';

const props = defineProps<{
  threshold?: number;
}>();

const emit = defineEmits<{
  submit: [data: FormData];
  blocked: [score: number];
}>();

const { waitUntilReady } = useBotSigged();
const isSubmitting = ref(false);

async function handleSubmit(e: Event) {
  const form = e.target as HTMLFormElement;
  isSubmitting.value = true;

  try {
    const result = await waitUntilReady();
    const threshold = props.threshold ?? 70;

    if (result?.score && result.score > threshold) {
      emit('blocked', result.score);
      return;
    }

    emit('submit', new FormData(form));
  } finally {
    isSubmitting.value = false;
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <slot :isSubmitting="isSubmitting" />
  </form>
</template>

Usage:

<template>
  <ProtectedForm @submit="onSubmit" @blocked="onBlocked" v-slot="{ isSubmitting }">
    <input name="email" type="email" />
    <button :disabled="isSubmitting">Submit</button>
  </ProtectedForm>
</template>

Pinia Store

For complex applications, manage BotSigged state in Pinia:

// stores/botsigged.ts
import { defineStore } from 'pinia';
import { BotSigged, ScoreUpdate } from '@botsigged/sdk';

export const useBotSiggedStore = defineStore('botsigged', {
  state: () => ({
    sdk: null as BotSigged | null,
    score: null as ScoreUpdate | null,
    isConnected: false,
    isReady: false,
  }),

  getters: {
    botScore: (state) => state.score?.bot_score ?? 0,
    classification: (state) => state.score?.classification ?? 'unknown',
    isBot: (state) => (state.score?.bot_score ?? 0) > 70,
  },

  actions: {
    init(apiKey: string) {
      if (this.sdk) return;

      this.sdk = new BotSigged({
        apiKey,
        autoStart: true,
        onScoreUpdate: (score) => {
          this.score = score;
          this.isReady = true;
        },
        onConnectionChange: (connected) => {
          this.isConnected = connected;
        },
      });
    },

    async waitUntilReady() {
      return this.sdk?.waitUntilReady();
    },

    async stop() {
      await this.sdk?.stop();
      this.sdk = null;
      this.isReady = false;
    },
  },
});

Usage in components:

<script setup lang="ts">
import { useBotSiggedStore } from '@/stores/botsigged';
import { storeToRefs } from 'pinia';

const store = useBotSiggedStore();
const { score, isReady, isBot } = storeToRefs(store);

async function handleSubmit() {
  if (store.isBot) {
    alert('Submission blocked');
    return;
  }

  await store.waitUntilReady();
  // submit form
}
</script>

Nuxt 3

Plugin Setup

Create a Nuxt plugin for client-side initialization:

// plugins/botsigged.client.ts
import { BotSigged } from '@botsigged/sdk';

export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig();

  const sdk = new BotSigged({
    apiKey: config.public.botsiggedKey,
    autoStart: true,
  });

  return {
    provide: {
      botSigged: sdk,
    },
  };
});

Configure the runtime key:

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      botsiggedKey: process.env.NUXT_PUBLIC_BOTSIGGED_KEY,
    },
  },
});

Composable for Nuxt

// composables/useBotSigged.ts
export function useBotSigged() {
  const { $botSigged } = useNuxtApp();
  const score = ref<ScoreUpdate | null>(null);
  const isReady = ref(false);

  onMounted(() => {
    // Subscribe to score updates
    $botSigged.onScoreUpdate?.((newScore) => {
      score.value = newScore;
      isReady.value = true;
    });
  });

  return {
    sdk: $botSigged,
    score: readonly(score),
    isReady: readonly(isReady),
    waitUntilReady: () => $botSigged?.waitUntilReady(),
    getSessionId: () => $botSigged?.getSessionId(),
  };
}

Middleware Protection

Protect routes based on bot score:

// middleware/bot-check.ts
export default defineNuxtRouteMiddleware(async (to) => {
  // Only run on client
  if (import.meta.server) return;

  const { $botSigged } = useNuxtApp();
  const result = await $botSigged?.waitUntilReady();

  if (result?.score && result.score > 80) {
    return navigateTo('/blocked');
  }
});

Apply to specific pages:

<!-- pages/checkout.vue -->

<script setup lang="ts">
definePageMeta({
  middleware: ['bot-check'],
});
</script>

VeeValidate Integration

<script setup lang="ts">
import { useForm } from 'vee-validate';
import { useBotSigged } from '@/composables/useBotSigged';
import * as yup from 'yup';

const { waitUntilReady, getSessionId } = useBotSigged();

const schema = yup.object({
  email: yup.string().email().required(),
  message: yup.string().min(10).required(),
});

const { handleSubmit, errors, isSubmitting } = useForm({
  validationSchema: schema,
});

const onSubmit = handleSubmit(async (values) => {
  const result = await waitUntilReady();

  if (result?.score && result.score > 70) {
    throw new Error('Submission blocked');
  }

  await fetch('/api/contact', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-BotSigged-Session': getSessionId() ?? '',
    },
    body: JSON.stringify(values),
  });
});
</script>

<template>
  <form @submit="onSubmit">
    <input name="email" />
    <span>{{ errors.email }}</span>

    <textarea name="message" />
    <span>{{ errors.message }}</span>

    <button :disabled="isSubmitting">Send</button>
  </form>
</template>

Score Display Component

<script setup lang="ts">
import { computed } from 'vue';
import { useBotSigged } from '@/composables/useBotSigged';

const { score, isConnected, isReady } = useBotSigged();

const statusClass = computed(() => {
  if (!isConnected.value) return 'disconnected';
  if (!isReady.value) return 'loading';
  return score.value?.classification ?? 'unknown';
});
</script>

<template>
  <div :class="['score-indicator', statusClass]">
    <template v-if="!isConnected">Disconnected</template>
    <template v-else-if="!isReady">Analyzing...</template>
    <template v-else>
      <span>Score: {{ score?.bot_score ?? 0 }}</span>
      <span>{{ score?.classification }}</span>
    </template>
  </div>
</template>

<style scoped>
.score-indicator {
  padding: 0.5rem 1rem;
  border-radius: 4px;
}
.human { background: #d4edda; }
.suspicious { background: #fff3cd; }
.bot { background: #f8d7da; }
.disconnected, .loading { background: #e2e3e5; }
</style>

Troubleshooting

SSR Hydration Mismatch

The SDK must only run on the client. In Nuxt, use .client.ts suffix for plugins or check import.meta.client:

if (import.meta.client) {
  // Initialize SDK
}

Reactivity Not Updating

Ensure you’re using ref() for SDK state and updating values correctly:

// Wrong - won't trigger reactivity
score = newScore;

// Correct
score.value = newScore;

Multiple Instances

Guard against multiple initializations:

let initialized = false;

export function useBotSigged(apiKey?: string) {
  if (initialized) return existingState;
  initialized = true;
  // ... initialize
}