This file provides guidance for Claude Code when working on the Dirework codebase.
ALWAYS update this section when creating or discovering important docs to prevent context loss.
- Architecture diagrams → (none yet)
- Database schemas →
packages/db/src/schema/(index.ts, auth.ts, app.ts) - Problem solutions → (none yet)
- Setup guides →
.env.example,coolify.md(gitignored)
Dirework is a self-hosted Pomodoro timer and task list with Twitch chat integration, designed for co-working and body-doubling streams. Single-user per instance. Streamers login with Twitch, connect a bot account, configure OBS overlays, and viewers interact via chat commands.
Turborepo + Bun workspaces. All packages use ESM ("type": "module").
apps/web → Next.js 16 app (frontend + API), port 3001
apps/fumadocs → Fumadocs documentation site, port 4000
packages/api → tRPC routers + business logic
packages/auth → Better Auth configuration (Twitch OAuth)
packages/db → Drizzle ORM schema + client (PostgreSQL)
packages/env → t3-env environment variable validation
packages/config → Shared TypeScript configuration
bun run dev # Start all apps (web + docs)
bun run build # Build all apps for production
bun run check-types # TypeScript type checking across all packages
bun run test # Run Vitest unit tests across all packages
bun run dev:web # Web app only
bun run dev:native # Native app only
bun run db:start # Start PostgreSQL via Docker
bun run db:stop # Stop PostgreSQL
bun run db:down # Tear down database completely
bun run db:push # Push Drizzle schema to database (dev only, no migration file)
bun run db:generate # Generate a new Drizzle migration from schema changes
bun run db:studio # Open Drizzle Studio
bun run db:migrate # Apply pending Drizzle migrations
bun run db:watch # Watch database changes- Next.js 16 (App Router) with React 19, React Compiler, and typed routes enabled
- Tailwind CSS v4 via
@tailwindcss/postcss - shadcn/ui (base-lyra style) with Lucide icons
- tRPC v11 with httpBatchLink, httpSubscriptionLink (SSE), splitLink, and
createTRPCOptionsProxyfor type-safe API - TanStack React Query for client-side data fetching
- TanStack React Form for form handling
- Better Auth with Twitch social provider (30-day sessions)
- Drizzle ORM with PostgreSQL 17 (Docker) and
drizzle-orm/node-postgres - Sonner for toast notifications
- next-themes for dark/light mode
- Google Fonts — Montserrat (headings/timer) + Roboto (body text)
- Fumadocs with Orama search for documentation
- TypeScript 5 in strict mode everywhere
The web app uses @/ mapping to apps/web/src/:
@/components— React components@/components/ui— shadcn/ui primitives (button, input, label, dropdown-menu, tooltip, tabs, etc.)@/components/theme-center— Theme Center editor components@/lib— utilities (auth-client, cn helper, config-types, theme-presets)@/utils— tRPC client setup
Internal packages are imported as @dirework/api, @dirework/auth, @dirework/db, @dirework/env.
- Functional components only, PascalCase names
"use client"directive on all interactive components- Server components only for auth checks and data loading (e.g.,
dashboard/page.tsx) - Styling via Tailwind utility classes + CSS variables for theming
- Class merging with
clsx+tailwind-mergeviacn()helper - Components using
useSearchParamsmust be wrapped in<Suspense>in their parent server component
Next.js typed routes are enabled. When using Link with dynamic href from arrays/objects, use as const on literal route strings to preserve the type:
const navItems = [
{ href: "/dashboard" as const, label: "Dashboard" },
];
// <Link href={item.href}> works because href is a string literal type- Montserrat — used for headings (
font-headingCSS class /--font-headingvariable) and timer display text - Roboto — used for body text (
font-sans/--font-robotovariable) - Loaded via
next/font/googlein root layout; overlay layout loads via Google Fonts CDN<link>tag
Routers live in packages/api/src/routers/. Two procedure types:
publicProcedure— no auth required (used by overlays)protectedProcedure— throws UNAUTHORIZED if no session
Router structure: user, task, timer, config, overlay.
Pure logic extracted for testability:
packages/api/src/routers/timer-logic.ts—DEFAULTS,getTimerConfig(),computeNextPhase()(timer state machine)apps/web/src/lib/timer-utils.ts—toHexOpacity(),formatTime(),roundedRectPath()(display helpers)apps/web/src/lib/task-utils.ts—groupTasksByAuthor(), re-exportedtoHexOpacity()(task grouping)
Context provides session (from Better Auth) and db (Drizzle client).
Drizzle ORM schema split across files in packages/db/src/schema/:
auth.ts— user, session, account, verification (Better Auth managed)app.ts— botAccount, task, timerState, timerConfig, timerStyle, taskStyle, botConfig (app-specific)index.ts— re-exports all tables + defines allrelations()for relational queries
Drizzle config: packages/db/drizzle.config.ts. Generated migrations: packages/db/drizzle/.
Key conventions:
- DB columns use snake_case; TypeScript field names use camelCase
- IDs use
text().primaryKey().$defaultFn(() => createId())(from@paralleldrive/cuid2) - User table extended with
twitchId,displayName,overlayTimerToken,overlayTasksToken - Tasks have priority system: 0 = broadcaster (pinned top), 1 = viewers
- TimerState is a state machine: idle → starting → work → break → longBreak → paused → finished
Database architecture uses 4 focused config models instead of one monolithic table:
timerConfig— timer durations, cycles, behavior flags, phase labels (17 columns)timerStyle— timer overlay appearance: dimensions, ring, colors, fonts (21 columns)taskStyle— task list overlay appearance: header, body, items, checkboxes, bullets (57 columns)botConfig— bot toggles, command aliases (jsonb), task messages (18), timer messages (14)
All columns have Drizzle .default() values — row creation only requires { userId }. Records are lazily provisioned on first access via ensureUserConfig() in the config router.
The API layer maps flat DB columns to nested frontend objects via build helpers (buildTimerConfig, buildTimerStylesConfig, buildTaskStylesConfig, buildBotConfig) and flattens writes via flattenTimerStyles/flattenTaskStyles.
- Better Auth handles Twitch OAuth login via
drizzleAdapter - Bot account connection is a separate OAuth flow via
/api/bot/authorize→/api/bot/callback/twitch - Bot callback includes error reason in redirect query params for user-facing toast notifications
- Overlay access uses UUID tokens (no auth needed), regenerable per user
Public routes at /overlay/t/[token] (timer) and /overlay/l/[token] (task list). Transparent backgrounds for OBS browser sources. Overlays use Server-Sent Events (SSE) via tRPC subscriptions for real-time updates (replaces polling).
SSE infrastructure:
packages/api/src/events.ts— in-processEventEmitterbus emittingtimerStateChange:{userId}andtaskListChange:{userId}eventstrpc.overlay.onTimerState/trpc.overlay.onTaskList— SSE subscription procedures that yield initial state then stream changesapps/web/src/utils/trpc.ts—splitLinkroutes subscriptions tohttpSubscriptionLink, queries/mutations tohttpBatchLink- Task and timer mutations emit events after DB writes; overlay subscriptions listen and push fresh data
Timer overlay supports two progress ring shapes:
- Circle — standard SVG
<circle>withstrokeDasharray/strokeDashoffset - Rounded rectangle (squircle) — SVG
<rect>with configurableborderRadius, macOS-style (default 22%)
Overlays receive pre-built nested config objects from trpc.overlay.* public procedures — no client-side merging needed.
Task list overlay groups tasks by author — each author gets a styled card container with a tinted header row showing their name and done/total count. Individual tasks render inside the container. Grouping uses authorTwitchId (falls back to authorDisplayName). Component: src/components/task-list-display.tsx.
Two-column layout: editor (left) + live preview (right).
Key files:
src/lib/config-types.ts— TypeScript interfaces forTimerStylesConfig,TaskStylesConfig,TimerConfigData,BotConfigData,AppConfigsrc/lib/theme-presets.ts— 11 theme presets + default style objectssrc/components/theme-center/— All editor components (ThemeBrowser, ThemeCard, TimerStyleEditor, TaskStyleEditor, PhaseLabelsEditor, ColorInput, FontSelect, SectionGroup, StylePreviewPanel)src/app/(app)/dashboard/styles/— Page and client component
Theme presets (11 total): Default, Liquid Glass Light, Liquid Glass Dark, Neon Cyberpunk, Cozy Cottage, Ocean Depths, Sakura, Retro Terminal, Minimal Light, Sunset, Twitch Purple.
Data flow:
- Load saved config via
trpc.config.get— returns pre-built nested{ timerConfig, timerStyles, taskStyles, botConfig } - Theme "Apply" or editor changes update working state (instant preview)
- "Save" calls
config.updateTimerStyles+config.updateTaskStyles+config.updatePhaseLabelsmutations - API flattens nested objects back to flat DB columns via
flattenTimerStyles/flattenTaskStyles
Phase Labels editor lives in the Timer tab (moved from Bot Settings — it's a timer display concern, not a bot concern). Style preview panel includes a timer animation toggle (play/pause) for live countdown simulation. Task list respects scroll.enabled toggle to switch between infinite scroll and static overflow.
- Time-of-day greetings (morning/afternoon/evening/night) with
suppressHydrationWarning - Overlay previews use iframes pointing to actual overlay pages (
/overlay/t/[token]and/overlay/l/[token]) - Bot connection feedback via URL search params (
?bot=connectedor?bot=error&reason=...) with toast notifications - Task manager groups tasks by author with per-author pending/done counts. Component:
src/components/task-manager.tsx
Two-column responsive layout (max-w-5xl):
- Left column (sticky sidebar,
lg:w-80): Bot Account card, Task/Timer command toggle cards, Command Aliases editor - Right column (scrollable): Task Messages + Variable Reference, Timer Messages + Variable Reference
- Collapses to single column on mobile (
< lg) - Components in
src/components/bot-settings/(message-editor, command-alias-editor, variable-reference) - Bot callback redirects use
env.BETTER_AUTH_URLinstead ofrequest.urlfor correct behavior behind reverse proxies
- Components depending on client-only state (e.g.,
next-themesresolved theme) must use amountedstate pattern to avoid hydration mismatches - Render a placeholder during SSR, swap to real content after
useEffectmount - When using controlled components (e.g., Base UI Switch), always pass the controlled prop (e.g.,
checked={false}) even in the pre-mount placeholder to avoid uncontrolled-to-controlled warnings
Vitest unit tests across packages/api and apps/web. Run with pnpm test.
Test file locations:
packages/api/src/routers/__tests__/config.test.ts— build helpers (buildTimerConfig, buildTimerStylesConfig, buildTaskStylesConfig, buildBotConfig)packages/api/src/routers/__tests__/timer-logic.test.ts— timer state machine (computeNextPhase), getTimerConfig defaultspackages/api/src/routers/__tests__/config-flatten.test.ts— flattenTimerStyles, flattenTaskStyles (full, partial, empty inputs)packages/api/src/routers/__tests__/config-roundtrip.test.ts— flatten → build round-trip consistency for both style typespackages/api/src/__tests__/events.test.ts— EventEmitter config, emit/receive, cross-fire isolationapps/web/src/lib/__tests__/timer-utils.test.ts— toHexOpacity, formatTime, roundedRectPathapps/web/src/lib/__tests__/task-utils.test.ts— groupTasksByAuthor (grouping, counts, ordering, fallback)apps/web/src/lib/__tests__/config-types.test.ts— TypeScript interface shape validationapps/web/src/lib/__tests__/theme-presets.test.ts— theme preset structure and uniqueness
When adding new pure functions, extract them into testable modules (not inline in components/routers) and add corresponding tests.
CI workflow: .github/workflows/ci.yml
- Triggers on push to
devandmain, and PRs tomain - Steps: install → check-types → build → test (no codegen step — Drizzle is schema-as-code)
SKIP_ENV_VALIDATION=trueis set to bypass t3-env during CI (no runtime secrets)
Docs deployment: .github/workflows/deploy-docs-to-pages.yml
- Triggers on push to
main - Builds fumadocs as static export → deploys to GitHub Pages
Deployed via Coolify using Dockerfile. Config in Dockerfile + docker-entrypoint.sh.
- Next.js uses
output: "standalone"for containerized deployment SKIP_ENV_VALIDATION=trueis set at build time to bypass t3-env validation (runtime secrets aren't available during build)- Docker build: install deps → build Drizzle migration bundle → build Next.js → copy static assets
- Start command:
node apps/web/.next/standalone/apps/web/server.js - PostgreSQL 17 Alpine as a separate Coolify service
- Instance-specific notes live in
coolify.md(gitignored) - Environment variable reference in
.env.example
Deployed via GitHub Actions (see CI/CD section above).
- Fumadocs uses
output: "export"for static site generation basePathis set dynamically viaNEXT_PUBLIC_BASE_PATHenv var fromactions/configure-pages(resolves to/direworkfor GitHub Pages subpath)- Uses ocean (blue) color preset (
fumadocs-ui/css/ocean.css) - GitHub link in nav bar via
githubUrlin shared layout options
main— production branchdev— development branch- Work on
dev, PR tomainfor releases
Defined in packages/env/src/server.ts. Required:
DATABASE_URL— PostgreSQL connection stringBETTER_AUTH_SECRET— min 32 charactersBETTER_AUTH_URL— app URL (e.g.,http://localhost:3001)CORS_ORIGIN— allowed CORS originTWITCH_CLIENT_ID/TWITCH_CLIENT_SECRET— from dev.twitch.tv
Optional:
ALLOWED_TWITCH_IDS— comma-separated allowlist (empty = allow all)PRIVACY_POLICY_URL— URL to Privacy Policy page (set to show link in footer)TERMS_OF_SERVICE_URL— URL to Terms of Service page (set to show link in footer)NODE_ENV— development/production/testSKIP_ENV_VALIDATION— set to"true"during CI/build to skip env validation
Both the web app and docs site use the same footer format:
© {year} DireWork by MrDemonWolf, Inc. — both names are links (no underline, font-medium, hover highlight). "DireWork" links to the GitHub repo, "MrDemonWolf, Inc." links to mrdemonwolf.com.
The web app footer also conditionally shows Privacy Policy and Terms of Service links when the corresponding env vars (PRIVACY_POLICY_URL, TERMS_OF_SERVICE_URL) are set. If neither is set, the legal links row is hidden.
- Web app: inline in
apps/web/src/app/(app)/layout.tsx - Docs: shared
Footercomponent inapps/fumadocs/src/components/footer.tsx, rendered from root layout
The README follows the MrDemonWolf format (see mrdemonwolf/fluffboost for reference). Section order: Title with tagline, Description, Features, Getting Started, Usage, Tech Stack, Development (Prerequisites, Setup, Scripts, Code Quality), Project Structure, License badge, Contact, Footer. No emojis. Bold feature names. Aligned tables. Code blocks with language tags.