Slides uses TurboRepo to manage a monorepo with two applications and three shared packages. This page documents every directory and explains how the pieces relate.
Top-level layout
.
├── apps/
│ ├── web/ # React 19 frontend (Vite + TanStack Router)
│ └── server/ # Elysia backend (thin host, no business logic)
├── packages/
│ ├── api/ # ORPC contracts, schemas, services, routers
│ ├── core/ # Pure domain logic (slideshow types, validation, AI helpers)
│ └── config/ # Shared env config and logger
├── docs/ # Documentation
├── turbo.json # TurboRepo pipeline configuration (guarded — do not edit)
└── bun.lock # Bun lockfile (guarded — do not edit)
turbo.json and bun.lock are treated as guarded files. Avoid editing them without explicit intent — changes to the build pipeline or lockfile can have broad effects across all packages.
apps/web/ — React frontend
apps/web/
├── src/
│ ├── app/ # App-wide setup: Router, Providers, global styles
│ ├── features/ # Feature-scoped code (the core of the application)
│ │ └── slideshow/ # Slideshow editing feature
│ │ ├── api/ # TanStack Query hooks wrapping ORPC calls
│ │ ├── components/ # UI components (blocks, panels, viewer)
│ │ ├── hooks/ # Feature-specific React hooks
│ │ ├── services/ # Frontend-side orchestration
│ │ ├── state/ # Local state stores
│ │ └── utils/ # Feature helpers
│ ├── domain/ # Domain adapters (wrap @slides/core exports)
│ │ └── slideshow/ # Per-concern adapter files
│ ├── infra/ # Infrastructure: API client setup
│ ├── routes/ # TanStack Router route definitions
│ └── components/ # Shared UI components (shadcn/ui primitives)
├── .env.example
└── package.json
The frontend is organized by Feature-Sliced Design: feature directories contain everything related to that feature — queries, components, hooks, state, and utilities — co-located rather than split by technical layer.
Do not hand-edit src/routeTree.gen.ts. This file is auto-generated by TanStack Router and will be overwritten on the next build.
apps/server/ — Elysia backend
apps/server/
├── src/
│ └── index.ts # Single entry point — wires config, adapter, ORPC handler
├── data/
│ └── slideshows/ # Default slideshow JSON files served by the binary
├── .env.example
└── package.json
The server app is intentionally minimal. Its src/index.ts does three things: reads validated environment variables from @slides/config/server, creates a filesystem adapter for slideshow persistence, and mounts the ORPC router from @slides/api on an Elysia server. All business logic lives in packages/api and packages/core.
packages/api/ — API surface
packages/api/src/
├── slideshow/
│ ├── contracts.ts # Procedure definitions: input, output, errors (WHAT)
│ ├── schemas.ts # TypeBox schemas for endpoint inputs/outputs
│ ├── assistant-service.ts # AI assistant orchestration (HOW)
│ └── service.ts # Load/save orchestration
├── routers/
│ ├── index.ts # Combine all routers into AppRouter
│ └── slideshow.ts # Thin handlers: wire contracts → services
├── context.ts # Request context (Anthropic config, FS adapter, session)
├── errors.ts # Typed error definitions
└── index.ts # Package barrel exports
The package is organized domain-first: all slideshow-related contracts, schemas, and services live under src/slideshow/. The separation between files is by responsibility:
| File | Responsibility |
|---|
contracts.ts | Declares procedure signatures (input type, output type, error types) |
schemas.ts | TypeBox schema definitions used by contracts |
*.service.ts | Orchestration: calls domain logic and external APIs |
routers/*.ts | Thin wiring: connects a contract to its service handler |
Routers are kept to under 20 lines per procedure. If orchestration logic grows, it belongs in a service file — not in the router.
packages/core/ — domain logic
packages/core/src/
├── schema/ # TypeBox schemas for slideshow structures
├── validation/ # Validation helpers built on top of schemas
├── utils/ # Domain utilities and transformations
├── types.ts # TypeScript type exports
├── transactions.ts # Patch/transaction helpers
├── ai-assistant.ts # AI helper utilities
├── prompts.ts # System prompts for Claude
└── index.ts # Public exports
This package has no dependencies on React, Elysia, or any framework. It is pure TypeScript. packages/api and both apps treat it as the single source of truth for what a slideshow is, how it is validated, and how AI operations are structured.
Apps should never import from internal files directly — consume the public API via @slides/core (or, on the frontend, via domain adapters).
packages/config/ — shared configuration
packages/config/
├── src/
│ ├── server.ts # Server env vars (TypeBox-validated, fail-fast on startup)
│ ├── client.ts # Client env vars (Vite import.meta.env)
│ └── logger.ts # Environment-aware logger (consola)
└── package.json # Exports: ./server, ./client, ./logger
Three subpath exports, each with a distinct purpose:
// Server-only environment (PORT, CORS_ORIGIN, ANTHROPIC_API_KEY, etc.)
import { env } from "@slides/config/server";
// Client-side Vite env (VITE_SERVER_URL, etc.)
import { clientEnv } from "@slides/config/client";
// Environment-aware logger
import { logger } from "@slides/config/logger";
Environment variables are TypeBox-validated at startup. If a required variable is missing, the process throws immediately rather than failing later at the call site.
How packages relate to apps
apps/web ──imports──▶ packages/api ──imports──▶ packages/core
apps/server ──imports──▶ packages/api ──imports──▶ packages/core
apps/web ──imports──▶ packages/config
apps/server ──imports──▶ packages/config
Dependency direction is strictly one-way. Packages do not import from apps. packages/core does not import from packages/api. This keeps the domain layer isolated and the dependency graph acyclic.
Import conventions
Configuration and environment
// ✅ Correct — use config helpers
import { env } from "@slides/config/server";
import { logger } from "@slides/config/logger";
// ❌ Wrong — never access process.env directly
const key = process.env.ANTHROPIC_API_KEY;
Domain adapters on the frontend
The frontend wraps @slides/core exports in adapter files under src/domain/slideshow/. Feature code always imports from the adapter, not from the package directly. This provides a single injection point for adding analytics, retry logic, or error reporting later without changing import sites.
// ✅ Correct — import via domain adapter
import { validateSlideshow } from "@/domain/slideshow/validation";
import { Slideshow } from "@/domain/slideshow/types";
// ❌ Wrong — bypasses the adapter layer
import { validateSlideshow } from "@slides/core";
Direct imports from @slides/core are only correct inside the src/domain/ adapter files themselves. All feature code should import from @/domain/slideshow/*.
Logging
// ✅ Correct
import { logger } from "@slides/config/logger";
logger.info("Slideshow loaded");
logger.error("Failed to parse", err);
// ❌ Wrong — never use console.* in production code
console.log("Slideshow loaded");