Monorepo wiring
pnpm workspaces, turbo, go module.
Five surfaces in one repo means five build systems pretending to be one. This lesson is the plumbing β how pnpm, Turbo, and the Go module work together so you don't end up running every command in every folder.
pnpm-workspace.yaml β declare your TS packages
packages:- "apps/web"- "apps/admin"- "apps/mobile"- "apps/desktop/frontend"- "packages/*"
Note: desktop is special. Wails has its own Go module at apps/desktop/ AND a React frontend at apps/desktop/frontend/. Only the frontend is in the pnpm workspace.
Importing shared types
Every TS package declares @my-saas/shared as a dependency. pnpm symlinks it so changes are picked up immediately:
{"name": "@my-saas/web","dependencies": {"@my-saas/shared": "workspace:*"}}
Now in any TS file:
import { Product, ProductSchema } from '@my-saas/shared'
Same import, same path, in web / admin / mobile / desktop. The shared package becomes muscle memory.
turbo.json β the build orchestrator
{"$schema": "https://turbo.build/schema.json","pipeline": {"dev": {"cache": false,"persistent": true},"build": {"dependsOn": ["^build"],"outputs": ["dist/**", ".next/**", "!.next/cache/**"]},"lint": {},"type-check": {}}}
Now pnpm dev at the root runs every package's dev script in parallel, and pnpm build builds them in the right order based on dependencies.
The Go side β separate modules
Go doesn't play in the pnpm workspace. There are two Go modules:
apps/api/go.modβ the APIapps/desktop/go.modβ the Wails desktop wrapper
They're independent on purpose β the API ships as a Docker image; desktop ships as a native binary. Pinning their Go deps separately keeps them clean.
Running it all from the root
# Boots web + admin + mobile + desktop frontend β Turbo handles parallelismpnpm dev# API runs separately (Turbo doesn't manage Go)make api # or: cd apps/api && go run cmd/server/main.go# Or use the included Procfile + overmind:overmind start
The Procfile that ships with the scaffold defines every service. Install overmind once and you boot the whole monorepo with one command.
The .env split
One .env at the root holds shared values (DATABASE_URL, JWT_SECRET). Each app reads what it needs. Mobile is special β it can't read root .env files at runtime, so its values are baked at build time via EXPO_PUBLIC_* vars.
# Shared by API and frontendsDATABASE_URL=postgres://...JWT_SECRET=...# Frontend-side overridesNEXT_PUBLIC_API_URL=http://localhost:8080EXPO_PUBLIC_API_URL=http://192.168.1.10:8080
Quick check
Try it
Prove the workspace works:
- Edit
packages/shared/types/user.tsβ addnickname: stringto the User type. - Save. In the web app, type
user.in any tsx file β autocomplete should shownickname. - Same check in admin, mobile, desktop. All four see it immediately.
- Revert the change (we'll add fields properly via
grit syncin the next chapter).
What's next
Chapter 2 β Shared Types Everywhere. We stop hand-editing the shared package and use grit sync to generate it from Go.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled β suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub