Effect SDK

Send email from Effect with Samva's Effect-native SDK entrypoint, typed errors, and retry support.

Use the samva/effect modules when your app already uses Effect. Each domain has a Pascal-cased namespace import, operations return Effects directly, and a single client Layer supplies authentication and the HTTP transport.

Samva sends from the verified sender configured on your account, so the Effect SDK examples do not include a from field.

Install

bun add samva effect@4.0.0-beta.102

Keep SAMVA_API_KEY server-side only. The Effect SDK currently uses effect/unstable/http, so pin a compatible Effect 4 beta while the namespace is still pre-stable.

First send

Import the client capability and the domains you use. Provide Client.layerFetch at the edge of the program:

import { Effect } from "effect";
import * as Client from "samva/effect/client";
import * as Email from "samva/effect/email";

const program = Email.send({
  to: "ada@example.com",
  subject: "Welcome",
  html: "<p>Hi Ada</p>",
  text: "Hi Ada",
}).pipe(
  Effect.provide(
    Client.layerFetch({ apiKey: process.env.SAMVA_API_KEY! }),
  ),
);

const message = await Effect.runPromise(program);
console.log(message.id, message.status);

Compose domain modules

Domain operations require Client.Service in their Effect environment. Build the Layer once, then provide it to the part of your program that calls Samva:

import { Effect } from "effect";
import * as Client from "samva/effect/client";
import * as Email from "samva/effect/email";

const SamvaLayer = Client.layerFetch({
  apiKey: process.env.SAMVA_API_KEY!,
});

const sendWelcome = (to: string) =>
  Email.send({
    to,
    subject: "Welcome",
    html: "<p>Your workspace is ready.</p>",
    text: "Your workspace is ready.",
  });

await Effect.runPromise(sendWelcome("ada@example.com").pipe(Effect.provide(SamvaLayer)));

Client.layerFetch(config) uses globalThis.fetch. Use Client.layer(config) if you want to provide a custom HttpClient.HttpClient implementation.

Default-on retry

Safe operations retry automatically, so you do not wrap calls in Effect.retry. Reads and sends retry on throttling (429), transient server errors, and request-transport failures, using jittered exponential backoff bounded to four attempts. A 429 waits for the server's retryAfterSeconds hint (capped at 60s). Sends carry an Idempotency-Key (see below) so a replayed attempt is deduplicated; every other mutating call is keyless and never auto-retries.

Turn retry off, or install your own policy, with the Retry Layer:

import { Effect } from "effect";
import { isRetryable } from "samva/effect/categories";
import * as Retry from "samva/effect/retry";

// No auto-retry for this program:
sendWelcome("ada@example.com").pipe(Effect.provide(Retry.layerDisabled));

// Or a custom policy, using any Effect.retry options:
sendWelcome("ada@example.com").pipe(
  Effect.provide(Retry.layer({ times: 6, while: isRetryable })),
);

Idempotency keys

Each email.send / messages.send generates an Idempotency-Key per call, stable across the built-in retries so a retried send never delivers twice. Pass your own key to deduplicate a send you might replay from another process; reusing a key with identical content replays the original response, and reusing it with different content fails with ConflictError.

await Effect.runPromise(
  sendWelcome("ada@example.com").pipe(Effect.provide(SamvaLayer)),
);

// Explicit key for cross-process dedup:
Email.send(input, { idempotencyKey: "order-4417-receipt" });

Stream pagination

Paginated operations expose pages and items streams. Query parameters retain their public OpenAPI wire types, so page and limit are strings.

import { Stream } from "effect";
import * as Contacts from "samva/effect/contacts";

const activeContacts = Contacts.list.items({
  page: "1",
  limit: "100",
  status: "active",
});

const firstTwoHundred = activeContacts.pipe(
  Stream.take(200),
  Stream.runCollect,
);

Sensitive values

Fields marked sensitive in Samva's OpenAPI document decode to Redacted. Unwrap them only where plaintext is required.

import { Effect, Redacted } from "effect";
import * as ApiKeys from "samva/effect/api-keys";

const createKey = Effect.gen(function* () {
  const created = yield* ApiKeys.create({ name: "Production" });
  console.log(String(created.key)); // <redacted>
  return Redacted.value(created.key);
});

Typed errors

Every operation fails with Samva's semantic tagged errors, such as RateLimitedError, ValidationError, PaymentRequiredError, and UnauthorizedError, directly in the Effect error channel. Match them by tag with Effect.catchTag / Effect.catchTags, reading each error's own fields (no wrapper, no cause). What reaches your handler has already survived the built-in retries, so a RateLimitedError or InternalError here means retrying did not recover.

For cross-cutting handling, the SDK ships category helpers: isRetryable (true for throttled, transient, and transport failures), isTransient, and catchAuthError / catchTransient and the other per-category combinators, which handle a whole category in one call and narrow it out of the error channel.

import { Effect } from "effect";

const handledSend = sendWelcome("ada@example.com").pipe(
  Effect.catchTags({
    ValidationError: (error) =>
      Effect.succeed({ status: 400, fields: error.fields ?? {} }),
    UnauthorizedError: () =>
      Effect.succeed({
        status: 500,
        message: "SAMVA_API_KEY is invalid or missing permissions.",
      }),
    RateLimitedError: (error) =>
      Effect.succeed({ status: 429, retryAfterSeconds: error.retryAfterSeconds }),
    InternalError: () =>
      Effect.succeed({
        status: 502,
        message: "Samva returned a transient server error after retries.",
      }),
    ExternalServiceError: () =>
      Effect.succeed({
        status: 502,
        message: "Samva returned a transient gateway error after retries.",
      }),
  }),
);

React Email and edge runtimes

Samva takes rendered html and optional text. If you use React Email, render the component first and pass the strings to email.send; the React Email integration covers templates and previewing in more depth.

Because the Effect transport uses fetch, the same send path runs in Bun, Node with fetch, Vercel Edge, and Cloudflare Workers. Pass the API key from your server or edge environment binding, not from browser code.

Cookbook and example

The Effect SDK cookbook goes deeper: the layer-provided client, typed error tags, retrying with Schedule, and React Email on edge runtimes.

If your app does not use Effect, use the standard TypeScript SDK instead.

On this page