Skip to main content
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:
FileResponsibility
contracts.tsDeclares procedure signatures (input type, output type, error types)
schemas.tsTypeBox schema definitions used by contracts
*.service.tsOrchestration: calls domain logic and external APIs
routers/*.tsThin 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");

Build docs developers (and LLMs) love