> Source: https://kaidn.io/docs/quickstart/vue
> Full documentation index: https://kaidn.io/llms.txt

Docs menuAll pages, quickstarts and this page’s contentsDocs
- [Introduction](https://kaidn.io/docs)
- [Quickstarts](https://kaidn.io/docs/quickstart)
- [Core concepts](https://kaidn.io/docs/concepts)
- [API reference](https://kaidn.io/docs/api)
- [Guides](https://kaidn.io/docs/guides)
- [Keys & dashboard](https://kaidn.io/docs/keys)
- [Glossary](https://kaidn.io/glossary)

Web
- [JavaScript](https://kaidn.io/docs/quickstart/javascript)
- [React](https://kaidn.io/docs/quickstart/react)
- [Next.js](https://kaidn.io/docs/quickstart/nextjs)
- [Preact](https://kaidn.io/docs/quickstart/preact)
- [Vue](https://kaidn.io/docs/quickstart/vue)
- [Nuxt](https://kaidn.io/docs/quickstart/nuxt)
- [Angular](https://kaidn.io/docs/quickstart/angular)
- [Svelte](https://kaidn.io/docs/quickstart/svelte)

Mobile
- Androidsoon
- iOSsoon
- React Nativesoon
- Fluttersoon

Server
- [Node.js](https://kaidn.io/docs#quickstart)
- [PHP](https://kaidn.io/docs/quickstart/php)
- C#/.NETsoon
- Gosoon
- Javasoon
- [Python](https://kaidn.io/docs/quickstart/python)

On this page
- [1. Get your publishable key](#step-1)
- [2. Set up your project](#step-2)
- [3. Build the signup form](#step-3)
- [4. Install the plugin](#step-4)
- [5. Collect on submit](#step-5)
- [6. Score it on your server](#step-6)
- [7. Test it](#step-7)
- [The other composable](#more)
- [Next steps](#next)

# Vue quickstart

Add Kaidn to a Vue 3 app and give every visitor a stable device id you can send to your backend. Scaffolded with Vite, and the example is the one most people start with: stopping the same person opening account after account.

Estimated time: under 10 minutes

### Before you start

- **Node 20+ and npm**, to run Vite.
- **Vue 3.** The Options API works too; the examples use `script setup`.
- **A free Kaidn account.** [Register](https://kaidn.io/register): 10,000 events a month, no card.

**Using Nuxt? You want a different package.** [@kaidn/nuxt](https://kaidn.io/docs/quickstart/nuxt) ships the server half as well, a device-token route and a scoring helper that reads the IP and cookie off the h3 event. This page is for a plain Vue 3 app, where `app.use()` is the idiomatic place for configuration.

**This is the frontend half, and on its own it blocks nothing.** By the end you will have a device id. That only becomes fraud prevention when your server sends it to `/v1/score` and acts on the verdict, so finish with a backend quickstart: [Python](https://kaidn.io/docs/quickstart/python), [PHP](https://kaidn.io/docs/quickstart/php) or [Node.js](https://kaidn.io/docs#quickstart).

## 1. Get your publishable key

- [Create an account](https://kaidn.io/register) if you do not have one.
- Go to **Fraud Scoring API → Device trackers**, create a tracker, and list the domains it may run on. Include `localhost` while you build.
- Copy the **publishable key**. It starts `pk_live_`.

**This key is meant to be visible in your bundle.** It is domain-locked and can only send fingerprints: it cannot score, read your data, or work from a site you did not authorise. Your `kdn_live_` secret key is the opposite, and passing one to `createKaidn()` throws where you wrote it rather than letting it ship to every visitor.

## 2. Set up your project

Skip to step 3 if you have a project already.

Terminal

```
npm create vite@latest kaidn-vue-quickstart -- --template vue
cd kaidn-vue-quickstart
npm install
```

Run it and open `http://localhost:5173`, Vite's default.

Terminal

```
npm run dev
```

## 3. Build the signup form

A component to attach to. Create `src/components/SignupForm.vue`:

src/components/SignupForm.vue

```
<script setup>
import { ref } from "vue";

defineProps({ isLoading: Boolean });
const emit = defineEmits(["submit"]);

const email = ref("");
const password = ref("");
</script>

<template>
  <form class="wrap" @submit.prevent="emit('submit', { email, password })">
    <h1>Create an account</h1>

    <label for="email">Email</label>
    <input id="email" v-model="email" type="email" required
           placeholder="you@example.com" />

    <label for="password">Password</label>
    <input id="password" v-model="password" type="password" required />

    <button type="submit" :disabled="isLoading">
      {{ isLoading ? "Checking…" : "Create account" }}
    </button>
  </form>
</template>
```

And enough CSS to see it. Append to `src/style.css`:

src/style.css

```
.wrap {
  max-width: 380px; min-height: 100vh; margin: 0 auto;
  display: flex; flex-direction: column; justify-content: center; gap: .5rem;
  padding: 1rem; text-align: left;
}

input { padding: .6rem; border: 1px solid #ccc; border-radius: 6px; font: inherit; }
label { font-size: .85rem; color: #555; }

button {
  margin-top: .75rem; padding: .7rem 1.2rem; font: inherit; cursor: pointer;
  background: #111; color: #fff; border: 0; border-radius: 6px;
}
button:disabled { opacity: .6; cursor: not-allowed; }
```

## 4. Install the plugin

Terminal

```
npm install @kaidn/vue
```

Install it once, where you create the app. The plugin holds configuration and **collects nothing on its own**, so it is safe on every page including the ones with no signup on them.

src/main.js

```
import { createApp } from "vue";
import { createKaidn } from "@kaidn/vue";
import App from "./App.vue";
import "./style.css";

const app = createApp(App);

app.use(createKaidn({ publishableKey: "pk_live_your_key_here" }));

app.mount("#app");
```

**For production, use an environment variable.** Vite exposes anything prefixed `VITE_`, so `import.meta.env.VITE_KAIDN_PK` keeps it out of your repository. It is not a secret, but a key you can rotate without a code change is worth having.

#### Why a plugin and not a configure() call

A module-level singleton is the obvious shortcut and it is wrong the moment you render on a server: one Node process serves many visitors, and the last `configure()` to run would win for all of them. `app.use()` scopes the configuration to one app instance, which is the unit SSR already creates per request.

The injection key is a `Symbol`, so nothing else that provides `"kaidn"` can quietly overwrite it.

## 5. Collect on submit

`useDeviceId()` collects when you ask it to, not on mount. That puts the work at the moment somebody acts, which is when the signal is freshest, and keeps fingerprinting off pages that do not need it.

src/App.vue

```
<script setup>
import { useDeviceId } from "@kaidn/vue";
import SignupForm from "./components/SignupForm.vue";

const { getDeviceId, isLoading } = useDeviceId();

async function handleSubmit({ email, password }) {
  // Never throws. A blocked script or an ad blocker returns null, and your
  // signup carries on with one signal fewer.
  const deviceId = await getDeviceId();

  await fetch("/api/signup", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      email: email.value,
      password: password.value,
      deviceId,          // your server scores THIS
    }),
  });
}
</script>

<template>
  <SignupForm :is-loading="isLoading" @submit="handleSubmit" />
</template>
```

The composable gives you four things:

- `getDeviceId()` collects and returns `string | null`. Never throws.
- `deviceId` is a ref holding the last id collected, or null before the first call.
- `isLoading` is a ref, true while a collection is in flight. Bind your button to it.
- `error` is a ref holding whatever went wrong last, for your logging rather than your user.

The three refs unwrap automatically in a template, so `isLoading` works as written above. In `script` you need `.value`.

Note what is *not* here: no verdict and no decision. The browser is an untrusted place to make one, so it never sees a score. It produces an id; your server produces the judgement.

**A double-clicked button fingerprints once, not twice.** The composable keeps one collection in flight at a time, so an impatient visitor costs you one beacon rather than two.

## 6. Score it on your server

This is the step that turns a device id into fraud prevention. Everything before it collects; nothing before it decides.

Your `/api/signup` handler takes the id and passes it to `/v1/score`. Node, since you are already in a JavaScript project:

C#/.NET*soon*Go*soon*Java*soon*

npm install @kaidn/sdk

api/signup.js

```
import { Kaidn } from "@kaidn/sdk";

// Your SECRET key. Server only, never the browser.
const kaidn = new Kaidn({ apiKey: process.env.KAIDN_API_KEY });

// Express, Fastify, Hono, a Next.js route handler: the call is the same.
app.post("/api/signup", async (req, res) => {
  let r;
  try {
    r = await kaidn.score({
      event: "signup",
      ip: req.ip,
      email: req.body.email,
      device_id: req.body.kaidn_device_id,   // the id the browser collected
    });
  } catch {
    // FAIL OPEN. An outage in your fraud vendor must never become an
    // outage in your signup form.
    return res.json({ ok: true });
  }

  if (r.verdict === "block") {
    // Do not name the signal. It teaches the next attempt.
    return res.status(403).json({ error: "We could not create that account." });
  }

  const user = await createUser(req.body);
  if (r.verdict === "review") await flagForReview(user.id, r.event_id, r.reason_text);

  res.json({ ok: true });
});
```

Three answers, and **you** decide what each one means. Kaidn never blocks anybody on your behalf.

## 7. Test it

Terminal

```
npm run dev
```

Open `http://localhost:5173`, fill the form, submit, and look at the network tab. Your `/api/signup` request carries the id:

Output

```
{ "email": "you@example.com", "deviceId": "8f1c2ae9d4b7c3e05a1f6b28d9074e3c" }
```

**Turn ad blockers off on localhost while you test.** Any fingerprinting script can be blocked by an extension, and when that happens you get nothing rather than a cautious answer. We measured what that does to a competitor and [published the numbers](https://kaidn.io/blog/fingerprinting-blocked-agent). That is why the composable returns null instead of throwing, and why the device id is one signal in a score rather than the whole thing.

## The other composable

#### useSessionWatch: catch what one fingerprint cannot

`useDeviceId`answers "who is this" at one instant. Some of the most useful signals only exist across *time*, because they are a change rather than a value: a VPN that drops mid-session and leaks the real home IP, a session that starts masking partway through, one device seen from a dozen addresses. None of those can be read from a single page load.

src/layouts/AccountLayout.vue

```
<script setup>
import { computed } from "vue";
import { useSessionWatch } from "@kaidn/vue";

const props = defineProps({ user: Object });

// Re-beacons the same device about once a minute while the tab is open, so a
// connection change becomes visible. Pass a ref and a logout stops it for you.
useSessionWatch({ active: computed(() => Boolean(props.user)) });
</script>

<template>
  <slot />
</template>
```

`active` takes a plain boolean or a ref. A ref is the normal case: it stops on logout without you remembering to tear anything down, and it stops on unmount either way.

**It costs nothing.** The `/v1/fp` beacon is free and rate-limited; you are billed per scored decision. Watching a session adds signal without adding events, which is why it belongs on a logged-in layout rather than being rationed.

#### Server rendering

`getDeviceId()` returns null when there is no `window`, so the same call is safe in a component that renders on both sides. The `immediate` option runs from `onMounted`, the one lifecycle hook Vue never calls while server-rendering, so it cannot fire on the server and there is no hydration mismatch to work around.

#### Consent

One flag on the plugin stops every composable collecting anything:

src/main.js

```
app.use(createKaidn({ publishableKey: "pk_live_…", enabled: hasConsent }));
```

Fingerprinting reads properties of a visitor's device, which in several jurisdictions needs a lawful basis *before* it happens rather than a note in a policy afterwards. This library will not assume one on your behalf.

#### What this package will never do

Two composables is the whole surface, and that is a boundary rather than a gap. The rest of the API, `config`, `lists`, `label`, `forget`, `events`, `stats`, `batch`, is reachable only with your **secret key**, and a secret key in a browser bundle is a leak found by whoever reads the bundle first. Every one of them lives in the server clients: [full reference](https://kaidn.io/docs/api).

## Next steps

**Nothing is being scored yet.** Send that `deviceId` to your backend and pass it to `/v1/score`, which is where a verdict and a decision actually happen.

[NuxtBoth halves in one package, if you are on Nuxt](https://kaidn.io/docs/quickstart/nuxt)[Python backendFastAPI, six steps, the other half of this](https://kaidn.io/docs/quickstart/python)[Node.js backend@kaidn/sdk, the oldest and most complete client](https://kaidn.io/docs#quickstart)[Device identityWhy a fingerprint is not a person, and the three rungs](https://kaidn.io/docs/concepts#identity)

Worth reading once the loop is closed: [the device token](https://kaidn.io/docs/concepts#device-token), which upgrades a guessed identity to a remembered one and is the single biggest accuracy win available to you.
