Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ dist
.env.*
*.md
tests
.mocharc.yml
eslint.config.js
4 changes: 0 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ OWNER_ID=""
MAIN_GUILD_ID=""
MAIN_CHANNEL_ID=""

# Post Hog configuration
POSTHOG_API_KEY=""
POSTHOG_HOST="https://[replacement].posthog.com"

# Premium subscription settings
PREMIUM_ENABLED=false
DISCORD_PREMIUM_SKU_ID=""
Expand Down
62 changes: 15 additions & 47 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,23 @@ on:

jobs:
test:
name: Test & Build
name: Test & Typecheck
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [20.x, 22.x, 24.x]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
bun-version: latest

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Generate Prisma client
run: pnpm db:generate
run: bun install --frozen-lockfile

- name: Run tests with coverage
run: pnpm test:coverage
run: bun test:coverage
env:
DATABASE_URL: postgres://ci:ci@localhost:5432/fluffboost
REDIS_URL: redis://localhost:6379
Expand All @@ -51,34 +38,20 @@ jobs:
OWNER_ID: ci-test-owner-id
MAIN_GUILD_ID: ci-test-guild-id
MAIN_CHANNEL_ID: ci-test-channel-id
POSTHOG_API_KEY: ci-test-posthog-key
POSTHOG_HOST: https://ci-test-posthog.example.com

- name: Run ESLint
run: pnpm lint:check
run: bun run lint:check

- name: Run TypeScript type check
run: pnpm tsc --noEmit

- name: Build project
run: pnpm build
run: bun run typecheck

- name: Upload coverage report
if: matrix.node-version == '22.x'
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 14

- name: Archive build artifacts
if: matrix.node-version == '22.x'
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: dist/
retention-days: 7

security:
name: Security Audit
runs-on: ubuntu-latest
Expand All @@ -87,25 +60,20 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
version: 9

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22.x"
cache: "pnpm"
bun-version: latest

- name: Install dependencies
run: pnpm install --frozen-lockfile
run: bun install --frozen-lockfile

- name: Run security audit
run: pnpm audit
run: bun pm audit
continue-on-error: true

- name: Check for outdated packages
run: pnpm outdated
run: bun pm outdated
continue-on-error: true

docker:
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ dist
# macOS Files
.DS_Store

# Prisma ORM files generated in src (see prisma/schema.prisma)
src/generated/prisma/
# Drizzle ORM
drizzle/meta/
# Claude Code
.claude
6 changes: 0 additions & 6 deletions .mocharc.yml

This file was deleted.

71 changes: 33 additions & 38 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,45 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

FluffBoost is a Discord bot (Discord.js v14) that delivers daily motivational quotes and manages bot status activities. It runs as a sharded bot process with an Express health-check API, PostgreSQL database (via Prisma 7), and BullMQ background jobs backed by Redis.
FluffBoost is a Discord bot (Discord.js v14) that delivers daily motivational quotes and manages bot status activities. It runs as a sharded bot process with an Express health-check API, PostgreSQL database (via Drizzle ORM), and BullMQ background jobs backed by Redis.

## Commands

```bash
# Development
pnpm dev # Run with tsx watch (hot reload)
pnpm build # Compile TypeScript to dist/
pnpm start # Run compiled dist/app.js
bun dev # Run with --watch (hot reload)
bun start # Run src/app.ts directly (Bun runs TS natively)

# Linting & formatting
pnpm lint # ESLint with auto-fix
pnpm lint:check # ESLint check only (used in CI)
pnpm format # Prettier formatting
bun run lint # ESLint with auto-fix
bun run lint:check # ESLint check only (used in CI)
bun run format # Prettier formatting

# Database
pnpm db:generate # Generate Prisma client (outputs to src/generated/prisma/)
pnpm db:push # Sync schema to database (dev)
pnpm db:migrate # Run migrations (production)
pnpm db:studio # Open Prisma Studio UI
bun run db:generate # Generate a new Drizzle migration
bun run db:push # Push schema changes to database (dev)
bun run db:migrate # Run migrations (production)
bun run db:studio # Open Drizzle Studio UI
bun run db:pull # Introspect database and generate schema

# Type checking
pnpm tsc --noEmit # TypeScript check without emitting
bun run typecheck # TypeScript check without emitting (tsc --noEmit)

# Tests
pnpm test # Mocha tests (cross-env NODE_ENV=test)
pnpm test:coverage # Tests with c8 coverage report
bun test # bun:test runner (NODE_ENV=test)
bun test --coverage # Tests with coverage report

# Infrastructure
docker compose up # Start PostgreSQL 16 + Redis 7 locally
```

**After changing `prisma/schema.prisma`**, always run `pnpm db:generate` to regenerate the client, then `pnpm db:push` (dev) or `pnpm db:migrate` (prod) to sync the database.
**After changing `src/database/schema.ts`**, run `bun run db:push` (dev) to sync changes, or `bun run db:generate` then `bun run db:migrate` (prod) to create and apply a migration. No code generation step is needed — Drizzle reads the schema at runtime.

## Architecture

### Entry Points & Process Model

The app uses **Discord.js ShardingManager**. `src/app.ts` is the main process — it verifies DB/Redis connectivity, starts the Express API server, then spawns shard processes that each run `src/bot.ts`. In development, shards are loaded via tsx; in production, from compiled JS in `dist/`.
The app uses **Discord.js ShardingManager**. `src/app.ts` is the main process — it verifies DB/Redis connectivity, starts the Express API server, then spawns shard processes that each run `src/bot.ts`. Bun runs TypeScript directly, so the ShardingManager always points to `./src/bot.ts` with no special loader flags.

Each shard (`src/bot.ts`) creates a Discord client, registers event listeners, initializes a BullMQ queue + worker, and logs into Discord.

Expand All @@ -51,10 +51,10 @@ Each shard (`src/bot.ts`) creates a Discord client, registers event listeners, i
- `src/commands/` — Slash commands. Each file exports `slashCommand` (SlashCommandBuilder) and `execute(client, interaction)`. Subcommand groups live in subdirectories (`admin/`, `setup/`).
- `src/events/` — Discord event handlers. Command routing happens in `interactionCreate.ts` via a switch on `commandName`.
- `src/worker/` — BullMQ worker setup and job handlers (`jobs/setActivity.ts`, `jobs/sendMotivation.ts`). Jobs are dispatched on repeating schedules.
- `src/database/index.ts` — Prisma singleton using global caching pattern with `@prisma/adapter-pg`.
- `src/database/index.ts` — Drizzle ORM instance using `postgres` driver with global caching pattern.
- `src/database/schema.ts` — Drizzle schema definitions (tables, enums, types). This is the source of truth for the database schema.
- `src/utils/env.ts` — Zod schema validating all environment variables at startup. The process exits immediately on invalid config.
- `src/utils/logger.ts` — Structured consola-based logger with context-specific sub-loggers (`logger.commands.*`, `logger.database.*`, `logger.api.*`, `logger.discord.*`).
- `src/generated/prisma/` — Auto-generated Prisma client (do not edit manually).

### Command Pattern

Expand All @@ -73,7 +73,7 @@ Worker log component names use `"Worker"` consistently.

### Database Models

Four Prisma models: `Guild` (server config with per-guild motivation schedule including frequency, time, timezone, day, and `lastMotivationSentAt`), `MotivationQuote`, `SuggestionQuote` (user-submitted, pending approval), `DiscordActivity` (bot status entries with type enum). The `MotivationFrequency` enum (Daily/Weekly/Monthly) controls delivery cadence.
Four Drizzle tables defined in `src/database/schema.ts`: `guilds` (server config with per-guild motivation schedule including frequency, time, timezone, day, and `lastMotivationSentAt`), `motivationQuotes`, `suggestionQuotes` (user-submitted, pending approval), `discordActivities` (bot status entries with type enum). Two pgEnums: `motivationFrequencyEnum` (Daily/Weekly/Monthly) and `discordActivityTypeEnum` (Custom/Listening/Streaming/Playing). Types are exported as `Guild`, `MotivationQuote`, `SuggestionQuote`, `DiscordActivity`, `MotivationFrequency`, `DiscordActivityType`.

### Discord.js Patterns

Expand All @@ -89,38 +89,33 @@ Four Prisma models: `Guild` (server config with per-guild motivation schedule in
- **Strict TypeScript** — `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noUncheckedIndexedAccess` are all enabled.
- **Logging** — Use `logger` from `src/utils/logger.ts` (never raw `console.log`). Use the appropriate sub-logger for context.
- **Max line length** — 120 characters (ESLint enforced).
- **Package manager** — pnpm 9.x (do not use npm or yarn).
- **Runtime & Package manager** — Bun (do not use npm, yarn, or pnpm).

## Environment Variables

All env vars are validated by Zod in `src/utils/env.ts`. Required variables include: `DATABASE_URL`, `REDIS_URL`, `DISCORD_APPLICATION_ID`, `DISCORD_APPLICATION_PUBLIC_KEY`, `DISCORD_APPLICATION_BOT_TOKEN`, `OWNER_ID`, `MAIN_GUILD_ID`, `MAIN_CHANNEL_ID`, `POSTHOG_API_KEY`, `POSTHOG_HOST`. See `.env.example` for the full list.
All env vars are validated by Zod in `src/utils/env.ts`. Required variables include: `DATABASE_URL`, `REDIS_URL`, `DISCORD_APPLICATION_ID`, `DISCORD_APPLICATION_PUBLIC_KEY`, `DISCORD_APPLICATION_BOT_TOKEN`, `OWNER_ID`, `MAIN_GUILD_ID`, `MAIN_CHANNEL_ID`. See `.env.example` for the full list.

## CI

GitHub Actions runs on push/PR to `main` and `dev`: Prisma client generation, test execution with c8 coverage, ESLint check, TypeScript type check, build, security audit, and Docker build test. Tested on Node 20.x, 22.x, and 24.x. Uses concurrency groups to cancel in-progress runs on new pushes. Coverage reports are uploaded as artifacts on the Node 22.x run.
GitHub Actions runs on push/PR to `main` and `dev`: test execution with Bun's built-in coverage, ESLint check, TypeScript type check, security audit, and Docker build test. Uses Bun via `oven-sh/setup-bun@v2`. Coverage reports are uploaded as artifacts.

## Docker / Deployment

The app is deployed via **Coolify** using a multi-stage Dockerfile (Node 24 Alpine). The production image runs migrations at container startup via `docker-entrypoint.sh`.
The app is deployed via **Coolify** using a multi-stage Dockerfile (`oven/bun:1`). Since Bun runs TypeScript directly, there is no build step — `src/` is copied directly into the production image. Migrations run at container startup via `docker-entrypoint.sh`.

### Key files

- `Dockerfile` — Multi-stage build: base → deps → prod-deps → build → production runtime
- `docker-entrypoint.sh` — Runs `prisma migrate deploy` then starts the app. Set `SKIP_MIGRATIONS=true` to skip.
- `prisma.config.ts` — Provides `DATABASE_URL` to Prisma CLI tools (required in Prisma 7 since `url` was removed from the schema datasource block). Does not affect runtime — the app uses `@prisma/adapter-pg`.
- `Dockerfile` — Multi-stage build: base → deps → prod-deps → production runtime (no build stage needed)
- `docker-entrypoint.sh` — Runs `bunx drizzle-kit migrate` then starts the app with `bun run src/app.ts`. Set `SKIP_MIGRATIONS=true` to skip.
- `drizzle.config.ts` — Provides database credentials and schema path to Drizzle Kit CLI tools.
- `drizzle/` — Migration SQL files generated by `drizzle-kit generate`.
- `.dockerignore` — Excludes tests, docs, CI config from build context

### Prisma 7 notes

- The `datasource` block in `prisma/schema.prisma` has **no `url` property** — this is intentional for Prisma 7 with driver adapters.
- CLI tools (`prisma migrate deploy`, `prisma generate`, etc.) get the database URL from `prisma.config.ts` instead.
- Prisma is installed globally in the production image (`npm install -g prisma@7.4.0`) for migrations. `NODE_PATH=/usr/local/lib/node_modules` is set so `prisma.config.ts` can resolve `prisma/config` from the global install.

### Build optimizations

- `COPY --chown=fluffboost:fluffboost` is used instead of `RUN chown -R` to avoid a slow recursive ownership change
- `deps` and `prod-deps` stages run in parallel during Docker build
- `db:generate` and `build` are combined into a single RUN layer
- No TypeScript compilation step — Bun runs `.ts` files directly

## Git Branching

Expand All @@ -144,7 +139,7 @@ Discord provides test entitlements so you can verify your subscription flow with
**Setup:**
1. Create a subscription SKU in the [Discord Developer Portal](https://discord.com/developers/applications) under your app's Monetization settings
2. Set `PREMIUM_ENABLED=true` and `DISCORD_PREMIUM_SKU_ID=<your_sku_id>` in your `.env`
3. Run `pnpm dev`
3. Run `bun dev`

**Testing the upsell flow (no entitlement):**
- Use `/premium` — you'll see the premium info embed with a purchase button
Expand Down Expand Up @@ -184,9 +179,9 @@ if (isPremiumEnabled() && !hasEntitlement(interaction)) {

## Testing

Tests use **Mocha** + **Chai** + **Sinon** + **esmock**, configured in `.mocharc.yml` with `tsx` and `esmock` loaders. Test files live in `tests/` (mirroring `src/` structure) and use `.test.ts` suffix. ESM module mocking uses `esmock` to replace imports at load time. Time-dependent tests use `sinon.useFakeTimers()` to control `dayjs()`.
Tests use **bun:test** + **Sinon**, configured in `bunfig.toml`. Test files live in `tests/` (mirroring `src/` structure) and use `.test.ts` suffix. Module mocking uses `mock.module()` from `bun:test` to replace imports at load time. Time-dependent tests use `sinon.useFakeTimers()` to control `dayjs()`.

- `tests/helpers.ts` — Shared mock factories (mockLogger, mockPrisma, mockPosthog, mockInteraction, mockClient, mockEnv, etc.)
- `tests/helpers.ts` — Shared mock factories (mockLogger, mockDb, mockDbChain, mockInteraction, mockClient, mockEnv, etc.)
- `tests/utils/timezones.test.ts` — Timezone utilities (ALL_TIMEZONES, isValidTimezone, filterTimezones)
- `tests/utils/scheduleEvaluator.test.ts` — Schedule evaluator (getCurrentTimeInTimezone, isGuildDueForMotivation)
- `tests/utils/cronParser.test.ts` — Cron parser utilities (cronToText, isValidCron, getCronDetails)
Expand All @@ -207,8 +202,8 @@ Tests use **Mocha** + **Chai** + **Sinon** + **esmock**, configured in `.mocharc
- `tests/commands/setup/schedule.test.ts` — Schedule command (validation, premium gate)
- `tests/commands/owner/testCreate.test.ts` — Owner test-create command (owner check, SKU check)

Run `pnpm test:coverage` to generate a coverage report with `c8`.
Run `bun test --coverage` to generate a coverage report.

## Setup Notes

If `node_modules` is missing, run `pnpm install` then `pnpm db:generate` before building or type-checking.
If `node_modules` is missing, run `bun install` before type-checking. No code generation step is needed — Drizzle has no codegen.
41 changes: 14 additions & 27 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,55 +1,42 @@
# ----------------------------
# Base image
# ----------------------------
FROM node:24-alpine AS base
FROM oven/bun:1 AS base
WORKDIR /usr/src/app
RUN apk add --no-cache openssl && corepack enable

# ----------------------------
# Install all deps (cached layer)
# ----------------------------
FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY package.json bun.lock bunfig.toml ./
RUN bun install --frozen-lockfile

# ----------------------------
# Install prod deps only (runs in parallel with build)
# Install prod deps only (runs in parallel with deps)
# ----------------------------
FROM base AS prod-deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod

# ----------------------------
# Build TypeScript
# ----------------------------
FROM base AS build
COPY --from=deps /usr/src/app/node_modules ./node_modules
COPY . .
RUN pnpm db:generate && pnpm build
COPY package.json bun.lock bunfig.toml ./
RUN bun install --frozen-lockfile --production

# ----------------------------
# Production runtime
# ----------------------------
FROM node:24-alpine

FROM oven/bun:1-slim
WORKDIR /usr/src/app

RUN apk add --no-cache openssl curl \
&& npm install -g prisma@7.4.0 \
&& addgroup -S fluffboost && adduser -S fluffboost -G fluffboost
RUN apt-get update && apt-get install -y openssl curl && rm -rf /var/lib/apt/lists/* \
&& groupadd -r fluffboost && useradd -r -g fluffboost fluffboost
Comment on lines +27 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add --no-install-recommends to reduce image size and attack surface.

The static analysis tool correctly identified that apt-get install is missing the --no-install-recommends flag. This flag prevents installation of recommended but non-essential packages, reducing image size and potential vulnerabilities.

🐛 Proposed fix
-RUN apt-get update && apt-get install -y openssl curl && rm -rf /var/lib/apt/lists/* \
+RUN apt-get update && apt-get install -y --no-install-recommends openssl curl && rm -rf /var/lib/apt/lists/* \
     && groupadd -r fluffboost && useradd -r -g fluffboost fluffboost
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RUN apt-get update && apt-get install -y openssl curl && rm -rf /var/lib/apt/lists/* \
&& groupadd -r fluffboost && useradd -r -g fluffboost fluffboost
RUN apt-get update && apt-get install -y --no-install-recommends openssl curl && rm -rf /var/lib/apt/lists/* \
&& groupadd -r fluffboost && useradd -r -g fluffboost fluffboost
🧰 Tools
🪛 Trivy (0.69.3)

[error] 27-28: 'apt-get' missing '--no-install-recommends'

'--no-install-recommends' flag is missed: 'apt-get update && apt-get install -y openssl curl && rm -rf /var/lib/apt/lists/* && groupadd -r fluffboost && useradd -r -g fluffboost fluffboost'

Rule: DS-0029

Learn more

(IaC/Dockerfile)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 27 - 28, The RUN line installing packages should
include --no-install-recommends to avoid pulling unnecessary recommended
packages; update the command that contains "apt-get update && apt-get install -y
openssl curl && rm -rf /var/lib/apt/lists/* \    && groupadd -r fluffboost &&
useradd -r -g fluffboost fluffboost" so the apt-get install becomes apt-get
install -y --no-install-recommends openssl curl while preserving the following
cleanup and the groupadd/useradd steps.


COPY --from=prod-deps --chown=fluffboost:fluffboost /usr/src/app/node_modules ./node_modules
COPY --from=build --chown=fluffboost:fluffboost /usr/src/app/dist ./dist
COPY --from=build --chown=fluffboost:fluffboost /usr/src/app/package.json ./
COPY --from=build --chown=fluffboost:fluffboost /usr/src/app/src/generated ./src/generated
COPY --from=build --chown=fluffboost:fluffboost /usr/src/app/prisma ./prisma
COPY --from=build --chown=fluffboost:fluffboost /usr/src/app/prisma.config.ts ./
COPY --from=build --chown=fluffboost:fluffboost /usr/src/app/docker-entrypoint.sh ./
COPY --chown=fluffboost:fluffboost package.json bunfig.toml ./
COPY --chown=fluffboost:fluffboost src ./src
COPY --chown=fluffboost:fluffboost drizzle ./drizzle
COPY --chown=fluffboost:fluffboost drizzle.config.ts ./
COPY --chown=fluffboost:fluffboost docker-entrypoint.sh ./

USER fluffboost

ENV NODE_ENV=production
ENV NODE_PATH=/usr/local/lib/node_modules

EXPOSE 3000

Expand Down
Loading
Loading