npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dravra/js

v0.1.1

Published

Dravra JavaScript SDK — identify users, track events, and send emails

Downloads

32

Readme

@dravra/js

Official JavaScript / TypeScript SDK for Dravra — the email automation platform for SaaS teams. Identify users, track lifecycle events, and send transactional emails directly from your application.

npm version license


What is Dravra?

Dravra is an email automation platform built for SaaS applications. It lets you:

  • Identify users — sync your users into Dravra as contacts with traits (plan, company, lifecycle stage)
  • Track events — record lifecycle moments (trial.started, subscription.upgraded, etc.) that trigger automated email sequences
  • Send emails — deliver transactional emails using templates you build inside Dravra's visual editor
  • Automate flows — build drip campaigns, onboarding sequences, and behavioral triggers in the Dravra dashboard

The @dravra/js SDK gives you a typed, retrying HTTP client that talks to the Dravra REST API. It is designed to work in any Node.js, Edge Runtime, or Deno environment — including Next.js Server Actions, Route Handlers, Supabase Edge Functions, and plain Express servers.


Table of contents


Installation

npm install @dravra/js
# or
yarn add @dravra/js
# or
pnpm add @dravra/js

Requires Node.js 18+ or any runtime with a native fetch implementation (Edge Runtime, Deno, Bun).


Quick start

import { Dravra } from "@dravra/js";

const dravra = new Dravra({ apiKey: "drv_live_..." });

// 1. Identify a user (creates or updates a contact)
await dravra.identify("user_123", "[email protected]", {
  firstName: "Alice",
  lastName: "Smith",
  plan: "pro",
});

// 2. Track a lifecycle event
await dravra.track("trial.started", {
  userId: "user_123",
  email: "[email protected]",
  properties: { trialDays: 14 },
});

// 3. Send a transactional email
await dravra.send(
  { email: "[email protected]" },
  "tmpl_welcome_email",
  { variables: { firstName: "Alice" } }
);

Configuration

Create a Dravra instance with your API key from the Dravra dashboard (Settings → API Keys):

const dravra = new Dravra({
  apiKey:  "drv_live_...", // required — must start with drv_live_
  baseUrl: "https://api.dravra.ai", // optional — override for self-hosted instances
  timeout: 10000, // optional — per-request timeout in ms (default: 10000)
});

| Option | Type | Default | Description | |---|---|---|---| | apiKey | string | required | Dravra API key. Must start with drv_live_. | | baseUrl | string | https://api.dravra.ai | Override the API base URL (useful for testing or self-hosted). | | timeout | number | 10000 | Per-request timeout in milliseconds. |

Store your API key as an environment variable — never hard-code it or commit it:

# .env.local
DRAVRA_API_KEY=drv_live_...

API reference

identify(userId, email, traits?)

Creates or updates a contact in Dravra. If a contact with the same email or externalUserId already exists, it is updated (upsert). Returns the full Contact object.

Signature

identify(
  userId: string,                    // Your system's user ID — stored as externalUserId
  email:  string,                    // User's email address
  traits?: Record<string, unknown>   // Any additional contact properties
): Promise<Contact>

Example

const contact = await dravra.identify("user_abc123", "[email protected]", {
  firstName:      "Alice",
  lastName:       "Smith",
  plan:           "pro",
  company:        "Acme Inc",
  trialStartedAt: "2026-04-01T00:00:00Z",
});

console.log(contact.id);             // Dravra contact ID  e.g. "cnt_..."
console.log(contact.lifecycleStage); // "lead" | "subscriber" | "active" | etc.
console.log(contact.createdAt);      // ISO 8601 timestamp

Return type: Contact

interface Contact {
  id:               string;
  tenantId:         string;
  email:            string;
  externalUserId?:  string | null;
  firstName?:       string | null;
  lastName?:        string | null;
  traits?:          Record<string, unknown>;
  lifecycleStage?:  string;
  createdAt:        string; // ISO 8601
  updatedAt:        string; // ISO 8601
}

track(event, options?)

Tracks a named event and associates it with a contact. At least one of options.userId or options.email must be provided so the API can link the event to a contact.

Events drive Dravra automations — when you track trial.started, any automation that listens for that event will fire.

Signature

track(
  event:    StandardEventName,  // Event name in dot notation, e.g. "trial.started"
  options?: TrackOptions
): Promise<TrackResult>

interface TrackOptions {
  userId?:        string;                    // Your system's user ID
  email?:         string;                    // User's email address
  properties?:    Record<string, unknown>;   // Arbitrary event properties
  occurredAt?:    string;                    // ISO 8601 — defaults to now
  idempotencyKey?: string;                   // Deduplicate retries (auto-generated if omitted)
}

Example

const result = await dravra.track("trial.started", {
  userId:     "user_abc123",
  email:      "[email protected]",
  properties: { trialDays: 14, plan: "pro" },
  occurredAt: "2026-04-01T09:00:00Z",
});

console.log(result.eventId); // Internal event ID
console.log(result.status);  // "accepted" | "duplicate"

Idempotency

Every track() call automatically generates a UUID as an idempotency key and sends it in the Idempotency-Key request header. If the API receives two requests with the same key, only the first is recorded and the second returns { status: "duplicate" } — safe to retry without double-counting.

Supply your own key to guarantee idempotency across process restarts:

await dravra.track("subscription.upgraded", {
  userId:         "user_abc123",
  idempotencyKey: `upgrade-${userId}-${invoiceId}`,
});

Return type: TrackResult

interface TrackResult {
  eventId: string;
  status:  "accepted" | "duplicate";
}

send(to, templateId, options?)

Sends a transactional email to a contact using a template published in the Dravra dashboard. The send is queued and processed asynchronously — the method returns as soon as the request is accepted.

Signature

send(
  to:         { contactId: string } | { email: string },
  templateId: string,
  options?:   SendOptions
): Promise<SendResult>

interface SendOptions {
  variables?:    Record<string, unknown>;         // Template variable substitutions
  messageClass?: "transactional" | "marketing";  // Default: "transactional"
}

Examples

// Send by email address — the contact is looked up or created automatically
const result = await dravra.send(
  { email: "[email protected]" },
  "tmpl_welcome_email",
  {
    variables:    { firstName: "Alice", trialDays: 14 },
    messageClass: "transactional",
  }
);

// Send by Dravra contact ID (more explicit)
await dravra.send(
  { contactId: "cnt_abc123" },
  "tmpl_trial_ending",
  { variables: { daysLeft: 3 } }
);

console.log(result.sendId); // ID you can use to check delivery status
console.log(result.status); // Always "queued" on success

messageClass

| Value | When to use | |---|---| | "transactional" | Password resets, receipts, account notifications. Sent regardless of marketing opt-out. | | "marketing" | Newsletters, promotions, drip campaigns. Respects unsubscribe preferences. |

Return type: SendResult

interface SendResult {
  sendId: string;
  status: "queued";
}

Next.js helpers (@dravra/js/next)

The @dravra/js/next subpath export provides two helpers optimized for Next.js applications. They are imported separately so tree-shaking excludes them from non-Next.js builds.

import { withDravra, clerkAdapter } from "@dravra/js/next";

withDravra(options)

A factory function that creates a Dravra instance. Functionally identical to new Dravra(options), but encourages the singleton pattern and reads more naturally in a lib/ file.

// lib/dravra.ts
import { withDravra } from "@dravra/js/next";

export const dravra = withDravra({
  apiKey: process.env.DRAVRA_API_KEY!,
});

Import this singleton in any Server Action or Route Handler — no need to re-instantiate the client on every request.


clerkAdapter(dravra, event)

Handles a Clerk webhook event and maps it to the appropriate Dravra calls. Supports user.created, user.updated, and user.deleted. For unsupported event types, it returns { handled: false } without throwing.

What it does per event type:

| Clerk event | Dravra calls | |---|---| | user.created | identify() + track("user.signed_up") | | user.updated | identify() + track("user.updated") | | user.deleted | track("user.deleted") | | anything else | no-op, returns { handled: false } |

Signature

clerkAdapter(
  dravra: Dravra,
  event:  ClerkWebhookEvent
): Promise<ClerkAdapterResult>

interface ClerkWebhookEvent {
  type: string;
  data: ClerkUserPayload;
}

interface ClerkAdapterResult {
  handled:   boolean;
  eventType: string;
  contactId?: string; // set for user.created and user.updated
  eventId?:  string;  // Dravra event ID
}

Example

// app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { withDravra, clerkAdapter } from "@dravra/js/next";

const dravra = withDravra({ apiKey: process.env.DRAVRA_API_KEY! });

export async function POST(req: Request) {
  const payload = await req.text();
  const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);

  // Verify Svix signature — throws if invalid
  const event = wh.verify(payload, Object.fromEntries(req.headers)) as any;

  const result = await clerkAdapter(dravra, event);
  return Response.json({ received: true, ...result });
}

Note: clerkAdapter does not verify the Svix signature — always call wh.verify() before passing the event to the adapter.


Integration guides

Next.js

1. Add your API key to .env.local:

DRAVRA_API_KEY=drv_live_...

2. Create a singleton client:

// lib/dravra.ts
import { withDravra } from "@dravra/js/next";

export const dravra = withDravra({
  apiKey: process.env.DRAVRA_API_KEY!,
});

3. Use in a Route Handler:

// app/api/waitlist/route.ts
import { dravra } from "@/lib/dravra";

export async function POST(req: Request) {
  const { email, firstName } = await req.json();

  await dravra.identify("", email, { firstName, source: "waitlist" });
  await dravra.track("user.signed_up", { email });

  return Response.json({ ok: true });
}

4. Use in a Server Action:

// app/actions/onboarding.ts
"use server";
import { auth } from "@clerk/nextjs/server";
import { dravra } from "@/lib/dravra";

export async function completeOnboarding(email: string) {
  const { userId } = await auth();
  if (!userId) throw new Error("Unauthorized");

  await dravra.identify(userId, email);
  await dravra.track("trial.started", { userId, email });
}

Clerk webhooks

Automatically sync new Clerk users into Dravra when they sign up.

Prerequisites: Install svix (npm install svix) and configure a webhook endpoint in the Clerk dashboard pointing to /api/webhooks/clerk.

Using clerkAdapter (recommended):

// app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { withDravra, clerkAdapter } from "@dravra/js/next";

const dravra = withDravra({ apiKey: process.env.DRAVRA_API_KEY! });

export async function POST(req: Request) {
  const payload = await req.text();
  const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
  const event = wh.verify(payload, Object.fromEntries(req.headers)) as any;

  const result = await clerkAdapter(dravra, event);
  return Response.json({ received: true, ...result });
}

Manual approach (full control):

// app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { dravra } from "@/lib/dravra";

export async function POST(req: Request) {
  const payload = await req.text();
  const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
  const event = wh.verify(payload, Object.fromEntries(req.headers)) as {
    type: string;
    data: {
      id: string;
      email_addresses: { email_address: string }[];
      first_name?: string;
      last_name?: string;
    };
  };

  const email = event.data.email_addresses[0]?.email_address ?? "";

  if (event.type === "user.created") {
    await dravra.identify(event.data.id, email, {
      firstName: event.data.first_name,
      lastName:  event.data.last_name,
    });
    await dravra.track("user.signed_up", { userId: event.data.id, email });
  }

  if (event.type === "user.deleted") {
    await dravra.track("user.deleted", { userId: event.data.id });
  }

  return Response.json({ received: true });
}

Supabase Edge Functions

Identify a new user automatically when they are inserted into your profiles table via a Supabase database webhook.

// supabase/functions/on-user-created/index.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { Dravra } from "https://esm.sh/@dravra/js";

const dravra = new Dravra({ apiKey: Deno.env.get("DRAVRA_API_KEY")! });

serve(async (req) => {
  const { record } = await req.json() as {
    record: {
      id:                   string;
      email:                string;
      raw_user_meta_data?:  Record<string, string>;
    };
  };

  const fullName = record.raw_user_meta_data?.full_name ?? "";
  const [firstName, ...rest] = fullName.split(" ");

  await dravra.identify(record.id, record.email, {
    firstName,
    lastName: rest.join(" ") || undefined,
  });

  await dravra.track("user.signed_up", {
    userId: record.id,
    email:  record.email,
  });

  return new Response(JSON.stringify({ ok: true }), {
    headers: { "Content-Type": "application/json" },
  });
});

Set the secret in your Supabase project:

supabase secrets set DRAVRA_API_KEY=drv_live_...

Standard event names

StandardEventName is an open-ended union — it provides autocomplete for built-in names while still accepting any string. You will never get a type error for a custom event name.

import type { StandardEventName } from "@dravra/js";

| Event name | When to fire | |---|---| | user.signed_up | User creates an account | | user.verified_email | User verifies their email address | | user.first_login | User logs in for the first time | | user.updated | User profile is updated | | user.deleted | User account is deleted | | trial.started | Free trial begins | | trial.ending_soon | Trial is 3–7 days from expiry | | subscription.upgraded | User upgrades their plan | | subscription.cancelled | User cancels their subscription |

Custom events — use any dot-notation name that makes sense for your domain:

await dravra.track("order.completed",        { userId, properties: { orderId, total } });
await dravra.track("feature.used",           { userId, properties: { feature: "exports" } });
await dravra.track("invoice.payment_failed", { userId, email });

TypeScript types

All types are exported from the main entry point:

import type {
  DravraOptions,
  Contact,
  TrackOptions,
  TrackResult,
  SendOptions,
  SendResult,
  StandardEventName,
} from "@dravra/js";

Types from @dravra/js/next:

import type {
  ClerkUserPayload,
  ClerkWebhookEvent,
  ClerkAdapterResult,
} from "@dravra/js/next";

Full type reference:

interface DravraOptions {
  apiKey:   string;
  baseUrl?: string;
  timeout?: number;
}

interface Contact {
  id:              string;
  tenantId:        string;
  email:           string;
  externalUserId?: string | null;
  firstName?:      string | null;
  lastName?:       string | null;
  traits?:         Record<string, unknown>;
  lifecycleStage?: string;
  createdAt:       string; // ISO 8601
  updatedAt:       string; // ISO 8601
}

interface TrackOptions {
  userId?:         string;
  email?:          string;
  properties?:     Record<string, unknown>;
  occurredAt?:     string; // ISO 8601, defaults to now
  idempotencyKey?: string; // auto-generated UUID if omitted
}

interface TrackResult {
  eventId: string;
  status:  "accepted" | "duplicate";
}

interface SendOptions {
  variables?:    Record<string, unknown>;
  messageClass?: "transactional" | "marketing"; // default: "transactional"
}

interface SendResult {
  sendId: string;
  status: "queued";
}

// Open union — built-in names are autocompleted, custom strings are accepted
type StandardEventName =
  | "user.signed_up"
  | "user.verified_email"
  | "trial.started"
  | "trial.ending_soon"
  | "subscription.upgraded"
  | "subscription.cancelled"
  | "user.first_login"
  | "user.updated"
  | "user.deleted"
  | (string & {});

Error handling & retries

Throwing behavior

All three methods (identify, track, send) throw an Error with a descriptive message if the request fails. Wrap calls in try/catch in production:

try {
  await dravra.track("trial.started", { userId, email });
} catch (err) {
  if (err instanceof Error) {
    console.error("Dravra error:", err.message);
    // Examples:
    // "Dravra: apiKey is required"
    // "Dravra API error: At least one of userId or email must be provided"
    // "Dravra API error: Template not found"
    // "Dravra API error: HTTP 429"
  }
}

Automatic retries

The SDK wraps every HTTP request with fetchWithRetry, which automatically retries on transient failures:

| Condition | Retried? | |---|---| | Network error / DNS failure | Yes | | 5xx server error | Yes | | 4xx client error | No — bad requests are not retried | | 2xx success | No — request succeeded |

Retry schedule (2 attempts after the initial request):

| Attempt | Delay | |---|---| | 1st retry | ~100 ms + random jitter | | 2nd retry | ~200 ms + random jitter |

Jitter prevents thundering-herd problems when multiple clients retry simultaneously.

Idempotency and duplicate detection

track() sends an Idempotency-Key header on every request. If a retry delivers a duplicate event, the API returns { status: "duplicate" } instead of double-counting it. You can pass your own key for deterministic deduplication across retries:

await dravra.track("subscription.upgraded", {
  userId:         "user_abc123",
  idempotencyKey: `upgrade-${userId}-${invoiceId}`,
});

License

MIT — see LICENSE for details.