> Source: https://kaidn.io/docs/quickstart/svelte
> 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. Set the configuration](#step-3)
- [4. Build the signup form](#step-4)
- [5. Collect on submit](#step-5)
- [6. Score it on your server](#step-6)
- [7. Test it](#step-7)
- [The other function](#more)
- [Next steps](#next)

# Svelte quickstart

Add Kaidn to a Svelte or SvelteKit app and give every visitor a stable device id you can send to your backend. The example is the one most people start with: stopping the same person opening account after account.

`@kaidn/svelte` works on **Svelte 4 and 5**, and ships no compiled components, so there is nothing for your build to reconcile.

Estimated time: under 10 minutes

### Before you start

- **Node 20+ and npm.**
- **Svelte 4 or 5.** SvelteKit is assumed below, but a plain Vite Svelte app works the same.
- **A free Kaidn account.** [Register](https://kaidn.io/register): 10,000 events a month, no card.

**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 `setKaidn()` 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

```
npx sv create kaidn-svelte-quickstart
cd kaidn-svelte-quickstart
npm install
npm install @kaidn/svelte
```

## 3. Set the configuration

Call `setKaidn()` once, in the script of your root layout. It holds configuration and **collects nothing on its own**, so it is safe on every route including the ones with no signup on them.

src/routes/+layout.svelte

```
<script>
  import { setKaidn } from "@kaidn/svelte";

  setKaidn({ publishableKey: import.meta.env.VITE_KAIDN_PK });
</script>

<slot />
```

.env

```
VITE_KAIDN_PK=pk_live_your_key_here
```

Vite exposes anything prefixed `VITE_` to the browser, which is what you want here. The publishable key is not a secret, but a key you can rotate without a code change is worth having.

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

A module-level singleton is the obvious shortcut and it is wrong the moment you render on a server: SvelteKit serves many visitors from one Node process, and the last `configure()` to run would win for all of them. Context belongs to a component tree, which is the unit SSR already creates per request. The context key is a `Symbol`, so nothing else can overwrite it.

`setKaidn`must be called during component initialisation, the same rule Svelte's own `setContext` follows.

## 4. Build the signup form

A route to attach to. Create `src/routes/signup/+page.svelte`:

src/routes/signup/+page.svelte

```
<script>
  let email = "";
  let password = "";
  let busy = false;

  async function handleSubmit() {
    busy = true;
    // fills in next step
    busy = false;
  }
</script>

<form class="wrap" on:submit|preventDefault={handleSubmit}>
  <h1>Create an account</h1>

  <label for="email">Email</label>
  <input id="email" bind:value={email} type="email" required
         placeholder="you@example.com" />

  <label for="password">Password</label>
  <input id="password" bind:value={password} type="password" required />

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

<style>
  .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; }
</style>
```

## 5. Collect on submit

`createDeviceId()` 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 routes that do not need it.

src/routes/signup/+page.svelte

```
<script>
  import { createDeviceId } from "@kaidn/svelte";

  const { getDeviceId, isLoading } = createDeviceId();

  let email = "";
  let password = "";

  async function handleSubmit() {
    // Never throws. A blocked script, an ad blocker, or server rendering all
    // return 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,
        password,
        deviceId,          // your server scores THIS
      }),
    });
  }
</script>

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

You get four things back:

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

The three stores auto-subscribe with the `$` prefix, so `$isLoading` works as written. They are handed out read-only on purpose: a consumer that could `.set()` them would be writing a device id nothing collected.

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.

**There is no `immediate` option, and that is deliberate.** [@kaidn/react](https://kaidn.io/docs/quickstart/react) has one only because a hook cannot be called conditionally. Svelte has no such constraint, so if you want it on mount you write `onMount(() => void getDeviceId())` and the intent is on the page rather than hidden in an option object.

## 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.

In SvelteKit that is `src/routes/api/signup/+server.js`, which takes the id and passes it to `/v1/score`:

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.

Your `kdn_live_` secret key belongs in `$env/static/private`, never in anything prefixed `VITE_` or `PUBLIC_`. SvelteKit will refuse the import if you get that the wrong way round, which is one of the nicer guard rails it has.

## 7. Test it

Terminal

```
npm run dev
```

Open `http://localhost:5173/signup`, 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 it returns null instead of throwing, and why the device id is one signal in a score rather than the whole thing.

## The other function

#### watchSession: catch what one fingerprint cannot

`createDeviceId`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/routes/(app)/+layout.svelte

```
<script>
  import { watchSession } from "@kaidn/svelte";

  export let data;

  // Re-beacons the same device about once a minute while the tab is open, so a
  // connection change becomes visible. Stops when this layout is destroyed.
  watchSession({ active: Boolean(data.user) });
</script>

<slot />
```

`active` takes a plain boolean or a store. Pass a store and it also stops the moment that store flips, so a logout ends the observation without a navigation.

**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.

#### Why stores and not runes

`$state` is Svelte 5 only and has to live in a `.svelte.ts` module. `setContext`, `onDestroy` and `svelte/store` are public API in Svelte 3, 4 and 5, and `$deviceId` auto-subscribes the same way in all of them.

For the same reason this package ships no `.svelte` components. Shipping one would mean shipping compiled output tied to the Svelte major it was built against, plus a `svelte` export condition for anyone compiling from source. Everything here is a function call, so it ships as plain modules with nothing for your build to reconcile.

#### Server rendering

`getDeviceId()` returns null when there is no `window`, so a component that calls it during server rendering gets the same null it renders on the client. There is no `import { browser } from "$app/environment"` for you to remember.

#### Consent

One flag stops everything collecting:

src/routes/+layout.svelte

```
setKaidn({ 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.

## 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.

[Node.js backend@kaidn/sdk, what a +server.js route should use](https://kaidn.io/docs#quickstart)[Python backendFastAPI, six steps, the other half of this](https://kaidn.io/docs/quickstart/python)[PHP backendPlain PHP on any host, including shared](https://kaidn.io/docs/quickstart/php)[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.
