Skip to content

Commit 6675695

Browse files
author
Rajat
committed
WIP: Just extracted most of the parts from CourseLit
0 parents  commit 6675695

242 files changed

Lines changed: 28372 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Dependencies
2+
node_modules/
3+
4+
# Build
5+
dist/

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## Development Tips
2+
- Don't duplicate stuff over and over. Re-use existing code and libraries.
3+
- For UI components, use shadcn/ui exclusively.

ARCHITECTURE.md

Lines changed: 461 additions & 0 deletions
Large diffs are not rendered by default.

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## Development Tips
2+
- Don't duplicate stuff over and over. Re-use existing code and libraries.
3+
- For UI components, use shadcn/ui exclusively.

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# SendLit
2+
Open-source email marketing platform
3+
4+
## Tech stack
5+
- TypeScript
6+
- PostgreSQL
7+
- Redis
8+
- Bull MQ
9+
- Nextjs
10+
- Tailwind CSS
11+
- shadcn/ui
12+
13+
## Status
14+
15+
SendLit is being bootstrapped by extracting the email composing/sending/
16+
automation capabilities out of [CourseLit](https://github.com/codelitdev/courselit)
17+
and reusing the OAuth2 implementation from
18+
[MediaLit](https://github.com/codelitdev/medialit)'s API. See
19+
[`ARCHITECTURE.md`](./ARCHITECTURE.md) for the full migration plan. `apps/api`
20+
(including its MCP server), `packages/email-editor`, `packages/email-blocks`
21+
and `apps/web` are built and have been validated end-to-end (OAuth login,
22+
contacts, templates, broadcasts and sequences, including the automation/
23+
delivery loop, and raw JSON-RPC calls against the MCP server). Account-wide
24+
analytics, bounce handling and multi-user accounts are still on the roadmap.
25+
26+
## Packages
27+
28+
- `apps/api` — OAuth2-protected REST API: contacts, templates, broadcasts/
29+
sequences, mail sending and automation.
30+
- `apps/web` — the dashboard UI (Next.js): sign in, manage contacts, compose
31+
templates/broadcasts/sequences, start/pause automations.
32+
- `packages/email-editor` — the WYSIWYG email editor (`@sendlit/email-editor`).
33+
- `packages/email-blocks` — headless composing blocks for broadcasts/
34+
sequences/templates (`@sendlit/email-blocks`), used by `apps/web`.
35+
36+
## Running everything locally
37+
38+
1. Start Postgres and Redis (e.g. via Docker).
39+
2. `apps/api`: copy `.env.example` to `.env`, fill in the values, then
40+
`pnpm --filter @sendlit/api db:push` and `pnpm --filter @sendlit/api dev`.
41+
3. `apps/web`: copy `.env.example` to `.env.local` (`API_URL` pointing at the
42+
API above), then `pnpm --filter @sendlit/web dev`.
43+
4. Build the two shared packages at least once so `apps/web` has something to
44+
import: `pnpm --filter @sendlit/email-editor build && pnpm --filter @sendlit/email-blocks build`
45+
(re-run, or use their `dev` scripts, after changing either package).

apps/api/.env.example

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Postgres connection string
2+
DB_CONNECTION_STRING=postgres://sendlit:sendlit@localhost:5432/sendlit
3+
4+
# Redis (used by BullMQ for mail sending + sequence delivery)
5+
REDIS_HOST=localhost
6+
REDIS_PORT=6379
7+
8+
PORT=4000
9+
NODE_ENV=development
10+
11+
# OAuth2 JWT signing key — must be at least 32 bytes. Generate with:
12+
# openssl rand -base64 48
13+
OAUTH_SIGNING_KEY=
14+
15+
# Used to sign open/click tracking pixel tokens.
16+
PIXEL_SIGNING_SECRET=
17+
18+
# Encrypts per-team ESP (SMTP) credentials at rest (AES-256-GCM). Must decode
19+
# to 32 bytes (base64) or contain at least 32 bytes (utf8). Generate with:
20+
# openssl rand -base64 32
21+
ESP_CREDENTIALS_ENCRYPTION_KEY=
22+
23+
# Used to build tracking-pixel/click/unsubscribe links embedded in outgoing mail.
24+
PROTOCOL=https
25+
DOMAIN=
26+
27+
# Platform default mail transport, used when a team hasn't configured its own
28+
# ESP (see src/esp/*, src/mail/transport.ts).
29+
EMAIL_HOST=
30+
EMAIL_PORT=587
31+
EMAIL_USER=
32+
EMAIL_PASS=
33+
EMAIL_FROM=
34+
35+
# Optional: on boot, if set and no account exists for this email yet, creates
36+
# one (with its default team + API key) and logs the key once — a dev/
37+
# self-host convenience, mirroring MediaLit's admin-bootstrap script. See
38+
# src/bootstrap.ts.
39+
SUPER_ADMIN_EMAIL=
40+
41+
# Required to use POST /provisioning/teams — the server-to-server endpoint a
42+
# multi-tenant consumer (e.g. CourseLit) uses to provision one SendLit team
43+
# per one of its own tenants. Generate a long random value and configure it
44+
# identically on both sides. See src/provisioning/routes.ts.
45+
PROVISIONING_SECRET=

apps/api/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
dist
3+
.env
4+
.env.local
5+
drizzle
6+
*.log

apps/api/Dockerfile

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
FROM node:24-alpine AS base
2+
RUN corepack enable pnpm
3+
4+
FROM base AS deps
5+
RUN apk add --no-cache libc6-compat
6+
WORKDIR /app
7+
8+
COPY package.json ./
9+
COPY pnpm-lock.yaml ./
10+
COPY pnpm-workspace.yaml ./
11+
COPY apps/api ./apps/api
12+
COPY packages/api-contract ./packages/api-contract
13+
COPY packages/email-editor ./packages/email-editor
14+
15+
RUN pnpm install --frozen-lockfile
16+
17+
FROM base AS builder
18+
WORKDIR /app
19+
COPY --from=deps /app/ ./
20+
21+
RUN pnpm --filter=@sendlit/email-editor build
22+
RUN pnpm --filter=@sendlit/api-contract build
23+
RUN pnpm --filter=@sendlit/api build
24+
25+
FROM base AS runner
26+
WORKDIR /app
27+
28+
ENV NODE_ENV=production
29+
30+
RUN addgroup --system --gid 1001 nodejs
31+
RUN adduser --system --uid 1001 nodeuser
32+
33+
COPY --chown=nodeuser:nodejs --from=builder /app/package.json ./
34+
COPY --chown=nodeuser:nodejs --from=builder /app/pnpm-lock.yaml ./
35+
COPY --chown=nodeuser:nodejs --from=builder /app/pnpm-workspace.yaml ./
36+
37+
COPY --chown=nodeuser:nodejs --from=builder /app/packages/email-editor/package.json ./packages/email-editor/package.json
38+
COPY --chown=nodeuser:nodejs --from=builder /app/packages/email-editor/dist ./packages/email-editor/dist
39+
40+
COPY --chown=nodeuser:nodejs --from=builder /app/packages/api-contract/package.json ./packages/api-contract/package.json
41+
COPY --chown=nodeuser:nodejs --from=builder /app/packages/api-contract/dist ./packages/api-contract/dist
42+
43+
COPY --chown=nodeuser:nodejs --from=builder /app/apps/api/package.json ./apps/api/package.json
44+
COPY --chown=nodeuser:nodejs --from=builder /app/apps/api/dist ./apps/api/dist
45+
46+
RUN pnpm install --prod --frozen-lockfile --ignore-scripts
47+
48+
USER nodeuser
49+
50+
EXPOSE 3001
51+
52+
CMD ["node", "apps/api/dist/src/index.js"]

apps/api/README.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Introduction
2+
3+
This API provides email automation.
4+
5+
## Capabilities
6+
7+
- Composing transactional mails, broadcasts, sequences and templates
8+
- Managing (CRUD) contacts, including tag/subscription/email-based segmentation
9+
- Sending emails (BullMQ + nodemailer), through a per-team ESP if configured
10+
- Automation based on events (tag added/removed, new subscriber, scheduled broadcasts)
11+
- Email open/click tracking and unsubscribe handling
12+
- Per-team ESP (email sending provider) configuration and test sends
13+
- Teams: an OAuth account can own/belong to any number of teams; every other
14+
resource (contacts, templates, sequences, ESP config, API keys) is scoped to
15+
a team, not the account — see "Teams" below.
16+
17+
## Architecture
18+
19+
- The API is protected by OAuth2 (PKCE, dynamic client registration, email-OTP
20+
passwordless login, JWT access/refresh tokens) — ported from
21+
[MediaLit's API](../../../medialit/apps/api/src/oauth). See
22+
`../../ARCHITECTURE.md` for the full mapping.
23+
- REST endpoints for contacts, templates, sequences/broadcasts — defined once
24+
as a `ts-rest` contract (`packages/api-contract`, `@sendlit/api-contract`),
25+
not hand-annotated per route. That contract is the single source of truth
26+
for request/response validation (`@ts-rest/express`, at runtime), the
27+
OpenAPI document (`@ts-rest/open-api`, see `src/swagger-generator.ts`), and
28+
`apps/web`'s typed client (`@ts-rest/core`) — so the docs can't describe a
29+
shape the server doesn't actually accept/return. Every `routes.ts` in this
30+
app is a *thin adapter*: it extracts `params`/`query`/`body` (already
31+
validated) and delegates to the unchanged, framework-agnostic functions in
32+
that domain's `queries.ts`, which holds all the actual business logic. If
33+
Express is ever swapped for something else, only these adapter files
34+
should need to change.
35+
- Two always-running loops (`src/automation/start.ts`):
36+
- `process-rules.ts` — fires date-scheduled broadcasts
37+
- `process-ongoing-sequences.ts` — polls due sequence/broadcast deliveries and
38+
hands them to a BullMQ queue, processed by `process-ongoing-sequence.ts`
39+
(renders the email, adds an open-tracking pixel + click-tracked links,
40+
sends it, and schedules the next email)
41+
- Tag/subscriber-added triggers are event-driven (`automation/fire-event.ts`),
42+
called directly from the contacts routes instead of being polled.
43+
- An OAuth/API-key-protected MCP server (`POST /mcp`), ported from
44+
[MediaLit's MCP server](../../../medialit/apps/api/src/mcp) — exposes the same
45+
contacts/templates/sequences/ESP/team capabilities as the REST API as MCP
46+
tools (see `src/mcp/tools/*`).
47+
- Each team can configure its own ESP (`src/esp/*`): any provider that
48+
exposes an SMTP relay works (SendGrid, Mailgun, Postmark, SES, Resend, or a
49+
custom server). Credentials are encrypted at rest
50+
(`src/utils/secret-crypto.ts`, AES-256-GCM) and never returned to clients.
51+
`src/mail/transport.ts` resolves the right transporter per team, falling
52+
back to the platform's `EMAIL_HOST`/etc when a team hasn't configured one.
53+
54+
### Teams
55+
56+
- An `account` (`src/account/*`) is purely a login identity (one email = one
57+
account, OTP-based). Every other resource is scoped by `teamId`
58+
(`src/team/*`), not `accountId` — a team is the actual tenant/data-scope,
59+
and holds its own sending identity (from name/email, mailing address) and
60+
mail quota.
61+
- Every account gets a default team on creation, and can create as many more
62+
as it wants (`POST /teams`). An account can belong to several teams
63+
(`team_members`, currently always with role `owner` — member invitations
64+
are a follow-up).
65+
- **API keys are team-scoped, not account-scoped** — a team can hold several,
66+
independently named/revocable keys (`src/apikey/*`, `POST/GET/DELETE
67+
/teams/:teamId/keys`). A key always resolves to exactly one team; there's no
68+
ambiguity for API/MCP clients authenticated this way.
69+
- **OAuth-authenticated (browser) requests** resolve their team from an
70+
explicit `X-Sendlit-Team-Id` header, validated against team membership on
71+
every call (`src/auth/require-team.ts`) — this is what lets the web
72+
dashboard switch teams instantly, without re-authenticating. If the header
73+
is omitted and the account belongs to exactly one team, that team is used
74+
automatically.
75+
- `POST /provisioning/teams` is a separate, secret-guarded, server-to-server
76+
endpoint for multi-tenant consumers (e.g. CourseLit provisioning one SendLit
77+
team per one of its own tenants) to find-or-create a team at any point after
78+
both stacks have booted, keyed by a consumer-supplied `externalId` rather
79+
than email (a consumer's own tenants may share an owner email) — see
80+
`src/provisioning/routes.ts`.
81+
- `SUPER_ADMIN_EMAIL` is a *different*, boot-time-only convenience (mirrors
82+
MediaLit's admin-bootstrap script): on startup, if set and no account exists
83+
for it yet, creates one (with its default team + key) and logs the key once
84+
— useful for local dev/self-hosting, not for provisioning a multi-tenant
85+
consumer's many tenants over time.
86+
87+
See the root [`ARCHITECTURE.md`](../../ARCHITECTURE.md) for what has been
88+
ported from CourseLit/MediaLit so far and what's still on the roadmap.
89+
90+
## Environment variables
91+
92+
See `.env.example`.
93+
94+
## Running the app
95+
96+
1. Start Postgres and Redis (e.g. via Docker).
97+
2. Copy `.env.example` to `.env` and fill in the values.
98+
3. Push the schema to your database: `pnpm --filter @sendlit/api db:push`
99+
4. Start the server: `pnpm --filter @sendlit/api dev`
100+
101+
The API listens on `PORT` (default `80`) and exposes:
102+
103+
- `GET /health` — liveness check
104+
- `GET /openapi.json`, `GET /docs` — OpenAPI spec / Swagger UI
105+
- `GET /.well-known/oauth-authorization-server` — OAuth2 metadata
106+
- `GET/POST/PATCH/DELETE /teams`, `/teams/:teamId` — team management
107+
(OAuth-authenticated only)
108+
- `GET/POST/DELETE /teams/:teamId/keys` — API keys for a team
109+
- `POST /contacts`, `GET /contacts`, ... — contacts
110+
- `POST /templates`, `GET /templates`, ... — email templates
111+
- `POST /sequences`, `GET /sequences`, ... — broadcasts & sequences
112+
- `GET /track/open`, `GET /track/click`, `GET /unsubscribe/:token` — tracking
113+
- `GET/PUT/DELETE /esp-config`, `POST /esp-config/test` — per-team ESP config
114+
- `POST /provisioning/teams` — server-to-server team provisioning for
115+
multi-tenant consumers (guarded by `X-Sendlit-Provisioning-Secret`, not
116+
OAuth/API-key auth)
117+
- `POST /mcp` — MCP server (JSON-RPC over HTTP, `Mcp-Session-Id` header for
118+
session continuation); authenticate the same way as REST (`Authorization:
119+
Bearer <token>` or `x-sendlit-apikey`)

apps/api/drizzle.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { defineConfig } from "drizzle-kit";
2+
3+
export default defineConfig({
4+
schema: "./src/db/schema.ts",
5+
out: "./drizzle",
6+
dialect: "postgresql",
7+
dbCredentials: {
8+
url: process.env.DB_CONNECTION_STRING || "",
9+
},
10+
});

0 commit comments

Comments
 (0)