Guides

Svelte Integration

Store-based patterns for Svelte and SvelteKit applications

Svelte Integration

This guide covers Svelte-specific patterns including stores, SvelteKit integration, form actions, and SSR considerations. Svelte’s reactive stores pair naturally with BotSigged’s real-time updates.

Installation

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

Svelte Store

Creating the Store

// lib/stores/botsigged.ts
import { writable, derived, get } from 'svelte/store';
import { browser } from '$app/environment';
import { BotSigged, ScoreUpdate } from '@botsigged/sdk';

function createBotSiggedStore() {
  const sdk = writable<BotSigged | null>(null);
  const score = writable<ScoreUpdate | null>(null);
  const isConnected = writable(false);
  const isReady = writable(false);

  function init(apiKey: string) {
    if (!browser) return;

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

    sdk.set(instance);
  }

  async function waitUntilReady() {
    const instance = get(sdk);
    return instance?.waitUntilReady();
  }

  function getSessionId() {
    const instance = get(sdk);
    return instance?.getSessionId();
  }

  function canSubmit() {
    const instance = get(sdk);
    return instance?.canSubmit() ?? { allowed: false, reason: 'SDK not initialized' };
  }

  async function stop() {
    const instance = get(sdk);
    await instance?.stop();
    sdk.set(null);
    isReady.set(false);
  }

  // Derived stores
  const botScore = derived(score, ($score) => $score?.bot_score ?? 0);
  const classification = derived(score, ($score) => $score?.classification ?? 'unknown');
  const isBot = derived(botScore, ($botScore) => $botScore > 70);

  return {
    // State
    sdk,
    score,
    isConnected,
    isReady,
    botScore,
    classification,
    isBot,
    // Methods
    init,
    waitUntilReady,
    getSessionId,
    canSubmit,
    stop,
  };
}

export const botSigged = createBotSiggedStore();

Initializing in Layout

<!-- routes/+layout.svelte -->

<script lang="ts">
  import { onMount } from 'svelte';
  import { botSigged } from '$lib/stores/botsigged';
  import { PUBLIC_BOTSIGGED_KEY } from '$env/static/public';

  onMount(() => {
    botSigged.init(PUBLIC_BOTSIGGED_KEY);
  });
</script>

<slot />

Form Protection

Basic Protected Form

<!-- components/SignupForm.svelte -->

<script lang="ts">
  import { botSigged } from '$lib/stores/botsigged';

  let email = '';
  let isSubmitting = false;
  let error: string | null = null;

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

    try {
      const result = await botSigged.waitUntilReady();

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

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

<form on:submit|preventDefault={handleSubmit}>
  <input bind:value={email} type="email" required />
  <button type="submit" disabled={isSubmitting}>
    {isSubmitting ? 'Submitting...' : 'Sign Up'}
  </button>
  {#if error}
    <p class="error">{error}</p>
  {/if}
</form>

Reactive Submit Button

<script lang="ts">
  import { botSigged } from '$lib/stores/botsigged';

  const { isReady, isBot } = botSigged;

  $: buttonText = !$isReady ? 'Verifying...' : $isBot ? 'Blocked' : 'Submit';
  $: buttonDisabled = !$isReady || $isBot;
</script>

<button type="submit" disabled={buttonDisabled}>
  {buttonText}
</button>

Form Action Component

Create a reusable action for form protection:

// lib/actions/protectedSubmit.ts
import { botSigged } from '$lib/stores/botsigged';

interface ProtectedSubmitOptions {
  threshold?: number;
  onBlocked?: (score: number) => void;
}

export function protectedSubmit(node: HTMLFormElement, options: ProtectedSubmitOptions = {}) {
  const { threshold = 70, onBlocked } = options;

  async function handleSubmit(event: SubmitEvent) {
    event.preventDefault();

    const result = await botSigged.waitUntilReady();

    if (result?.score && result.score > threshold) {
      onBlocked?.(result.score);
      return;
    }

    // Re-dispatch the submit event
    node.dispatchEvent(new CustomEvent('protectedSubmit', {
      detail: new FormData(node),
    }));
  }

  node.addEventListener('submit', handleSubmit);

  return {
    destroy() {
      node.removeEventListener('submit', handleSubmit);
    },
    update(newOptions: ProtectedSubmitOptions) {
      // Update options if needed
    },
  };
}

Usage:

<script lang="ts">
  import { protectedSubmit } from '$lib/actions/protectedSubmit';

  function onSubmit(event: CustomEvent<FormData>) {
    const formData = event.detail;
    // Process form data
  }

  function onBlocked(score: number) {
    alert(`Blocked with score: ${score}`);
  }
</script>

<form
  use:protectedSubmit={{ threshold: 60, onBlocked }}
  on:protectedSubmit={onSubmit}
>
  <input name="email" type="email" />
  <button type="submit">Submit</button>
</form>

SvelteKit Integration

Form Actions

Integrate with SvelteKit’s form actions:

<!-- routes/signup/+page.svelte -->

<script lang="ts">
  import { enhance } from '$app/forms';
  import { botSigged } from '$lib/stores/botsigged';
  import type { ActionData } from './$types';

  export let form: ActionData;

  let isSubmitting = false;
</script>

<form
  method="POST"
  use:enhance={async ({ formData, cancel }) => {
    isSubmitting = true;

    const result = await botSigged.waitUntilReady();

    if (result?.score && result.score > 70) {
      cancel();
      isSubmitting = false;
      return;
    }

    // Add session ID to form data
    formData.append('sessionId', botSigged.getSessionId() ?? '');

    return async ({ update }) => {
      await update();
      isSubmitting = false;
    };
  }}
>
  <input name="email" type="email" required />
  <button disabled={isSubmitting}>Sign Up</button>
</form>

{#if form?.error}
  <p class="error">{form.error}</p>
{/if}

Server-side action:

// routes/signup/+page.server.ts
import type { Actions } from './$types';
import { fail } from '@sveltejs/kit';

export const actions: Actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    const email = data.get('email');
    const sessionId = data.get('sessionId');

    // Optionally verify session server-side
    if (sessionId) {
      const session = await fetch(`${BOTSIGGED_API}/sessions/${sessionId}`);
      const { bot_score } = await session.json();

      if (bot_score > 70) {
        return fail(403, { error: 'Request blocked' });
      }
    }

    // Process signup
    return { success: true };
  },
};

Hooks for Server-Side Verification

// hooks.server.ts
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
  const sessionId = event.request.headers.get('X-BotSigged-Session');

  if (sessionId && event.url.pathname.startsWith('/api/')) {
    // Verify session with BotSigged API
    try {
      const response = await fetch(`${BOTSIGGED_API}/sessions/${sessionId}`);
      const { bot_score } = await response.json();

      if (bot_score > 80) {
        return new Response('Forbidden', { status: 403 });
      }
    } catch {
      // Continue if verification fails
    }
  }

  return resolve(event);
};

Load Function Integration

// routes/checkout/+page.ts
import type { PageLoad } from './$types';
import { browser } from '$app/environment';
import { redirect } from '@sveltejs/kit';
import { botSigged } from '$lib/stores/botsigged';
import { get } from 'svelte/store';

export const load: PageLoad = async () => {
  if (browser) {
    const result = await botSigged.waitUntilReady();

    if (result?.score && result.score > 80) {
      throw redirect(307, '/blocked');
    }
  }

  return {};
};

Score Display Component

<!-- components/BotScoreIndicator.svelte -->

<script lang="ts">
  import { botSigged } from '$lib/stores/botsigged';

  const { score, isConnected, isReady, classification } = botSigged;

  $: statusClass = !$isConnected
    ? 'disconnected'
    : !$isReady
      ? 'loading'
      : $classification;
</script>

<div class="score-indicator {statusClass}">
  {#if !$isConnected}
    Disconnected
  {:else if !$isReady}
    Analyzing...
  {:else}
    <span>Score: {$score?.bot_score ?? 0}</span>
    <span>{$classification}</span>
  {/if}
</div>

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

Superforms Integration

If using Superforms for form handling:

<script lang="ts">
  import { superForm } from 'sveltekit-superforms/client';
  import { botSigged } from '$lib/stores/botsigged';

  export let data;

  const { form, errors, enhance, submitting } = superForm(data.form, {
    onSubmit: async ({ cancel }) => {
      const result = await botSigged.waitUntilReady();

      if (result?.score && result.score > 70) {
        cancel();
        // Show error
      }
    },
  });
</script>

<form method="POST" use:enhance>
  <input name="email" bind:value={$form.email} />
  {#if $errors.email}
    <span class="error">{$errors.email}</span>
  {/if}

  <button disabled={$submitting}>Submit</button>
</form>

Context API Alternative

For component-scoped state:

<!-- routes/+layout.svelte -->

<script lang="ts">
  import { setContext, onMount } from 'svelte';
  import { writable } from 'svelte/store';
  import { BotSigged } from '@botsigged/sdk';
  import { PUBLIC_BOTSIGGED_KEY } from '$env/static/public';

  const sdk = writable<BotSigged | null>(null);
  const score = writable(null);

  setContext('botSigged', { sdk, score });

  onMount(() => {
    const instance = new BotSigged({
      apiKey: PUBLIC_BOTSIGGED_KEY,
      onScoreUpdate: (s) => score.set(s),
    });
    sdk.set(instance);

    return () => instance.stop();
  });
</script>

<slot />

Child components:

<script lang="ts">
  import { getContext } from 'svelte';

  const { sdk, score } = getContext('botSigged');
</script>

Runes (Svelte 5)

Using Svelte 5 runes for reactive state:

// lib/botsigged.svelte.ts
import { BotSigged, ScoreUpdate } from '@botsigged/sdk';

class BotSiggedState {
  sdk = $state<BotSigged | null>(null);
  score = $state<ScoreUpdate | null>(null);
  isConnected = $state(false);
  isReady = $state(false);

  botScore = $derived(this.score?.bot_score ?? 0);
  classification = $derived(this.score?.classification ?? 'unknown');
  isBot = $derived(this.botScore > 70);

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

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

  getSessionId() {
    return this.sdk?.getSessionId();
  }
}

export const botSigged = new BotSiggedState();

Usage:

<script lang="ts">
  import { botSigged } from '$lib/botsigged.svelte';

  async function handleSubmit() {
    if (botSigged.isBot) {
      alert('Blocked');
      return;
    }
    // submit
  }
</script>

<p>Score: {botSigged.botScore}</p>
<button disabled={botSigged.isBot}>Submit</button>

Troubleshooting

SSR Errors

The SDK must only run in the browser. Always check:

import { browser } from '$app/environment';

if (browser) {
  botSigged.init(apiKey);
}

Store Not Reactive

Ensure you’re using the $ prefix to subscribe:

<!-- Wrong -->

{botSigged.score}

<!-- Correct -->

{$botSigged.score}

Or with destructuring:

<script>
  const { score } = botSigged;
</script>

{$score}

Memory Leaks

Clean up on component unmount:

<script>
  import { onDestroy } from 'svelte';

  const unsubscribe = botSigged.score.subscribe((s) => {
    // handle score
  });

  onDestroy(unsubscribe);
</script>

Or use auto-subscription with $:

<!-- Auto-unsubscribes when component is destroyed -->

{$score}