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
9 changes: 9 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
dist
build
.next
coverage
*.min.js
pnpm-lock.yaml
**/prisma/migrations
**/prisma/client
20 changes: 20 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"plugins": ["@trivago/prettier-plugin-sort-imports"],
"importOrder": [
"^react(-dom)?$",
"^react-",
"^next",
"^@?\\w",
"^@repo/",
"^@/",
"^\\.\\./",
"^\\./"
],
"importOrderSeparation": false,
"importOrderSortSpecifiers": true
}
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 pnpm install --frozen-lockfile
RUN timeout 60 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 pnpm install --frozen-lockfile
RUN timeout 60 pnpm install --frozen-lockfile

COPY . .

Expand Down
1 change: 1 addition & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
- Added ESLint configurations for all packages and apps
- Updated TypeScript configurations across the monorepo
- Consolidated `@types/react` version overrides for consistency
- Added Prettier configuration with automatic import sorting
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
Loading