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.102Keep 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.