This project was created with Better-T-Stack, a modern TypeScript stack that combines React, TanStack Start, Self, ORPC, and more.
- TypeScript - For type safety and improved developer experience
- TanStack Start - SSR framework with TanStack Router
- TailwindCSS - Utility-first CSS for rapid UI development
- Shared UI package - shadcn/ui primitives live in
packages/ui - oRPC - End-to-end type-safe APIs with OpenAPI integration
- Drizzle - TypeScript-first ORM
- PostgreSQL - Database engine
- Authentication - Better-Auth
- Husky - Git hooks for code quality
- Oxlint - Oxlint + Oxfmt (linting & formatting)
- Turborepo - Optimized monorepo build system
First, install the dependencies:
pnpm installThis project uses PostgreSQL with Drizzle ORM.
-
Make sure you have a PostgreSQL database set up.
-
Update your
apps/web/.envfile with your PostgreSQL connection details. -
Apply the schema to your database:
pnpm run db:pushThen, run the development server:
pnpm run devOpen http://localhost:3001 in your browser to see the fullstack application.
To connect a ChatGPT plan (optional), set CHATGPT_CLIENT_ID and CHATGPT_HOST_ID in apps/web/.env and keep BETTER_AUTH_URL=http://localhost:3001. ChatGPT's redirect lands on http://127.0.0.1:3001/auth/callback and bounces back to localhost, where the session cookie lives.
Vitest runs every package's tests from the root config (vitest.config.ts):
pnpm db:start # the compose Postgres
pnpm test # whole suite, through Turbo
pnpm test:vitest packages/botsdk/memory # one package, file or folder, without Turbo
pnpm test:watch # watch modeThe compose Postgres image is pgvector/pgvector:0.8.7-pg18-trixie (Postgres 18 with pgvector). The SDK no longer needs pgvector (spec #737), so a plain postgres:18 works too.
*.test.tsfiles are unit tests.*.integration.test.tsfiles run one at a time against a real Postgres database: it is created if missing, migrated once per run, and every table is truncated before each test.- The test database defaults to
postgresql://postgres:password@localhost:5432/ai-chat_test. PointTEST_DATABASE_URLat another one, for example to give parallel runs their own database:TEST_DATABASE_URL=postgresql://postgres:password@localhost:5434/ai-chat_test_2 pnpm test. - Tests never call a Provider, Tavily or the network. Each package keeps its tests in its own
test/folder. Build a real instance withcreateTestSdk, over the fakes in@botsdk/testing(scripted model adapter, fake Computer, fake channel, search client);@ai-chat/test-supportwraps it with the test database and holds the repo's other test helpers. Live-model evals live in the privatepackages/evalsand never run in CI.
CI (.github/workflows/ci.yml) runs lint, typecheck and tests on every pull request.
React web apps in this stack share shadcn/ui primitives through packages/ui.
- Change design tokens and global styles in
packages/ui/src/styles/globals.css - Update shared primitives in
packages/ui/src/components/* - Adjust shadcn aliases or style config in
packages/ui/components.jsonandapps/web/components.json
Run this from the project root to add more primitives to the shared UI package:
npx shadcn@latest add accordion dialog popover sheet table -c packages/uiImport shared components like this:
import { Button } from "@ai-chat/ui/components/button";If you want to add app-specific blocks instead of shared primitives, run the shadcn CLI from apps/web.
Each app owns its environment schema in .env.schema. Varlock generates src/env.ts during installation; run pnpm run env:generate after changing a schema. Commit schemas, and keep secrets in ignored env files or your deployment platform.
Import the generated ENV accessor in application code. Shared database and auth packages receive configuration or initialized clients from the application. See Varlock's monorepo guide.
Bun's automatic env loading is disabled in bunfig.toml; the framework integration or server bootstrap loads Varlock. Node deployments must include Varlock and its dependencies alongside the app schema.
Run standalone Node/Bun tools that use Varlock from the owning app directory so they load that app's schema and env files. env:generate only generates TypeScript files; it does not initialize environment values in a subsequent command.
Rate limits count each client by its IP, read from x-forwarded-for unless TRUSTED_IP_HEADERS names other headers (e.g. cf-connecting-ip on Cloudflare). Production must run behind a proxy that sets that header. Otherwise no client IP is found, and every client shares one rate-limit bucket keyed no-trusted-ip|<path>.
- Target: web + server
- Config:
docker-compose.yml(app Dockerfiles live inapps/*/Dockerfile) - Build images: pnpm run docker:build
- Start: pnpm run docker:up
- Logs: pnpm run docker:logs
- Stop: pnpm run docker:down
Environment variables are read from each app's .env file (baked into web builds for public variables) and overridden in docker-compose.yml for container networking.
For more details, see the guide on Deploying with Docker Compose.
- Initialize hooks:
pnpm run prepare - Run checks:
pnpm run check
ai-chat/
├── apps/
│ └── web/ # Fullstack application (React + TanStack Start)
├── packages/
│ ├── ui/ # Shared shadcn/ui components and styles
│ ├── api/ # API layer / business logic
│ ├── auth/ # Authentication configuration & logic
│ └── db/ # Database schema & queries
pnpm run dev: Start all applications in development modepnpm run build: Build all applicationspnpm run dev:web: Start only the web applicationpnpm run check-types: Check TypeScript types across all appspnpm run db:push: Push schema changes to databasepnpm run db:generate: Generate database client/typespnpm run db:migrate: Run database migrationspnpm run db:studio: Open database studio UIpnpm run check: Run Oxlint and Oxfmtpnpm run docker:build: Build the Docker Compose imagespnpm run docker:up: Build and start the Docker Compose stackpnpm run docker:logs: Tail logs from the Docker Compose stackpnpm run docker:down: Stop the Docker Compose stack
After changing auth plugins or schema options, run pnpm run auth:generate from the project root. The script runs the Better Auth CLI through varlock run from the owning app directory, loading the auth instance from src/services.ts. Review the schema changes, then use your ORM's migration workflow to apply them.