Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,18 @@ The script bumps version in `package.json`, creates a git tag, and pushes it. Gi
### Backend Architecture

TRPC routers in `apps/backend/src/router/`:

- Each domain (user, campaign, subscriber, etc.) has its own directory
- Pattern: `router.ts` (definition), `mutation.ts` (writes), `query.ts` (reads)

Key entry points:

- `apps/backend/src/app.ts` - Express app setup, middleware, routes
- `apps/backend/src/trpc.ts` - TRPC context and auth
- `apps/backend/src/cron/` - Scheduled jobs (email sending, maintenance)

Endpoints:

- `/trpc/*` - TRPC RPC endpoints
- `/api/*` - REST API (Swagger documented)
- `/t/:id` - Link tracking redirect
Expand All @@ -75,6 +78,7 @@ Endpoints:
### Frontend Architecture

React Router app in `apps/web/src/`:

- `app.tsx` - Route definitions
- `pages/` - Page components matching routes
- TRPC client with React Query for data fetching
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ COPY packages/eslint-config/package.json ./packages/eslint-config/
COPY packages/typescript-config/package.json ./packages/typescript-config/
COPY packages/ui/package.json ./packages/ui/

RUN timeout 60 pnpm install --frozen-lockfile
RUN timeout 120 pnpm install --frozen-lockfile

COPY . .

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.node
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ COPY packages/eslint-config/package.json ./packages/eslint-config/
COPY packages/typescript-config/package.json ./packages/typescript-config/
COPY packages/ui/package.json ./packages/ui/

RUN timeout 60 pnpm install --frozen-lockfile
RUN timeout 120 pnpm install --frozen-lockfile

COPY . .

Expand Down
2 changes: 1 addition & 1 deletion apps/backend/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import globals from "globals"
import pluginJs from "@eslint/js"
import globals from "globals"
import * as tseslint from "typescript-eslint"

export default tseslint.config({
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/prisma/seed.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import dayjs from "dayjs"
import { hashPassword } from "../src/utils/auth"
import { prisma } from "../src/utils/prisma"
import { SmtpEncryption, type Prisma } from "./client"
import dayjs from "dayjs"
import { type Prisma, SmtpEncryption } from "./client"

async function seed() {
if (!(await prisma.organization.findFirst())) {
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/api/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { prisma } from "../utils/prisma"
import express, { NextFunction } from "express"
import { prisma } from "../utils/prisma"

export const authenticateApiKey = async (
req: express.Request,
Expand Down
12 changes: 6 additions & 6 deletions apps/backend/src/api/server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import crypto from "crypto"
import dayjs from "dayjs"
import express from "express"
import { prisma } from "../utils/prisma"
import { authenticateApiKey } from "./middleware"
import fs from "fs/promises"
import path from "path"
import { z } from "zod"
import { Prisma } from "../../prisma/client"
import crypto from "crypto"
import { Mailer } from "../lib/Mailer"
import fs from "fs/promises"
import path from "path"
import dayjs from "dayjs"
import { prisma } from "../utils/prisma"
import { authenticateApiKey } from "./middleware"

export const apiRouter = express.Router()

Expand Down
29 changes: 14 additions & 15 deletions apps/backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
import * as trpcExpress from "@trpc/server/adapters/express"
import path from "path"
import express from "express"
import cors from "cors"
import { prisma } from "./utils/prisma"
import express from "express"
import path from "path"
import swaggerUi from "swagger-ui-express"

import { createContext, router } from "./trpc"
import { userRouter } from "./user/router"
import { listRouter } from "./list/router"
import { organizationRouter } from "./organization/router"
import { subscriberRouter } from "./subscriber/router"
import { templateRouter } from "./template/router"
import { apiRouter } from "./api/server"
import { campaignRouter } from "./campaign/router"
import { ONE_PX_PNG } from "./constants"
import { dashboardRouter } from "./dashboard/router"
import { listRouter } from "./list/router"
import { messageRouter } from "./message/router"
import { organizationRouter } from "./organization/router"
import { settingsRouter } from "./settings/router"
import { webhookRouter } from "./webhook/router"
import swaggerSpec from "./swagger"
import { apiRouter } from "./api/server"
import { dashboardRouter } from "./dashboard/router"
import { statsRouter } from "./stats/router"
import { ONE_PX_PNG } from "./constants"
import { subscriberRouter } from "./subscriber/router"
import swaggerSpec from "./swagger"
import { templateRouter } from "./template/router"
import { createContext, router } from "./trpc"
import { userRouter } from "./user/router"
import { prisma } from "./utils/prisma"
import { handleWebhook } from "./webhook/handler"
import { webhookRouter } from "./webhook/router"

const appRouter = router({
user: userRouter,
Expand Down
6 changes: 3 additions & 3 deletions apps/backend/src/campaign/mutation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { z } from "zod"
import { authProcedure } from "../trpc"
import { prisma } from "../utils/prisma"
import { TRPCError } from "@trpc/server"
import pMap from "p-map"
import { z } from "zod"
import { Mailer } from "../lib/Mailer"
import { authProcedure } from "../trpc"
import { prisma } from "../utils/prisma"

const createCampaignSchema = z.object({
title: z.string().min(1, "Campaign title is required"),
Expand Down
8 changes: 4 additions & 4 deletions apps/backend/src/campaign/query.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { TRPCError } from "@trpc/server"
import { z } from "zod"
import { Prisma } from "../../prisma/client"
import { authProcedure } from "../trpc"
import { messageStatus } from "../utils/message-status"
import { resolveProps } from "../utils/pProps"
import { prisma } from "../utils/prisma"
import { TRPCError } from "@trpc/server"
import { paginationSchema } from "../utils/schemas"
import { Prisma } from "../../prisma/client"
import { resolveProps } from "../utils/pProps"
import { messageStatus } from "../utils/message-status"

export const listCampaigns = authProcedure
.input(z.object({ organizationId: z.string() }).merge(paginationSchema))
Expand Down
8 changes: 4 additions & 4 deletions apps/backend/src/campaign/router.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { router } from "../trpc"
import {
cancelCampaign,
createCampaign,
updateCampaign,
deleteCampaign,
startCampaign,
cancelCampaign,
sendTestEmail,
duplicateCampaign,
sendTestEmail,
startCampaign,
updateCampaign,
} from "./mutation"
import { getCampaign, listCampaigns } from "./query"

Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/cron/cleanupWebhookLogs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cronJob } from "./cron.utils"
import { prisma } from "../utils/prisma"
import dayjs from "dayjs"
import { prisma } from "../utils/prisma"
import { cronJob } from "./cron.utils"

export const cleanupWebhookLogsCron = cronJob(
"cleanup-webhook-logs",
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/cron/cron.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import cron from "node-cron"
import { sendMessagesCron } from "./sendMessages"
import { cleanupWebhookLogsCron } from "./cleanupWebhookLogs"
import { dailyMaintenanceCron } from "./dailyMaintenance"
import { processQueuedCampaigns } from "./processQueuedCampaigns"
import { cleanupWebhookLogsCron } from "./cleanupWebhookLogs"
import { sendMessagesCron } from "./sendMessages"

type CronJob = {
name: string
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/cron/dailyMaintenance.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cronJob } from "./cron.utils"
import { prisma } from "../utils/prisma"
import dayjs from "dayjs"
import { prisma } from "../utils/prisma"
import { cronJob } from "./cron.utils"

export const dailyMaintenanceCron = cronJob("daily-maintenance", async () => {
const organizations = await prisma.organization.findMany({
Expand Down
10 changes: 5 additions & 5 deletions apps/backend/src/cron/processQueuedCampaigns.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { prisma } from "../utils/prisma"
import { LinkTracker } from "../lib/LinkTracker"
import pMap from "p-map"
import { v4 as uuidV4 } from "uuid"
import { Prisma, Subscriber, SubscriberMetadata } from "../../prisma/client"
import { LinkTracker } from "../lib/LinkTracker"
import {
replacePlaceholders,
PlaceholderDataKey,
replacePlaceholders,
} from "../utils/placeholder-parser"
import pMap from "p-map"
import { Subscriber, Prisma, SubscriberMetadata } from "../../prisma/client"
import { prisma } from "../utils/prisma"
import { cronJob } from "./cron.utils"

// TODO: Make this a config
Expand Down
5 changes: 2 additions & 3 deletions apps/backend/src/cron/sendMessages.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { subSeconds } from "date-fns"
import pMap from "p-map"
import { Mailer } from "../lib/Mailer"
import { logger } from "../utils/logger"
import { prisma } from "../utils/prisma"
import { messageStatus } from "../utils/message-status"

import { prisma } from "../utils/prisma"
import { cronJob } from "./cron.utils"
import { subSeconds } from "date-fns"

export const sendMessagesCron = cronJob("sendMessages", async () => {
const organizations = await prisma.organization.findMany()
Expand Down
13 changes: 7 additions & 6 deletions apps/backend/src/dashboard/query.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { z } from "zod"
import { authProcedure } from "../trpc"
import { prisma } from "../utils/prisma"
import { TRPCError } from "@trpc/server"
import { subMonths } from "date-fns"
import pMap from "p-map"
import { z } from "zod"
import { MessageStatus } from "../../prisma/client"
import { countDbSize, subscriberGrowthQuery } from "../../prisma/client/sql"
import pMap from "p-map"
import { subMonths } from "date-fns"
import { authProcedure } from "../trpc"
import { messageStatus } from "../utils/message-status"
import { prisma } from "../utils/prisma"

export const getDashboardStats = authProcedure
.input(
Expand Down Expand Up @@ -144,7 +144,8 @@ export const getDashboardStats = authProcedure
continue
}

const prev = subscriberGrowthCumulative[i - 1]?.count ?? baselineSubscriberCount
const prev =
subscriberGrowthCumulative[i - 1]?.count ?? baselineSubscriberCount

subscriberGrowthCumulative.push({
date: point.date,
Expand Down
8 changes: 4 additions & 4 deletions apps/backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export type * from "./app"
export type * from "../prisma/client"
export type * from "./types"

import { app } from "./app"
import { initializeCronJobs } from "./cron/cron"
import { prisma } from "./utils/prisma"

export type * from "./app"
export type * from "../prisma/client"
export type * from "./types"

const cronController = initializeCronJobs()

const PORT = process.env.PORT || 5000
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/lib/Mailer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import nodemailer from "nodemailer"
import SMTPTransport from "nodemailer/lib/smtp-transport"
import { SmtpSettings } from "../../prisma/client"
import nodemailer from "nodemailer"
import { stripAngleBrackets } from "../utils/message-id"

type SendMailOptions = {
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/list/mutation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TRPCError } from "@trpc/server"
import { z } from "zod"
import { authProcedure } from "../trpc"
import { prisma } from "../utils/prisma"
import { TRPCError } from "@trpc/server"

const createListSchema = z.object({
name: z.string().min(1, "List name is required"),
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/list/query.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { TRPCError } from "@trpc/server"
import { z } from "zod"
import { Prisma } from "../../prisma/client"
import { authProcedure } from "../trpc"
import { prisma } from "../utils/prisma"
import { TRPCError } from "@trpc/server"
import { paginationSchema } from "../utils/schemas"
import { Prisma } from "../../prisma/client"

export const getLists = authProcedure
.input(
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/list/router.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { router } from "../trpc"
import { createList, updateList, deleteList } from "./mutation"
import { createList, deleteList, updateList } from "./mutation"
import { getList, getLists } from "./query"

export const listRouter = router({
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/message/mutation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { TRPCError } from "@trpc/server"
import { z } from "zod"
import { MessageStatus } from "../../prisma/client"
import { authProcedure } from "../trpc"
import { prisma } from "../utils/prisma"
import { TRPCError } from "@trpc/server"
import { MessageStatus } from "../../prisma/client"

export const resendMessage = authProcedure
.input(
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/message/query.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { TRPCError } from "@trpc/server"
import { z } from "zod"
import { Prisma } from "../../prisma/client"
import { authProcedure } from "../trpc"
import { prisma } from "../utils/prisma"
import { TRPCError } from "@trpc/server"
import { paginationSchema } from "../utils/schemas"
import { Prisma } from "../../prisma/client"

const messageStatusEnum = z.enum([
"QUEUED",
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/message/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { router } from "../trpc"
import { listMessages, getMessage } from "./query"
import { resendMessage } from "./mutation"
import { getMessage, listMessages } from "./query"

export const messageRouter = router({
list: listMessages,
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/organization/mutation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { TRPCError } from "@trpc/server"
import fs from "fs/promises"
import { z } from "zod"
import { authProcedure } from "../trpc"
import { prisma } from "../utils/prisma"
import fs from "fs/promises"
import { TRPCError } from "@trpc/server"

const createOrganizationSchema = z.object({
name: z.string().min(1, "Organization name is required"),
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/organization/query.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TRPCError } from "@trpc/server"
import { z } from "zod"
import { authProcedure } from "../trpc"
import { prisma } from "../utils/prisma"
import { TRPCError } from "@trpc/server"

export const getOrganizationById = authProcedure
.input(
Expand Down
6 changes: 3 additions & 3 deletions apps/backend/src/settings/mutation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { z } from "zod"
import { authProcedure } from "../trpc"
import { prisma } from "../utils/prisma"
import { TRPCError } from "@trpc/server"
import { randomBytes } from "crypto"
import { z } from "zod"
import { Mailer } from "../lib/Mailer"
import { authProcedure } from "../trpc"
import { prisma } from "../utils/prisma"

const smtpSchema = z.object({
organizationId: z.string(),
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/settings/query.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TRPCError } from "@trpc/server"
import { z } from "zod"
import { authProcedure } from "../trpc"
import { prisma } from "../utils/prisma"
import { TRPCError } from "@trpc/server"

export const getSmtp = authProcedure
.input(
Expand Down
Loading