Developers
Optional UI components
The server SDK (Get started) is required for contact updates and webhooks. UI helpers in anemone-server-js are optional — use them when you want a ready-made account connection widget or sync status strip instead of building your own.
Package exports
anemone-server-js/embed— framework-agnostic account widget (createAnemoneWidget) plus session probe helpersanemone-server-js/ui— ReactAnemoneSyncCardfor sync feedback (requires React 18+)
React is an optional peer dependency. Import only the subpath you need; tree-shaking keeps the main server client out of browser bundles when you bundle carefully.
1. Expose tenant connection info (server)
Both the embed and a hand-rolled UI call a route on your backend that returns connect/disconnect URLs and an opaque link token. Never put your API key in the browser — mint tokens server-side with client.mintDirectConnectToken (preferred) or sign offline with encodeExternalUserId using your API key prefix. For auto-connect, mount the SDK's fixed signed match endpoint at /api/anemone/auto-connect/match; when an Anemone user adds a new address, Anemone sends that endpoint only the verified email needed for a one-user lookup. See the demo implementations: GET /api/anemone/connection and POST /api/anemone/auto-connect/match.
// GET /api/tenant/connection — authenticated session required
const { linkToken } = await client.mintDirectConnectToken({ externalUserId: session.userId });
{
"externalUserId": "user-123",
"connectExternalUserId": "anlt1....",
"returnUrl": "https://your-app.com/account",
"primaryBaseUrl": "https://app.anemone.com",
"connectUrl": "https://app.anemone.com/connect?external_user_id=anlt1....",
"disconnectUrl": "https://app.anemone.com/disconnect?external_user_id=anlt1....",
"connectSignInUrl": "https://app.anemone.com/sign-in?redirect_url=...",
"disconnectSignInUrl": "https://app.anemone.com/sign-in?redirect_url=...",
"connected": true,
"autoConnected": true,
"mockMode": false
}The auto-connect match endpoint receives a signed account.email_match.requested request and should return only { "matched": true, "externalUserId": "user-123" } or { "matched": false }.
Primary stores the mapping in tenant_link_token (opaque) and user_tenant_link (after connect). Paths are /connect?external_user_id=… and /disconnect?external_user_id=… — no internal tenant id or name in URLs or SDK types.
2. Initialize the account widget embed
Call createAnemoneWidget in the browser after your page mounts. It renders sign-in / connect / disconnect actions, probes the user's Clerk session on the primary origin via a hidden iframe, and refreshes when the tab becomes visible again after a redirect.
Live preview · mock data
Styles are injected automatically (injectStyles: true by default). To use your own CSS, set injectStyles: false and import anemone-server-js/embed/widget.css, or copy the bundled stylesheet and theme tokens from the SDK source.
AnemoneSyncCard (below) instead of the embed's optional sync block.3. React sync status card (optional)
If you already have your own account UI but want consistent sync messaging (syncing, synced, queued, failed with retry), render AnemoneSyncCard and drive it with helpers from the main package export.
Live preview · mock data
Synced with Anemone
4. Hand-roll your own UI
You do not have to use the embed or React card. Many partners keep full control of layout and styling by reimplementing the same flow the SDK encodes: load connection metadata from your API, probe primary sign-in state, redirect to connect/disconnect URLs, and invalidate the session cache when the user returns from Anemone.
Use these building blocks from anemone-server-js/embed:
probePrimaryClerkSession(primaryBaseUrl, connectExternalUserId)— hidden iframe to/auth/embed-sessionon the primary origin; returnssignedIn,email, andlinkedinvalidatePrimarySessionCache()— call before navigating to sign-in, connect, or disconnect so the next probe is fresh
Refresh Clerk status when document.visibilityState becomes visible again and the tab was hidden for at least ~2 seconds (the user likely completed a redirect on the primary site).
Implementation example (React)
The following is a condensed version of the demo's hand-rolled connection UI — same behavior as the embed, styled with your design system. Full source: CustomAnemoneContactUI (also see useAnemoneConnection, fetchAnemoneConnection, and probePrimaryClerkSession).
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import {
invalidatePrimarySessionCache,
probePrimaryClerkSession,
type TenantConnectionInfo,
} from "anemone-server-js/embed";
const VISIBILITY_REFRESH_MS = 2_000;
export function AnemoneConnectionPanel() {
const [info, setInfo] = useState<TenantConnectionInfo | null>(null);
const [linked, setLinked] = useState(false);
const [signedIn, setSignedIn] = useState(false);
const [email, setEmail] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const hiddenAt = useRef<number | null>(null);
const infoRef = useRef<TenantConnectionInfo | null>(null);
const refreshSession = useCallback(async (connection: TenantConnectionInfo, force = false) => {
if (connection.mockMode || !connection.primaryBaseUrl) {
setLinked(false);
setSignedIn(connection.mockMode);
setEmail(null);
return;
}
const status = await probePrimaryClerkSession(
connection.primaryBaseUrl,
connection.connectExternalUserId,
{ force },
);
setLinked(status.linked);
setSignedIn(status.signedIn);
setEmail(status.email);
}, []);
useEffect(() => {
void (async () => {
const res = await fetch("/api/tenant/connection");
if (!res.ok) return;
const connection = (await res.json()) as TenantConnectionInfo;
setInfo(connection);
infoRef.current = connection;
setLoading(false);
await refreshSession(connection);
})();
}, [refreshSession]);
useEffect(() => {
function onVisibilityChange() {
if (document.visibilityState === "hidden") {
hiddenAt.current = Date.now();
return;
}
const connection = infoRef.current;
if (!connection) return;
const hiddenFor = hiddenAt.current ? Date.now() - hiddenAt.current : 0;
hiddenAt.current = null;
if (hiddenFor < VISIBILITY_REFRESH_MS) return;
invalidatePrimarySessionCache();
void refreshSession(connection, true);
}
document.addEventListener("visibilitychange", onVisibilityChange);
return () => document.removeEventListener("visibilitychange", onVisibilityChange);
}, [refreshSession]);
if (loading || !info) return <p>Loading…</p>;
function goToSignIn() {
if (!info?.connectSignInUrl) return;
invalidatePrimarySessionCache();
window.location.assign(info.connectSignInUrl);
}
function goToConnect() {
if (!info?.connectUrl || !signedIn) return;
invalidatePrimarySessionCache();
window.location.assign(info.connectUrl);
}
function goToDisconnect() {
if (!info?.disconnectUrl) return;
if (!signedIn && info.disconnectSignInUrl) {
window.location.assign(info.disconnectSignInUrl);
return;
}
invalidatePrimarySessionCache();
window.location.assign(info.disconnectUrl);
}
return (
<section aria-label="Anemone account">
<h2>Anemone account</h2>
<p>
{linked
? "Your account is connected to Anemone."
: signedIn
? "Connect your account to sync contact updates."
: "Sign in to Anemone to connect your account."}
</p>
{!info.mockMode && (
<span>{signedIn ? (email ?? "Signed in") : "Not signed in"}</span>
)}
<span>{linked ? "Connected" : "Not connected"}</span>
{linked ? (
<button type="button" onClick={goToDisconnect}>
{signedIn ? "Disconnect account" : "Sign in to disconnect"}
</button>
) : signedIn ? (
<button type="button" onClick={goToConnect}>
Connect account
</button>
) : (
<button type="button" onClick={goToSignIn}>
Sign in
</button>
)}
</section>
);
}Pair this panel with your own contact form and optionally AnemoneSyncCard for save/sync feedback. See Examples & sample app and the full paws-and-tails-demo project (account/contact page).