Skip to content

TypeScript SDK for SendLit (for CourseLit campaign sending; CLI-ready, MediaLit-style) #61

Open

Description

@clbotdev

Background

CourseLit still sends its own campaigns with the email marketing engine that SendLit was extracted from. We want CourseLit to send campaigns through SendLit instead, so there's one engine to maintain and CourseLit becomes the reference platform customer (see apps/api/docs/platform-customer.md).

To do that, CourseLit needs a server-side TypeScript SDK it can import. There isn't one today. @sendlit/api-contract (ts-rest contract plus Zod schemas) is private and only used by apps/web, and the "no published SDK" gap has been flagged in earlier reviews. We also plan a SendLit CLI, so the SDK should be the shared core that the CLI builds on, the same way MediaLit does it.

MediaLit's setup to copy (codelitdev/medialit):

  • packages/medialit is published as medialit, the server SDK. It's built with tsup to ESM, CJS and types, MIT licensed, with Node >= 18. It has a MediaLit class configured with apiKey or accessToken and endpoint, falling back to the MEDIALIT_API_KEY and MEDIALIT_ENDPOINT env vars and a default cloud endpoint. It throws in the browser and validates IDs before building URLs.
  • packages/cli is published as @medialit/cli (bin medialit) and depends on the SDK through workspace:^.
  • Releases go through changesets.

Target flow (first use case)

  1. In SendLit, create a "CourseLit" team and add an ESP to it.
  2. In that team, set up the onboarding campaign as a sequence with trigger subscriber:added (EventType.SUBSCRIBER_ADDED) and start it.
  3. In the CourseLit app (app.courselit.app, repo courselit-subscriptions), import the SDK and call sendlit.contacts.create({ email, name, tags?, customFields? }) when a user signs up. This replaces the current contact creation through CourseLit's REST API (repo courselit).
  4. SendLit enrolls the new contact in the running sequence and starts sending.

The server already supports this, so the flow needs no API changes:

  • POST /contacts is find-or-create per team. A new contact fires subscriber:added (apps/api/src/contacts/queries.ts, then automation/fire-event.ts), which enrolls it in matching active sequences.
  • If the email already exists, the existing contact is returned and no event fires, so a re-signup won't re-enroll. That's the expected behaviour, but the SDK docs should say so.
  • Tags passed on create don't fire tag:added, so subscriber:added is the trigger to use for onboarding.

MVP scope: the client (auth, team, endpoint, errors) plus contacts.create, get, update, addTag and removeTag. That's all CourseLit needs for this flow. The rest of the resources listed below are the follow-up scope for the full SDK and the CLI.

Proposed fix

Add packages/sendlit, published as the sendlit package (or @sendlit/sdk if we prefer a scope), and keep room for a later packages/cli (@sendlit/cli, bin sendlit) that depends on it.

Client and config

  • Usage looks like new SendLit({ apiKey, accessToken?, endpoint?, teamId? }), with fallbacks to SENDLIT_API_KEY, SENDLIT_ENDPOINT and SENDLIT_TEAM_ID. The default endpoint is the hosted API.
  • Auth uses the x-sendlit-apikey header, or Authorization: Bearer for an OAuth access token. x-sendlit-team-id is sent when a team is set.
  • Server-only: it throws in browser environments. Path params are validated before requests, as MediaLit does.
  • Errors are typed (SendLitError with status, code and message). The client supports retries with backoff on 429 and 5xx for idempotent calls, and an optional idempotency key on sends.

Typed from the contract

  • Generate types and requests from @sendlit/api-contract, so the SDK can't drift from the server. Either bundle the contract into the SDK build or publish it as a dependency.

Resources (first version, scoped to what CourseLit needs)

  • contacts: create, list, get, update, remove, addTag, removeTag. Add an upsert-by-email if the API supports it; otherwise file a follow-up.
  • segments: CRUD.
  • templates: list, get, create, update, duplicate, remove.
  • sequences, covering broadcasts and sequences (type: "broadcast" | "sequence"): create, list, get, update, remove, addEmail, updateEmail, removeEmail, start, pause, stats.
  • emails (transactional): send, get, list.
  • provisioning (org key): create, get, update and delete teams, plus team keys, suspend, resume and usage. This lets CourseLit provision one SendLit team per CourseLit school.
  • suppressions and deliveries, read access for showing status in CourseLit.
  • Paginated list helpers, including an async iterator.

Packaging and docs

  • tsup ESM, CJS and .d.ts, MIT, Node >= 18, files: ["dist"], public publish through changesets.
  • README with a quickstart and a CourseLit-style example: provision a team, sync contacts, create and start a broadcast.
  • Add an SDK page in apps/docs.

CLI readiness (not in this ticket)

  • Keep all API logic in the SDK so the future CLI is a thin wrapper. Add a separate ticket for @sendlit/cli.

Acceptance criteria

  • packages/sendlit exists, builds with tsup to ESM, CJS and types, and is publishable to npm through changesets.
  • The client accepts an API key or access token, an endpoint and a team ID, with env var fallbacks, and throws in the browser.
  • The listed resources work against the API and are typed from @sendlit/api-contract (no hand-written duplicate types).
  • Typed errors, plus retries on 429 and 5xx for safe methods.
  • Tests cover auth headers, ID validation, error mapping and at least one call per resource, against a mocked server or the integration test stack.
  • README and docs page with a quickstart and a platform/provisioning example.
  • End to end: a test signup in courselit-subscriptions calls contacts.create through the SDK, the contact appears in the CourseLit team, and the subscriber:added sequence sends its first email through the team's ESP.
  • Signing up again with an existing email returns the existing contact and doesn't re-enroll it (documented).
  • MVP (client + contacts) can ship on its own, before the remaining resources.
  • The package layout leaves room for a later packages/cli that depends on the SDK through workspace:^.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    severity: highMajor gap or broken flow for many users

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions