Documentation

Integrate Elaan in an afternoon

One API for an in-app inbox, email, and push. Drop a component into your frontend, trigger events from your backend, and Elaan handles rendering, delivery, and real-time updates across every channel.

On this page Overview Quickstart SDK packages Authentication Trigger a notification Sync contacts Self-hosting

Overview

Elaan is real-time notification infrastructure. You model notification types and templates once, then trigger a single event whenever something happens — Elaan fans it out to each channel the recipient has enabled (in-app inbox, email, push), rendering per-brand templates and delivering asynchronously.

Integrating takes three pieces, and this guide covers all of them:

  • Your frontend drops in a prebuilt inbox, bell, and preferences UI — or uses the headless hooks/stores to build your own.
  • Your backend mints short-lived contact tokens for the browser and triggers notification events.
  • Elaan resolves channels, renders templates, and delivers — pushing live updates to connected clients.

How it works

  1. Your backend mints a contact token. Call POST /v1/contacts/tokens with your service key and a contact's external_id. You get back a short-lived token scoped to that one contact.
  2. Your frontend renders the UI. Hand the SDK a tokenProvider that fetches that token from your own endpoint. Drop in <NotificationBell />, <NotificationFeed />, and <Preferences />.
  3. Your backend triggers events. When something happens, call POST /v1/notifications with a type key, the recipient's external_id, and any template variables. Elaan accepts it (202) and delivers asynchronously.
  4. Elaan delivers & streams. The inbox updates in real time over SSE (with polling fallback); email and push are sent by background workers.
The SDK never sees your API key. Only your backend holds the service key; the browser only ever receives a short-lived, contact-scoped token.

Prerequisites

  • An Elaan account — sign up at console.elaan.io. The first sign-up provisions your tenant and a default brand.
  • A service key (sk_…) from the console — used by your backend to authenticate. Keep it server-side.
  • At least one notification type and a template for the channels you want to send, created in the console.

The API base is https://api.elaan.io/v1 (self-hosted installs use your own host — see Self-hosting).

Quickstart

A complete React integration. Other frameworks follow the same shape — see SDK packages.

1 · Install
npm install @elaanio/react
2 · Expose a token endpoint on your backend
// Your server. The service key stays here — never in the browser.
app.get("/api/elaan-token", async (req, res) => {
  const r = await fetch("https://api.elaan.io/v1/contacts/tokens", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.ELAAN_SERVICE_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ external_id: req.user.id }), // your own user id
  });
  res.json(await r.json()); // { token, contact_id, expires_in }
});
3 · Wrap your app & drop in components
import { ElaanProvider, NotificationBell, Preferences } from "@elaanio/react";
import "@elaanio/react/styles.css";

async function tokenProvider() {
  const res = await fetch("/api/elaan-token");
  const { token, contact_id } = await res.json();
  return { token, contactId: contact_id };
}

export function App() {
  return (
    <ElaanProvider apiBase="https://api.elaan.io/v1" tokenProvider={tokenProvider}>
      <NotificationBell />
      <Preferences />
    </ElaanProvider>
  );
}
4 · Trigger a notification from your backend
curl https://api.elaan.io/v1/notifications \
  -H "Authorization: Bearer $ELAAN_SERVICE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "notification_type_key": "order_shipped",
       "external_id": "crm-12345",
       "variables": { "order_id": "A-1042" } }'

That's it — the bell badge updates live, the inbox fills in, and any enabled email/push channels are delivered by background workers.

SDK packages

Every package is a thin layer over a shared, framework-agnostic core, so behavior is identical across frameworks. Pick the one for your stack:

PackageForShips
@elaanio/reactReact (web)Components + hooks, SSE realtime
@elaanio/vueVue 3Components + composables, SSE realtime
@elaanio/svelteSvelteReactive stores (headless)
@elaanio/react-nativeReact NativeNative components, SSE via react-native-sse
@elaanio/elementsAny page / frameworkWeb Components (custom elements)
@elaanio/react-coreReact (custom UI)Provider + hooks, no components
@elaanio/coreAny JSClient + observable stores + realtime

All three components are available everywhere: a bell with an unread badge and popover, an inbox feed, and a preferences matrix. Auth (the tokenProvider), realtime, polling, and optimistic updates work the same in each.

React

npm install @elaanio/react
import { ElaanProvider, NotificationBell, NotificationFeed, Preferences } from "@elaanio/react";
import "@elaanio/react/styles.css";

<ElaanProvider apiBase="https://api.elaan.io/v1" tokenProvider={tokenProvider}>
  <NotificationBell />              {/* bell + badge + popover */}
  <NotificationFeed />             {/* inbox list, inline */}
  <Preferences />                  {/* per-type × channel toggles */}
</ElaanProvider>

Realtime is on by default over SSE with automatic polling fallback. Pass realtime={false} to force polling, or pollInterval={ms} to tune it.

Vue

npm install @elaanio/vue
<script setup>
import { ElaanProvider, NotificationBell, Preferences } from "@elaanio/vue";
import "@elaanio/vue/styles.css";
</script>

<template>
  <ElaanProvider apiBase="https://api.elaan.io/v1" :tokenProvider="tokenProvider">
    <NotificationBell />
    <Preferences />
  </ElaanProvider>
</template>

Composables (useNotifications, useUnreadCount, usePreferences, usePush) return Vue refs for building your own UI.

Svelte

The Svelte package is headless — it gives you native stores and you render your own markup.

npm install @elaanio/svelte
// elaan.js
import { createElaan } from "@elaanio/svelte";

export const elaan = createElaan({
  apiBase: "https://api.elaan.io/v1",
  tokenProvider,
});
<script>
  import { elaan } from "./elaan.js";
  const { state, unreadCount, markRead } = elaan.inbox;
</script>

<span>{$unreadCount}</span>
{#each $state.notifications as n (n.id)}
  <button on:click={() => markRead(n.id)}>{n.title}</button>
{/each}

React Native

npm install @elaanio/react-native
import { ElaanProvider, NotificationBell, Preferences } from "@elaanio/react-native";

<ElaanProvider apiBase="https://api.elaan.io/v1" tokenProvider={tokenProvider}>
  <NotificationBell />
  <Preferences />
</ElaanProvider>

Components render with native primitives (no stylesheet import). Realtime uses SSE via react-native-sse with polling fallback. Pair with push notifications for delivery while the app is backgrounded.

Web Components

Framework-agnostic custom elements — drop them into plain HTML, Angular, Rails/Laravel views, or anywhere.

npm install @elaanio/elements
import { defineElaanElements, configureElaan } from "@elaanio/elements";

defineElaanElements();
configureElaan({ apiBase: "https://api.elaan.io/v1", tokenProvider });
<elaan-bell></elaan-bell>
<elaan-feed></elaan-feed>
<elaan-preferences></elaan-preferences>

Headless — bring your own UI

Every packaged component is built on hooks/stores you can use directly. For a framework we don't ship yet, drop to @elaanio/core — the same observable stores every binding is built on:

import { ElaanClient, createInboxStore } from "@elaanio/core";

const client = new ElaanClient("https://api.elaan.io/v1", tokenProvider);
const inbox = createInboxStore(client, { pollInterval: 30000 });

inbox.subscribe(() => render(inbox.getState())); // getState() / getUnreadCount()
inbox.markRead(id);                              // + markAllRead / remove / refresh

Theming

The web components (React, Vue, Web Components) are styled entirely with CSS custom properties. Override them on :root — light/dark is automatic via prefers-color-scheme.

/* your global stylesheet */
:root {
  --elaan-accent: #7c3aed;
  --elaan-radius: 6px;
  --elaan-bg: #fff;
  --elaan-text: #1a1d23;
}

Full tokens: --elaan-accent, --elaan-accent-ink, --elaan-bg, --elaan-bg-hover, --elaan-text, --elaan-muted, --elaan-border, --elaan-danger, --elaan-radius, --elaan-shadow. For heavier customization, use the headless hooks and render your own markup.

Real-time

The inbox updates live over Server-Sent Events when your deployment has realtime enabled, and falls back to polling (default every 30s) otherwise — nothing to configure. A missed signal is never a lost notification: the client reconciles against the durable inbox over REST on reconnect.

Hosted (SaaS) note. The managed API currently serves the inbox over polling; SSE streaming is available on self-hosted / ALB deployments. Either way the SDK behaves identically — it just uses whichever is available.

Push tokens

Register a device's push token from the frontend (the SDK sends it under the signed-in contact):

import { usePush } from "@elaanio/react"; // or /vue, /react-native

const { register, unregister } = usePush();
// provider: "fcm" | "apns" | "expo" | "onesignal" | "webpush"
await register(deviceToken, "fcm", "web"); // platform: ios | android | web

Authentication

Every request carries Authorization: Bearer <token>. The tenant is derived from the verified token — never from a header you send. There are two credential kinds:

  • Service key (sk_…) — held by your backend. Authorizes management calls, triggering events, and minting contact tokens.
  • Contact token — short-lived, scoped to a single contact. Minted by your backend and handed to the browser; the SDK uses it for the inbox, preferences, push tokens, and the stream.
Never ship the service key to the browser. The frontend only ever holds a contact token, and only for its own contact.

Contact tokens

Your backend exchanges a contact's external_id (your own user id) for a short-lived token plus Elaan's internal contact id.

Request
POST /v1/contacts/tokens
Authorization: Bearer sk_…

{ "external_id": "crm-12345" }
Response
{
  "token": "eyJ…",
  "token_type": "bearer",
  "expires_in": 900,
  "contact_id": "01J…"
}

Return token and contact_id to the browser from your own endpoint; the SDK refreshes automatically when the token expires by calling your tokenProvider again.

Trigger a notification

The single front door. Record an event for a recipient; Elaan resolves the enabled channels, renders templates, and delivers asynchronously. Returns 202 immediately with an event_id.

Request
POST /v1/notifications
Authorization: Bearer sk_…

{
  "notification_type_key": "order_shipped",
  "external_id": "crm-12345",
  "branding_key": "acme",        // optional; falls back to the tenant default
  "variables": { "order_id": "A-1042" }
}
Response · 202 Accepted
{ "event_id": "01J…" }
  • notification_type_key — the type you created in the console.
  • external_id — the recipient (your own user id).
  • branding_key — optional; an unknown or omitted brand falls back to the tenant default.
  • variables — a flat string map merged into the template alongside brand values and contact attributes.

Sync contacts

Create or update the contacts you'll notify. Address them by your own external_id — you never need to store Elaan's internal ids.

Create
POST /v1/contacts
Authorization: Bearer sk_…

{
  "external_id": "crm-12345",
  "emails": ["ada@example.com"],
  "attributes": { "first_name": "Ada" }
}

Every management route also accepts ext:<external_id> in place of the internal id — e.g. GET /v1/contacts/ext:crm-12345 — so your backend operates entirely in your own identifiers.

Typically your backend syncs the basic contact (identity, email, attributes), while the frontend adds device-facing bits like push tokens and preference overrides through the SDK.

Preferences

The <Preferences /> component renders and edits the full matrix automatically. To build your own, read the matrix (every notification type × channel, with the effective on/off and whether the contact overrode it):

GET /v1/contacts/{contact_id}/preferences   // contact or service token
PUT /v1/contacts/{contact_id}/preferences   // { notification_type_key, channel, enabled }

In the SDK this is the usePreferences() hook / elaan.preferences store: setPreference(typeKey, channel, enabled) and clearPreference(typeKey, channel).

Delivery status

Because delivery is asynchronous, you can query an event's outcome with the event_id from the trigger response:

GET /v1/notifications/{event_id}
Authorization: Bearer sk_…

// → pending | processed | failed, with any per-channel error summary

Per-channel failures are isolated: a misconfigured email template never blocks the in-app or push delivery of the same event.

Self-hosting

Elaan is self-hostable — the same image serves the hosted product and on-prem, with config flags selecting features. Self-hosted installs get real-time SSE streaming and run entirely in your own infrastructure (single tenant, your own Postgres, your own transport credentials).

Point the SDK's apiBase at your own host; everything else is identical. See the deployment guide in the repository.

SDK source on GitHub →

Reference

  • API base: https://api.elaan.io/v1
  • npm: @elaanio/react, @elaanio/vue, @elaanio/svelte, @elaanio/react-native, @elaanio/elements, @elaanio/react-core, @elaanio/core
  • SDK source & issues: github.com/ThingsIDoForLove/elaan-js
  • Console: console.elaan.io — create notification types, templates, brands, and service keys.