Monorepo for an interactive programming learning platform: courses, lessons, tasks, and live React / Vue sandboxes backed by Docker. Built on Better-T-Stack (TanStack Start, Fastify, oRPC, Drizzle, Better Auth).
| Requirement | Notes |
|---|---|
| Node.js | 20+ recommended |
| pnpm | corepack enable then use the version from packageManager in root package.json |
| PostgreSQL | Required for Drizzle / auth / app data |
| Docker | Required to build and run playground preview containers (see below) |
-
Install dependencies
pnpm install
-
Configure the server — copy or edit
apps/server/.envwith at least:DATABASE_URL— PostgreSQL connection string- Values required by
@ctl/env/server(Better Auth, OAuth, CORS, UploadThing, Google AI, etc. — see that package for the full schema)
Playground sandboxes use
PLAYGROUND_DOCKER_IMAGE, which defaults toctl-playground:latest. Override only if you tag the image differently. -
Apply the database schema
pnpm db:push
For versioned migrations in other environments, use
pnpm db:migrateafterpnpm db:generatewhen you change schema. -
Build the playground image (once, or after template changes)
pnpm docker:playground
Equivalent to:
docker build -t ctl-playground:latest -f infra/playground/Dockerfile infra/playground
The first build can take several minutes (
npm installruns for both React and Vue templates inside the image). -
Start development
pnpm dev
| Service | URL |
|---|---|
| Web app | http://localhost:3001 |
| API | http://localhost:3000 |
The API spawns per-session containers (see packages/api playground session logic) using the image built from infra/playground/Dockerfile. It bundles React and Vue Vite templates under infra/playground/templates/.
- Ensure Docker Desktop (or compatible daemon) is running before using sandbox / preview features.
- Re-run
pnpm docker:playgroundafter changing templates or the Dockerfile.
| Command | Description |
|---|---|
pnpm dev |
Start web + server (and other dev tasks via Turborepo) |
pnpm dev:web |
Web app only |
pnpm dev:server |
API server only |
pnpm build |
Production build for apps/packages in the pipeline |
pnpm check |
Lint and format check (Ultracite / Biome) |
pnpm fix |
Apply safe lint/format fixes |
pnpm check-types |
TypeScript project references across the monorepo |
pnpm db:push |
Push Drizzle schema to the database (dev-friendly) |
pnpm db:generate |
Generate migrations from schema changes |
pnpm db:migrate |
Run migrations |
pnpm db:studio |
Open Drizzle Studio |
pnpm docker:playground |
Build ctl-playground:latest for sandboxes |
ctl/
├── apps/
│ ├── web/ # TanStack Start + Vite frontend
│ └── server/ # Fastify: oRPC, auth proxy, playground preview
├── packages/
│ ├── api/ # oRPC routers, playground session, AI runner
│ ├── auth/ # Better Auth
│ ├── db/ # Drizzle schema & migrations
│ ├── env/ # Typed env (web + server)
│ ├── ui/ # Shared shadcn/Radix components & styles
│ └── config/ # Shared TS config
└── infra/
└── playground/ # Dockerfile + React/Vue sandbox templates
Shared primitives live in packages/ui.
- Tokens and globals:
packages/ui/src/styles/globals.css - Components:
packages/ui/src/components/* - Config:
packages/ui/components.json,apps/web/components.json
Add components to the shared package from the repo root:
npx shadcn@latest add accordion dialog popover sheet table -c packages/uiimport { Button } from "@ctl/ui/components/button";For app-only blocks, run the shadcn CLI from apps/web.