@avsbhq/svelte
v1.0.0
Published
Svelte 5 SDK adapter for A vs B. Provides reactive Svelte stores and a context provider for feature flags and A/B experiments. Full SvelteKit support including SSR bootstrap via server hooks.
Readme
@avsbhq/svelte
Svelte 5 SDK adapter for A vs B. Provides reactive Svelte stores and a context provider for feature flags and A/B experiments. Full SvelteKit support including SSR bootstrap via server hooks.
1. Install
npm install @avsbhq/svelte @avsbhq/browser
# peer dependency — Svelte 5 required
npm install svelte@^5.0.0
# optional: SvelteKit integration
npm install @sveltejs/kit@^2.0.02. Quickstart (5-min integration)
Wrap your root layout with AvsbProvider.svelte. All child components can then call the store factories.
src/routes/+layout.svelte
<script lang="ts">
import AvsbProvider from '@avsbhq/svelte/AvsbProvider.svelte'
</script>
<AvsbProvider sdkKey="pub_YOUR_SDK_KEY" context={{ kind: 'user', key: 'anonymous' }}>
{@render children()}
</AvsbProvider>src/routes/+page.svelte
<script lang="ts">
import { flag, flagReady } from '@avsbhq/svelte'
const ready = flagReady()
const darkMode = flag('dark-mode', false)
</script>
{#if $ready}
<p>Dark mode: {$darkMode.value}</p>
{:else}
<p>Loading...</p>
{/if}3. SDK keys
Two key types are used:
- Public key (
pub_...) — used client-side in the browser. Safe to embed in front-end code. Pass toAvsbProviderorcreateAvsbContext. - Server key (
srv_...) — used server-side only (SvelteKit hooks, server load functions). Never expose in browser bundles.
4. Identity (identify, updateAttributes, alias, reset)
Call identify() to replace the current evaluation context. All subsequent flag evaluations use the new context.
<script lang="ts">
import { identify, alias } from '@avsbhq/svelte'
const identifyFn = identify()
const aliasFn = alias()
function onLogin(userId: string) {
const prev = { kind: 'user', key: 'anonymous' }
const next = { kind: 'user', key: userId }
aliasFn(prev, next) // stitch sessions
identifyFn(next) // switch context
}
</script>For attribute-only updates, access the client directly:
<script lang="ts">
import { getAvsbClient } from '@avsbhq/svelte'
const client = getAvsbClient()
client?.updateAttributes({ plan: 'pro' })
</script>5. Multi-context
Pass a MultiContext to evaluate flags against multiple context kinds simultaneously:
<AvsbProvider
sdkKey="pub_..."
context={{
kind: 'multi',
user: { kind: 'user', key: 'u1', plan: 'pro' },
org: { kind: 'org', key: 'org-42', tier: 'enterprise' }
}}
>
{@render children()}
</AvsbProvider>6. Reading flags
All flag stores return Readable<Flag<T>>. Subscribe with Svelte's $ prefix.
Typed stores
<script lang="ts">
import { boolFlag, stringFlag, numberFlag, jsonFlag, flagValue } from '@avsbhq/svelte'
const featureEnabled = boolFlag('new-checkout', false)
const theme = stringFlag('ui-theme', 'light')
const timeout = numberFlag('request-timeout-ms', 5000)
const config = jsonFlag<{ maxItems: number }>('cart-config', { maxItems: 10 })
// Raw value shortcut (Readable<T> instead of Readable<Flag<T>>)
const themeValue = flagValue('ui-theme', 'light')
</script>
{#if $featureEnabled.isEnabled()}
<NewCheckout />
{/if}
<p>Theme: {$themeValue}</p>Full Flag object
<script lang="ts">
import { flag } from '@avsbhq/svelte'
const experiment = flag('checkout-flow', 'control')
</script>
<p>Variation: {$experiment.variationKey}</p>
<p>Source: {$experiment.source}</p>
<p>Exists: {$experiment.exists()}</p>All flags (debug / admin panels)
<script lang="ts">
import { allFlags } from '@avsbhq/svelte'
const flags = allFlags()
</script>
<pre>{JSON.stringify($flags, null, 2)}</pre>Flag shape
Every store value is a Flag<T> object:
| Property | Type | Description |
|---|---|---|
| value | T | The evaluated value |
| variationKey | string \| null | The variation key served |
| source | EvaluationSource | 'rule' \| 'default' \| 'not_found' \| ... |
| ruleId | string \| null | The matched rule identifier |
| ruleType | RuleType \| null | 'ab_test' \| 'feature_flag' \| ... |
| reasons | string[] | Evaluation reason chain |
| isEnabled() | () => boolean | source is rule-like and value is truthy |
| exists() | () => boolean | Flag key was found in the datafile |
7. Tracking events
<script lang="ts">
import { getTrack } from '@avsbhq/svelte'
const track = getTrack()
</script>
<button onclick={() => track('add_to_cart', { value: 29.99, properties: { sku: 'ABC' } })}>
Add to cart
</button>getTrack() returns a stable function. If the client is still loading, calls are silently no-ops.
8. Error handling
Pass a logger in the options to capture SDK-internal warnings and errors:
<AvsbProvider
sdkKey="pub_..."
context={{ kind: 'user', key: 'u1' }}
logger={{ warn: console.warn, error: console.error }}
>
{@render children()}
</AvsbProvider>For reactive error state, access the client's event bus directly:
<script lang="ts">
import { getAvsbClient } from '@avsbhq/svelte'
import { onDestroy } from 'svelte'
const client = getAvsbClient()
let sdkError = $state<Error | undefined>(undefined)
const unsub = client?.on('error', (err) => { sdkError = err })
onDestroy(() => unsub?.())
</script>
{#if sdkError}
<p>SDK error: {sdkError.message}</p>
{/if}9. SSR / hydration
SvelteKit server hooks
Wrap your handle in src/hooks.server.ts to inject a per-request bound client into event.locals:
// src/hooks.server.ts
import { withAvsbHooks } from '@avsbhq/svelte/sveltekit'
import { AvsbServerClient } from '@avsbhq/node'
import type { Handle } from '@sveltejs/kit'
const serverClient = new AvsbServerClient({ sdkKey: 'srv_...' })
await serverClient.onReady()
export const handle: Handle = withAvsbHooks(
({ event, resolve }) => resolve(event),
{
serverClient,
resolveContext: ({ cookies }) => ({
kind: 'user',
key: cookies.get('user_id') ?? 'anonymous',
}),
}
)Extend App.Locals in src/app.d.ts:
import type { AvsbLocals } from '@avsbhq/svelte/sveltekit'
declare global {
namespace App {
interface Locals {
avsb: AvsbLocals
}
}
}Server load functions
// src/routes/+layout.server.ts
import { serializeAvsbBootstrap } from '@avsbhq/svelte/sveltekit'
import type { LayoutServerLoad } from './$types'
export const load: LayoutServerLoad = ({ locals }) => ({
avsbBootstrap: serializeAvsbBootstrap(locals.avsb.client),
})Pass the snapshot to the client-side AvsbProvider to skip the loading flash:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import AvsbProvider from '@avsbhq/svelte/AvsbProvider.svelte'
let { data, children } = $props()
</script>
<AvsbProvider
sdkKey="pub_..."
context={{ kind: 'user', key: 'u1' }}
bootstrap={data.avsbBootstrap}
>
{@render children()}
</AvsbProvider>10. Graceful shutdown
In Mode A (provider owns the client), the client is closed automatically when AvsbProvider is destroyed. In Mode B (you pass a pre-built client), call close() yourself:
import { AvsbClient } from '@avsbhq/browser'
const client = new AvsbClient({ sdkKey: 'pub_...' })
// Later, on app teardown:
await client.close() // flushes pending events and closes connections11. Testing (@avsbhq/test recipes)
All store factories accept an optional ctx parameter that bypasses Svelte context, making them trivially testable without mounting components:
import { describe, it, expect, vi } from 'vitest'
import { get } from 'svelte/store'
import { createFlag, notFoundFlag } from '@avsbhq/core'
import { flag, boolFlag, getTrack } from '@avsbhq/svelte'
import type { AvsbContextValue } from '@avsbhq/svelte'
import type { SvelteAvsbClient } from '@avsbhq/svelte'
const FLAG_ON = createFlag<boolean>({
value: true, variationKey: 'on', source: 'rule',
ruleId: 'r1', ruleType: 'ab_test', reasons: [],
})
function makeMockClient(): SvelteAvsbClient {
const listeners = new Map<string, Set<() => void>>()
return {
subscribe: (key, cb) => {
if (!listeners.has(key)) listeners.set(key, new Set())
listeners.get(key)!.add(cb)
return () => listeners.get(key)?.delete(cb)
},
getSnapshot: (key, def) => FLAG_ON as never ?? notFoundFlag(def, 'no data'),
// ... other methods
} as SvelteAvsbClient
}
function makeCtx(client: SvelteAvsbClient | null): AvsbContextValue {
return { client, status: client ? 'ready' : 'loading' }
}
describe('my feature', () => {
it('reads the flag correctly', () => {
const client = makeMockClient()
const store = flag('my-flag', false, makeCtx(client))
expect(get(store).value).toBe(true)
})
})12. Migration from LaunchDarkly
| LaunchDarkly (React SDK) | @avsbhq/svelte |
|---|---|
| <LDProvider clientSideID="..."> | <AvsbProvider sdkKey="pub_..."> |
| useFlags()['flag-key'] | $flag('flag-key', defaultValue) |
| useLDClient() | getAvsbClient() |
| useTrack()('event') | getTrack()('event') |
| asyncWithLDProvider | withAvsbHooks (SvelteKit hooks) |
Key differences:
- All flag reads require a
defaultValue— there is no "undefined if not found" path. - Flag stores return
Flag<T>(full evaluation metadata), not rawT. UseflagValue()for the raw value shortcut. isEnabled()means "rule-served and truthy" — distinct fromvalue === true.
