An opinionated full-stack TypeScript template built with Effect, React Router Framework, TanStack Query, Drizzle ORM, PostgreSQL, and Better Auth.
- Effect v4 — backend effects, services, failures, schemas, and structured concurrency
- React Router 8 Framework Mode + Vite — server rendering, loaders, hydration, and server routes
- Orval + TanStack Query — an OpenAPI-generated browser and SSR query client
- shadcn/ui + Tailwind CSS — accessible UI components without application-specific handwritten CSS
- React 19 — hydrated user interface
- Drizzle ORM + PostgreSQL — typed persistence
- Better Auth — same-origin cookie sessions
- Docker Compose — application services, PostgreSQL, Redis, and S3-compatible storage
effect-stack/
├── apps/
│ ├── backend/ # Effect API, handlers, auth, and database
│ └── frontend/
│ ├── src/routes/ # React Router route modules and API proxy
│ ├── src/components/ui/ # shadcn/ui components generated by its CLI
│ ├── src/generated/ # Orval models and TanStack Query hooks
│ ├── src/components/ # Application UI
│ └── src/lib/ # Auth, localization, fetch, and query runtime
├── packages/api/ # Shared HttpApi contract and OpenAPI document
├── Dockerfile.backend
├── Dockerfile.frontend # Node.js React Router SSR image
└── compose.production.yaml
- Node.js 24
- Yarn 4 through Corepack
- Docker Engine or Docker Desktop
- Task for the documented workspace commands
Install dependencies:
corepack enable
yarn installStart PostgreSQL, Redis, and RustFS, migrate the database, then run the backend and React Router development servers:
task devOpen http://localhost:5173. The React Router server uses http://localhost:30000 as BACKEND_URL by default. Override it when the backend is elsewhere:
BACKEND_URL=http://localhost:30000 task frontend:devBrowser authentication and API requests stay on /api. The React Router server proxies that path to BACKEND_URL, so session cookies remain same-origin. Initial todo data is fetched during SSR and dehydrated into the TanStack Query cache; later queries and mutations use the same generated client in the browser.
Useful infrastructure commands:
task infra:config
task dev:status
task dev:logs -- postgres
task dev:stop # stop containers and preserve volumes
task dev:destroy # delete development containers and data@effect-stack/api is the source of truth for endpoint paths, schemas, failures, and middleware. It exports openApiDocument from the same Api value implemented by the backend.
Generate the frontend client after changing an API contract:
task frontend:generateThe command writes a temporary OpenAPI JSON document, then Orval regenerates apps/frontend/src/generated/. The generated query functions are used by both React Router loaders and hydrated React components. Do not edit generated files by hand.
The running backend also serves /api/openapi.json and Scalar API documentation at /api/docs.
- Declare routes in
apps/frontend/src/routes.tsand implement them undersrc/routes/. - Use Orval-generated TanStack Query hooks and query options; the frontend does not use Effect Atom.
- Add shadcn/ui components with
yarn dlx shadcn@latest add <component>fromapps/frontend/. - Use shadcn components and Tailwind utilities for layout and presentation.
src/app.cssis the CLI-generated theme layer; do not add handwritten component CSS. - Keep every user-visible string in
src/lib/localization.ts. English and Simplified Chinese resources are checked by TypeScript. - Keep
BACKEND_URLserver-only. Browser code must call same-origin/api.
task typecheck # type-check every workspace
task test # API contract and frontend behavior tests
task lint # lint backend and frontend
task build # build backend and frontend SSR bundles
task infra:config # render and validate Compose configurationThe frontend build regenerates its OpenAPI client before compiling, which catches drift between the shared contract and generated code.
Copy the production example and replace every placeholder:
cp .env.production.example .env.production
docker compose --env-file .env.production -f compose.yaml -f compose.production.yaml configBETTER_AUTH_URL and BETTER_AUTH_TRUSTED_ORIGINS should both use the public frontend origin, such as https://example.com.
Start dependencies, initialize storage, migrate, and launch the applications:
docker compose --env-file .env.production -f compose.yaml -f compose.production.yaml up -d --wait postgres redis rustfs
docker compose --env-file .env.production -f compose.yaml -f compose.production.yaml run --rm rustfs-setup
docker compose --env-file .env.production -f compose.yaml -f compose.production.yaml run --rm --build migrate
docker compose --env-file .env.production -f compose.yaml -f compose.production.yaml up -d --build --wait backend frontendThe frontend container runs the React Router Node server on port 3000. It performs SSR and forwards /api to the backend through the private Compose network.
Build images directly:
docker build -f Dockerfile.frontend -t effect-stack/frontend .
docker build -f Dockerfile.backend -t effect-stack/backend .
docker build -f Dockerfile.backend --target migrate -t effect-stack/migrate .