Stellar SDK & REST API

The Stellar SDK lets you A/B test components, flows and features inside your own code. React and Next.js get first-class bindings; Vue, Svelte and plain JavaScript use the framework-agnostic core; any other language or backend can use the REST API. You create a code experiment in the Stellar dashboard, branch on the assigned variant in your code, and read results, goals and statistical significance in the same dashboard you use for visual experiments.

Early access: the SDK is new and currently in early access. If you hit a rough edge, write to hello@gostellar.app and we will help you get set up.

Overview

The SDK ships as two packages:

  • @gostellar-app/sdk — the framework-agnostic core. Zero dependencies, about 5 KB gzipped. It works in any browser framework: use it directly from Vue, Svelte, plain JavaScript or anything else that runs in the browser.
  • @gostellar-app/react — a thin layer on top of the core: a provider, hooks and a declarative component for React 17+, including Next.js App Router and Pages Router client components.

React has first-class bindings. Every other framework uses the core directly (see Other frameworks), and any language or backend can evaluate experiments and record conversions through the REST API.

The workflow is:

  1. In the Stellar dashboard, create a Code experiment, give it a key (for example new-dashboard) and variant keys (for example control and treatment).
  2. Attach conversion goals and targeting, then launch it.
  3. In your app, read the variant (with a React hook or component, the core client, or a REST call) and render the matching UI.

Assignment is deterministic and sticky: it is computed in the browser from the visitor id, so the same visitor sees the same variant across routes, refreshes and return visits. There is no round trip per evaluation and no extra request on the critical path beyond one cached config fetch, which means no flicker for client-rendered components.

Server-side rendering is safe. The SDK is a no-op on the server and renders the fallback until the client is ready. It does not evaluate experiments on the server or inside React Server Components; read variants from client components.

Install

# React / Next.js
npm install @gostellar-app/sdk @gostellar-app/react

# Vue, Svelte, plain JavaScript or any other framework
npm install @gostellar-app/sdk

You will also need your project's Stellar API key from the dashboard. Expose it to the browser as a public environment variable, for example NEXT_PUBLIC_STELLAR_API_KEY in Next.js or VITE_STELLAR_API_KEY in a Vite project.

Set up the provider

Create one client with createStellar and wrap your app in StellarProvider once. Every hook and component below reads from that provider.

Next.js App Router

The provider uses browser APIs, so it lives in a client component that you render from your root layout:

// app/providers.tsx
'use client';
import { createStellar } from '@gostellar-app/sdk';
import { StellarProvider } from '@gostellar-app/react';

const stellar = createStellar({
  apiKey: process.env.NEXT_PUBLIC_STELLAR_API_KEY,
});

export function Providers({ children }) {
  return <StellarProvider client={stellar}>{children}</StellarProvider>;
}
// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Next.js Pages Router

// pages/_app.tsx
import { createStellar } from '@gostellar-app/sdk';
import { StellarProvider } from '@gostellar-app/react';

const stellar = createStellar({
  apiKey: process.env.NEXT_PUBLIC_STELLAR_API_KEY,
});

export default function App({ Component, pageProps }) {
  return (
    <StellarProvider client={stellar}>
      <Component {...pageProps} />
    </StellarProvider>
  );
}

Plain React or other frameworks

In a non-Next.js React app, wrap your root component the same way. If you are not using React at all, there is no provider: create the client once in a module and use it directly. See Other frameworks for Vue, Svelte and plain JavaScript.

Read a variant

With the hook

useStellarVariant(experimentKey) returns the variant key assigned to the current visitor. Branch on it in any client component:

'use client';
import { useStellarVariant, useTrack } from '@gostellar-app/react';

function Dashboard() {
  const variant = useStellarVariant('new-dashboard');
  const track = useTrack();

  return variant === 'treatment'
    ? <NewDashboard onUpgrade={() => track('upgraded-plan')} />
    : <CurrentDashboard />;
}

With the declarative component

If you prefer to keep the branching in JSX, use StellarExperiment with one StellarVariant per variant key. Only the assigned variant renders:

import { StellarExperiment, StellarVariant } from '@gostellar-app/react';

<StellarExperiment experiment="new-onboarding">
  <StellarVariant name="control">
    <CurrentOnboarding />
  </StellarVariant>
  <StellarVariant name="treatment">
    <NewOnboarding />
  </StellarVariant>
</StellarExperiment>

Feature-flag style

For a gradual rollout of a single feature, useStellarFeature gives you a boolean instead of a variant key:

import { useStellarFeature } from '@gostellar-app/react';

function Checkout() {
  const newCheckoutEnabled = useStellarFeature('new-checkout');
  return newCheckoutEnabled ? <NewCheckout /> : <CurrentCheckout />;
}

Fallback behaviour: before the config has loaded, on the server, or when the experiment is not running, the hook resolves to the fallback and the control branch renders. Write your components so that the control branch is always a complete, working UI.

Other frameworks

@gostellar-app/sdk is plain JavaScript with zero dependencies, so it works in any browser framework. The pattern is always the same: create the client once in a module, wait for ready(), call activate(key) where the variant becomes visible, and use subscribe() to react when the assignment changes (for example after identify or setAttributes). There is no official Vue or Svelte package; the small wrappers below are all you need.

Start by creating the client once, in its own module:

// stellar.js
import { createStellar } from '@gostellar-app/sdk';

export const stellar = createStellar({
  apiKey: import.meta.env.VITE_STELLAR_API_KEY,
});

Vue

With the Composition API, wrap the client in a small composable that exposes the variant as a ref. It waits for the config in onMounted, records the exposure with activate and keeps the ref in sync through subscribe:

// useStellarVariant.js
import { ref, onMounted, onUnmounted } from 'vue';
import { stellar } from './stellar';

export function useStellarVariant(key, fallback = 'control') {
  const variant = ref(fallback);
  let unsubscribe;

  onMounted(async () => {
    await stellar.ready();
    variant.value = stellar.activate(key, fallback);
    unsubscribe = stellar.subscribe(() => {
      variant.value = stellar.getVariant(key, fallback);
    });
  });

  onUnmounted(() => unsubscribe && unsubscribe());

  return variant;
}

Then branch on it in any component:

<script setup>
import { useStellarVariant } from './useStellarVariant';
import { stellar } from './stellar';

const variant = useStellarVariant('new-checkout');
</script>

<template>
  <NewCheckout
    v-if="variant === 'treatment'"
    @upgrade="stellar.track('upgraded-plan')"
  />
  <CurrentCheckout v-else />
</template>

Svelte

In Svelte, expose the variant as a readable store. The store starts at the fallback, records the exposure once the config is ready and updates whenever the assignment changes:

// stellarVariant.js
import { readable } from 'svelte/store';
import { stellar } from './stellar';

export function stellarVariant(key, fallback = 'control') {
  return readable(fallback, (set) => {
    let unsubscribe;
    stellar.ready().then(() => {
      set(stellar.activate(key, fallback));
      unsubscribe = stellar.subscribe(() => {
        set(stellar.getVariant(key, fallback));
      });
    });
    return () => unsubscribe && unsubscribe();
  });
}
<script>
  import { stellarVariant } from './stellarVariant';
  import { stellar } from './stellar';

  const variant = stellarVariant('new-checkout');
</script>

{#if $variant === 'treatment'}
  <NewCheckout on:upgrade={() => stellar.track('upgraded-plan')} />
{:else}
  <CurrentCheckout />
{/if}

Vanilla JavaScript and other frameworks

Without a framework (or in Angular, Solid, Alpine, Astro islands, jQuery and so on), use the client directly. ready() never rejects, so it is safe to await unconditionally:

import { stellar } from './stellar';

await stellar.ready();

const variant = stellar.activate('new-checkout');

if (variant === 'treatment') {
  renderNewCheckout();
} else {
  renderCurrentCheckout();
}

// later, when the visitor converts
stellar.track('upgraded-plan');

Everything on the client is available here: getVariant (evaluate only), activate (evaluate and record exposure), exposure, getExperiment, track, identify, setAttributes, reset, subscribe, override, refresh, isReady and getConfig. On the server the client is a no-op that returns the fallback, so the same module can be imported in SSR code without guards.

Count exposure where the user can see it. Call activate (or getVariant followed by exposure) only at the point where the difference is actually visible to the visitor, not in a global bootstrap. Exposing visitors who never reach the test dilutes your results.

Record conversions

Attach one or more conversion goals to the experiment in the dashboard, then fire the matching event from your code. In React use the useTrack hook; from plain JavaScript call stellar.track():

// React
const track = useTrack();
track('upgraded-plan');

// Core
stellar.track('upgraded-plan');

The event name must match the goal configured for the experiment in the dashboard. Goals, segments and statistical significance for code experiments work the same way they do for visual experiments. To record a conversion from a backend (for example when an order is confirmed server-side), use the track endpoint of the REST API.

Identify users and attributes

Visitors are tracked anonymously by default. When a user logs in, call identify with a stable user id and any attributes you want to target on. Call setAttributes whenever those attributes change, and reset on logout so the next visitor on the same device starts fresh:

stellar.identify(user.id, {
  plan: 'pro',
  accountAgeDays: 42,
  orgType: 'agency',
});

// later, when something changes
stellar.setAttributes({ plan: 'scale' });

// on logout
stellar.reset();

Targeting and traffic

Code experiments support the following targeting rules:

  • Device type.
  • New vs returning visitors.
  • Custom user attributes that you pass through identify or setAttributes, such as plan, account age or organization type.

Traffic allocation and mutual exclusion between experiments are managed in the dashboard, not in code. Change the percentage of visitors in an experiment or the split between variants at any time without a deploy.

Previewing a variant

To see a specific variant regardless of your assignment, open your app with the stellar_force query parameter set to experiment-key:variant-key:

https://app.example.com/dashboard?stellar_force=new-dashboard:treatment

A forced variant is a preview only. No exposure is recorded, so previewing does not affect your results.

How exposure is counted

A visitor counts toward an experiment only when the variant component actually renders, not when the variant is merely evaluated. This keeps results from being diluted by visitors who never saw the test, for example when an experiment lives on a page most users never reach, or when you read a variant early to prepare data.

  • React hooks and components record exposure when the component that renders the variant mounts.
  • stellar.getVariant(experimentKey) evaluates the assignment without counting it. Use it when you need to know the variant before anything is shown.
  • stellar.activate(experimentKey) records the exposure explicitly. Call it at the moment the variant becomes visible when you are using the core without React.

Running alongside the visual snippet

The SDK and the visual snippet work side by side. A site can run visual experiments created in the editor and code experiments created with the SDK at the same time; the visual editor keeps working as before.

Both share the same visitor id, so one visitor gets one variant wherever the experiment is evaluated, and heatmaps, funnels and conversions line up across both kinds of experiments.

Allowed origins

The SDK is allowed to run on your project's domain out of the box. If your app is served from a different origin than the project domain (for example app.example.com when the project is example.com, or localhost during development), add it under Additional allowed origins in your Account settings. Requests from origins that are not allowed are rejected and the SDK falls back to control.

REST API (any language or backend)

If you are not running JavaScript in the browser (a Go, Python, Ruby, PHP or .NET backend, a mobile app, an edge function, a server-rendered template), use the REST API. It exposes the same three operations the SDK performs: fetch the config, decide a variant and track a conversion. Assignment uses the same deterministic hash as the JS SDK, so a visitor gets the same variant whichever path evaluates it.

Authentication is the project's public API key, passed in the URL or the body. It is the same key the snippet and the SDK use. No secret is required because nothing sensitive is returned; for the same reason, do not put secrets in targeting rule values.

Visitor ids are yours to manage. Supply a stable visitorId for each visitor (for example a first-party cookie or a hash of the session) and send the same value to decide and to track, otherwise conversions cannot be attributed.

Config

GET https://api.gostellar.app/public/sdk/config/:apiKey

Returns every running code experiment for the project. The response is cached and supports ETag / If-None-Match, so poll it cheaply. Use it when you want to evaluate experiments yourself or list what is running; for assignment, prefer the decide endpoint.

{
  "v": 1,
  "projectId": 123,
  "experiments": [
    {
      "id": 42,
      "key": "new-checkout",
      "name": "New checkout",
      "traffic": 100,
      "identity": "visitor",
      "variants": [
        { "id": 1, "key": "control", "name": "Control", "weight": 50, "isControl": true },
        { "id": 2, "key": "treatment", "name": "Treatment", "weight": 50, "isControl": false }
      ],
      "excludedExperimentIds": [],
      "targetRules": null,
      "goals": [{ "id": 7, "key": "upgraded-plan", "type": "CUSTOM", "isMain": true }]
    }
  ],
  "goals": [{ "id": 7, "key": "upgraded-plan", "type": "CUSTOM" }]
}

Decide

GET  https://api.gostellar.app/public/sdk/decide/:apiKey?visitorId=…&experiment=new-checkout
POST https://api.gostellar.app/public/sdk/decide/:apiKey

Evaluates one experiment (or every running code experiment when experiment is omitted) for a visitor. The query parameters on GET and the JSON body fields on POST are the same:

  • visitorId (required) — a stable id for the visitor.
  • experiment — the experiment key. Omit it to get a decision for every running code experiment.
  • userId — the logged-in user id, for experiments that bucket by user.
  • attributes — a JSON object of custom attributes for targeting (URL-encoded on GET).
  • device — mobile, tablet or desktop, for device targeting.
  • returning — true or false, for new vs returning targeting.
  • expose — true records the exposure so the visitor is counted in results. Without it, decide only evaluates.
  • ip and userAgent (POST only) — forward the real visitor's values from your backend so analytics reflect the visitor, not your server.
curl -X POST https://api.gostellar.app/public/sdk/decide/YOUR_API_KEY \
  -H 'Content-Type: application/json' \
  -d '{
    "visitorId": "v_8f3a2c",
    "experiment": "new-checkout",
    "userId": "user_123",
    "attributes": { "plan": "pro" },
    "device": "desktop",
    "returning": true,
    "expose": true,
    "ip": "203.0.113.4",
    "userAgent": "Mozilla/5.0 ..."
  }'
{
  "visitorId": "v_8f3a2c",
  "decisions": [
    {
      "experiment": "new-checkout",
      "experimentId": 42,
      "variant": "treatment",
      "variantId": 2,
      "eligible": true,
      "reason": "assigned",
      "warnings": []
    }
  ]
}

reason is one of assigned, traffic (held out by traffic allocation), target_rules (excluded by targeting), unknown_experiment or no_variants. Treat any decision with eligible: false as the control branch, the same way the SDK falls back.

The same call from Python, recording the exposure:

import requests

res = requests.post(
    "https://api.gostellar.app/public/sdk/decide/YOUR_API_KEY",
    json={
        "visitorId": visitor_id,
        "experiment": "new-checkout",
        "expose": True,
        "ip": request.remote_addr,
        "userAgent": request.headers.get("User-Agent"),
    },
    timeout=2,
)
decision = res.json()["decisions"][0]

if decision["variant"] == "treatment":
    render_new_checkout()
else:
    render_current_checkout()

Server-side limitations: country rules and mutual exclusion between experiments are not evaluated by the decide endpoint. When an experiment uses either, the decision includes a warning in warnings and you should treat the result accordingly. Everything else (traffic, device, new vs returning and custom attributes) is evaluated exactly as in the SDK.

Track

POST https://api.gostellar.app/public/sdk/track

Records a conversion for a visitor and attributes it to the experiments the visitor was previously exposed to, through decide with expose=true or through the SDK in the browser. Use experiments to limit attribution to specific keys and idempotencyKey (for example an order id) so retries do not double count:

curl -X POST https://api.gostellar.app/public/sdk/track \
  -H 'Content-Type: application/json' \
  -d '{
    "apiKey": "YOUR_API_KEY",
    "visitorId": "v_8f3a2c",
    "goal": "upgraded-plan",
    "experiments": ["new-checkout"],
    "idempotencyKey": "order_123"
  }'
{ "recorded": 1, "experiments": ["new-checkout"], "warnings": [] }

recorded is the number of experiments the conversion was attributed to. recorded: 0 means the visitor had no prior exposure, usually because decide was called without expose=true, or because a different visitorId was sent to track than to decide.

Troubleshooting

The variant is always control

Check these in order:

  • The experiment is not launched. Draft and paused experiments always resolve to control. Launch it from the dashboard.
  • Key typo. The experiment key and variant keys in code must match the dashboard exactly, including case and dashes.
  • Your app's origin is not allowed. Add it under Additional allowed origins (see above). Check the browser console and network tab for a rejected config request.
  • No goal attached. An experiment without a conversion goal cannot report results. Attach at least one goal before launching.
  • Targeting excludes you. If the experiment targets a device, new or returning visitors, or an attribute you have not set with identify, you will not be assigned. Use ?stellar_force=experiment-key:variant-key to preview a variant while you verify the rules.

No conversions show up

  • Make sure the event name passed to track matches the goal configured for the experiment.
  • Conversions only count for visitors who were exposed to the experiment. If the variant component never rendered, there is no exposure to attribute the conversion to.

Hydration warnings

If you render different markup on the server and the client based on the variant, React will warn about a hydration mismatch. Read variants from client components only and let the fallback render until the SDK is ready; do not derive variant-specific markup during server rendering.

Need help? If something does not behave as described here, contact us at hello@gostellar.app with your experiment key and a short description of your setup.