Skip to content

Commit 0ba3ec0

Browse files
authored
Fix: Subscriber Growth (#50)
1 parent 45ad2c7 commit 0ba3ec0

11 files changed

Lines changed: 458 additions & 65 deletions

File tree

CLAUDE.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
LetterSpace is an open source, self-hosted newsletter platform. It's a pnpm monorepo with Turbo for task orchestration.
8+
9+
## Commands
10+
11+
### Development
12+
13+
```bash
14+
pnpm dev # Run all dev servers (Turbo)
15+
pnpm build # Build all projects
16+
pnpm lint # Lint all projects
17+
pnpm format # Format with Prettier
18+
```
19+
20+
### Backend-specific
21+
22+
```bash
23+
pnpm --filter backend dev # Run backend only
24+
pnpm --filter backend test # Run tests (Vitest)
25+
pnpm --filter backend generate # Prisma codegen
26+
pnpm --filter backend migrate:dev # Dev migrations
27+
```
28+
29+
### Database
30+
31+
```bash
32+
pnpm --filter backend prisma db seed # Seed database
33+
cd apps/backend && pnpm prisma migrate reset --force # Reset and reseed
34+
```
35+
36+
### Release Process
37+
38+
```bash
39+
./scripts/release.sh # Bumps patch version (default)
40+
./scripts/release.sh minor # Bumps minor version
41+
./scripts/release.sh major # Bumps major version
42+
```
43+
44+
The script bumps version in `package.json`, creates a git tag, and pushes it. GitHub Actions builds Docker images and creates a release from `RELEASE_NOTES.md`.
45+
46+
## Architecture
47+
48+
### Monorepo Structure
49+
50+
- `apps/backend` - Express + TRPC backend (Bun/Node.js)
51+
- `apps/web` - Vite + React dashboard (PWA)
52+
- `apps/landing-page` - Next.js marketing site
53+
- `apps/docs` - Nextra documentation
54+
- `packages/ui` - Shared Shadcn UI components
55+
- `packages/shared` - Shared TypeScript types
56+
- `packages/eslint-config` - Shared ESLint config
57+
58+
### Backend Architecture
59+
60+
TRPC routers in `apps/backend/src/router/`:
61+
- Each domain (user, campaign, subscriber, etc.) has its own directory
62+
- Pattern: `router.ts` (definition), `mutation.ts` (writes), `query.ts` (reads)
63+
64+
Key entry points:
65+
- `apps/backend/src/app.ts` - Express app setup, middleware, routes
66+
- `apps/backend/src/trpc.ts` - TRPC context and auth
67+
- `apps/backend/src/cron/` - Scheduled jobs (email sending, maintenance)
68+
69+
Endpoints:
70+
- `/trpc/*` - TRPC RPC endpoints
71+
- `/api/*` - REST API (Swagger documented)
72+
- `/t/:id` - Link tracking redirect
73+
- `/img/:id/img.png` - Email open tracking pixel
74+
75+
### Frontend Architecture
76+
77+
React Router app in `apps/web/src/`:
78+
- `app.tsx` - Route definitions
79+
- `pages/` - Page components matching routes
80+
- TRPC client with React Query for data fetching
81+
- Token auth via cookies
82+
83+
### Database
84+
85+
Prisma ORM with PostgreSQL. Schema at `apps/backend/prisma/schema.prisma`.
86+
87+
Key models: User, Organization (multi-tenancy), Subscriber, List, Campaign, Template, Webhook.
88+
89+
## Tech Stack
90+
91+
- **Backend**: Express, TRPC, Prisma, PostgreSQL, Bun/Node.js 22+
92+
- **Frontend**: React 19, Vite, React Router, Shadcn UI, TailwindCSS
93+
- **Shared**: TypeScript (strict), Zod, React Hook Form
94+
95+
## Code Style
96+
97+
- Adhere to existing code patterns in the codebase
98+
- Minimal comments
99+
- Component files: kebab-case (e.g., `user-profile.tsx`)
100+
- Component exports: PascalCase (e.g., `export const UserProfile`)
101+
- Colocate types/data with components when only used by that component
102+
103+
## Development Notes
104+
105+
- Email sending is disabled in development (`NODE_ENV=development`) - cron jobs skip and mailer returns mock responses
106+
- Backend tests use `.env.test` for configuration
107+
- Webhook transformers run in QuickJS sandbox with configurable memory limits

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
FROM oven/bun:1
22

33
RUN apt-get update -y && apt-get install -y openssl
4-
RUN bun i -g pnpm@10.10.0
4+
RUN bun i -g pnpm@10.26.2
55

66
WORKDIR /app
77

Dockerfile.node

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
FROM node:22-slim
22

33
RUN apt-get update -y && apt-get install -y openssl
4-
RUN npm install -g pnpm@10.10.0
4+
RUN npm install -g pnpm@10.26.2
55

66
WORKDIR /app
77

RELEASE_NOTES.md

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
### ✨ Features
22

3-
- **Webhooks Support** - Receive real-time notifications for email events (delivered, opened, clicked, bounced, complained)
4-
- Custom webhook endpoints with authentication and transformation code support
5-
- Process email status updates from external email service providers
6-
- **Webhook Request Logging** - View detailed logs of all webhook requests with status codes, payloads, responses, and timing information
7-
- **Complained Message Status** - Track when recipients mark emails as spam
8-
- **Monaco Code Editor** - Enhanced code editing experience for webhook configuration
9-
- **Awaiting Webhook Status** - Track when messages are awaiting webhook processing
3+
- **Migration Scripts** - Added `migrate:dev` and `migrate:deploy` scripts for easier database migration management
4+
- **Release Script** - Added automated release script (`scripts/release.sh`) for version bumping and tag creation
105

116
### 🐛 Bug Fixes
127

13-
- Fixed unsubscribe functionality
14-
15-
### 📚 Docs
16-
17-
- Added comprehensive webhook documentation and event reference
18-
- See the latest documentation at [docs.letterspace.app](https://docs.letterspace.app).
8+
- Fixed dashboard subscriber growth calculation to properly handle baseline subscriber count
9+
- Improved seed data distribution - subscribers now distributed over 2 years instead of 12 days for better testing

apps/backend/CLAUDE.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Commands
6+
7+
```bash
8+
pnpm dev # Dev server with watchexec
9+
pnpm start # Production server (Bun)
10+
pnpm build # Compile TypeScript
11+
pnpm test # Run tests (Vitest with .env.test)
12+
pnpm lint # ESLint
13+
pnpm lint:fix # ESLint with auto-fix
14+
pnpm generate # Prisma codegen
15+
pnpm migrate:dev # Create/apply dev migrations
16+
pnpm migrate:deploy # Apply production migrations
17+
```
18+
19+
### Database
20+
21+
```bash
22+
pnpm prisma db seed # Seed database
23+
pnpm prisma migrate reset --force # Reset and reseed
24+
```
25+
26+
## Architecture
27+
28+
Express + TRPC backend with Prisma ORM. Runs on Bun (or Node.js 22+).
29+
30+
### Key Files
31+
32+
- `src/app.ts` - Express app setup, routes, TRPC middleware
33+
- `src/trpc.ts` - TRPC context, auth middleware, procedure definitions
34+
- `src/index.ts` - Server entry point, starts cron jobs
35+
- `src/shared.ts` - Exports for frontend type sharing
36+
- `prisma/schema.prisma` - Database schema
37+
38+
### TRPC Router Pattern
39+
40+
Each domain has its own directory with consistent structure:
41+
42+
```
43+
src/user/
44+
router.ts - Router definition combining queries and mutations
45+
query.ts - Read procedures (authProcedure or publicProcedure)
46+
mutation.ts - Write procedures
47+
```
48+
49+
Available routers: user, organization, list, subscriber, campaign, template, message, settings, webhook, dashboard, stats
50+
51+
### Procedures
52+
53+
- `publicProcedure` - No auth required
54+
- `authProcedure` - Requires valid JWT, provides `ctx.user`
55+
56+
### HTTP Endpoints
57+
58+
- `/trpc/*` - TRPC RPC endpoints
59+
- `/api/*` - REST API (Swagger docs at `/docs`)
60+
- `/t/:id` - Link tracking redirect (updates click stats)
61+
- `/img/:id/img.png` - Email open tracking pixel
62+
- `/webhook/:webhookId` - Webhook handler for external integrations
63+
- `/*` - Serves frontend SPA static files
64+
65+
### Cron Jobs
66+
67+
Defined in `src/cron/`:
68+
69+
- `sendMessages.ts` - Process queued emails
70+
- `processQueuedCampaigns.ts` - Handle scheduled campaigns
71+
- `dailyMaintenance.ts` - Database cleanup
72+
- `cleanupWebhookLogs.ts` - Remove old webhook logs
73+
74+
Cron jobs are disabled when `NODE_ENV=development`.
75+
76+
### Authentication
77+
78+
JWT-based auth with password versioning:
79+
80+
- Token in `Authorization: Bearer <token>` header
81+
- `verifyToken()` in `src/utils/auth.ts`
82+
- Password version (`pwdVersion`) forces re-auth on password change
83+
84+
### Webhook Transformer
85+
86+
Custom JavaScript transformers run in QuickJS sandbox (`src/webhook/transformer.ts`). Memory limits configurable via env vars.
87+
88+
### Email Sending
89+
90+
Nodemailer integration in `src/lib/Mailer.ts`. Returns mock success in development.
91+
92+
### Testing
93+
94+
Vitest with Supertest for API tests. Uses `.env.test` for test database.
95+
96+
```bash
97+
pnpm test # Run all tests
98+
pnpm test -- --watch # Watch mode
99+
pnpm test -- path/to/file # Single file
100+
```

apps/backend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"lint:fix": "eslint . --fix",
1212
"generate": "prisma generate",
1313
"generate:sql": "prisma generate --sql",
14-
"test": "dotenv -e .env.test -- vitest"
14+
"test": "dotenv -e .env.test -- vitest",
15+
"migrate:dev": "prisma migrate dev",
16+
"migrate:deploy": "prisma migrate deploy"
1517
},
1618
"exports": {
1719
".": "./src/index.ts",

apps/backend/prisma/seed.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,38 @@ async function seed() {
5757
})
5858
}
5959

60-
// Create 5000 subscribers
61-
const subscribers = Array.from({ length: 5000 }, (_, i) => ({
62-
name: `Subscriber ${i + 1}`,
63-
email: `subscriber${i + 1}@example.com`,
64-
organizationId: orgId,
65-
createdAt: dayjs().subtract(12, "days").toDate(),
66-
}))
60+
// Create 5000 subscribers distributed over 2 years (from 2 years ago to today)
61+
const twoYearsAgo = dayjs().subtract(2, "years")
62+
const now = dayjs()
63+
const daysInTwoYears = now.diff(twoYearsAgo, "days")
64+
const subscribers = Array.from({ length: 5000 }, (_, i) => {
65+
const progress = i / (5000 - 1)
66+
const createdAt =
67+
i === 4999
68+
? now.toDate()
69+
: twoYearsAgo
70+
.add(Math.floor(progress * daysInTwoYears), "days")
71+
.toDate()
72+
return {
73+
name: `Subscriber ${i + 1}`,
74+
email: `subscriber${i + 1}@example.com`,
75+
organizationId: orgId,
76+
createdAt,
77+
}
78+
})
6779
await prisma.subscriber.createMany({
6880
data: subscribers,
6981
skipDuplicates: true,
7082
})
71-
// Then 10 more for each day for 10 days
72-
const now = new Date()
83+
// Then 10 more for each day for 10 days, distributed over 2 years
7384
for (let d = 0; d < 10; d++) {
74-
const day = dayjs(now)
75-
.subtract(d + 1, "day")
76-
.toDate()
85+
const progress = d / 9
86+
const day =
87+
d === 9
88+
? now.toDate()
89+
: twoYearsAgo
90+
.add(Math.floor(progress * daysInTwoYears), "days")
91+
.toDate()
7792

7893
const dailySubs = Array.from({ length: 10 }, (_, i) => ({
7994
name: `DailySub ${d + 1}-${i + 1}`,

0 commit comments

Comments
 (0)