Workspaces Guide
This guide covers how to work with monorepo workspaces using the @geekmidas toolbox.
Overview
The CLI supports workspace mode for monorepos, enabling:
- Unified development server orchestration across multiple apps
- Smart dependency-aware build ordering
- Shared configuration with app-specific overrides
- Automatic client regeneration when API changes
Workspace Structure
A typical workspace layout:
my-monorepo/
├── apps/
│ ├── api/ # Backend API
│ │ ├── src/
│ │ │ └── endpoints/
│ │ └── gkm.config.ts
│ ├── web/ # Frontend (Next.js, etc.)
│ │ └── package.json
│ └── admin/ # Admin dashboard
│ └── package.json
├── packages/
│ ├── shared/ # Shared utilities
│ └── types/ # Shared TypeScript types
├── gkm.config.ts # Root workspace config
├── pnpm-workspace.yaml
└── turbo.jsonWorkspace Configuration
Root Configuration
// gkm.config.ts (root)
import { defineWorkspace } from '@geekmidas/cli/config';
export default defineWorkspace({
name: 'my-monorepo',
apps: {
api: {
path: 'apps/api',
type: 'backend',
port: 3000,
routes: './src/endpoints/**/*.ts',
envParser: './src/config/env',
logger: './src/config/logger',
},
web: {
type: 'frontend',
path: 'apps/web',
port: 3001,
framework: 'nextjs',
dependencies: ['api'],
},
admin: {
type: 'frontend',
path: 'apps/admin',
port: 3002,
framework: 'nextjs',
dependencies: ['api'],
},
},
services: {
db: true,
cache: true,
mail: true,
events: 'pgboss', // or 'sns' or 'rabbitmq'
},
deploy: {
default: 'dokploy',
},
});TIP
Use defineWorkspace() (not defineConfig()) for multi-app workspaces. The defineWorkspace() helper provides type-safe dependency validation — dependencies values are checked against the app names in your config.
App-Specific Configuration
Each app can have its own gkm.config.ts:
// apps/api/gkm.config.ts
import { defineConfig } from '@geekmidas/cli/config';
export default defineConfig({
routes: './src/endpoints/**/*.ts',
envParser: './src/config/env',
logger: './src/config/logger',
telescope: { enabled: true },
studio: { enabled: true },
providers: {
server: { enableOpenApi: true },
},
});Development Workflow
Starting All Apps
# From workspace root
gkm dev
# Output:
# [api] Starting on http://localhost:3000
# [api] Telescope: http://localhost:3000/__telescope
# [web] Waiting for api...
# [api] Ready
# [web] Starting on http://localhost:3001
# [admin] Starting on http://localhost:3002Starting Specific Apps
# Start only API
gkm dev --app api
# Start API and web
gkm dev --app api --app webBuild Order
The CLI automatically determines build order based on dependencies:
gkm build
# Builds in order:
# 1. api (no dependencies)
# 2. web (depends on api)
# 3. admin (depends on api)Frontend Integration
Auto-Generated API Client
When your frontend depends on the API, the CLI can automatically regenerate the typed client when endpoints change:
// gkm.config.ts (root)
import { defineWorkspace } from '@geekmidas/cli/config';
export default defineWorkspace({
apps: {
api: {
path: 'apps/api',
type: 'backend',
port: 3000,
routes: './src/endpoints/**/*.ts',
envParser: './src/config/env',
logger: './src/config/logger',
},
web: {
type: 'frontend',
path: 'apps/web',
port: 3001,
framework: 'nextjs',
dependencies: ['api'],
// Auto-regenerate client when API changes
client: {
output: './src/api',
},
},
},
services: { db: true },
});Manual Client Generation
# Generate OpenAPI spec from API
gkm openapi --app api
# Generate React Query hooks for web app
gkm generate:react-query \
--input apps/api/.gkm/openapi.ts \
--output apps/web/src/api/hooks.tsShared Packages
Creating Shared Code
// packages/shared/src/index.ts
export * from './utils';
export * from './constants';Using in Apps
// apps/api/src/endpoints/users.ts
import { formatDate } from '@myorg/shared';
import { e } from '@geekmidas/constructs/endpoints';
export const getUser = e
.get('/users/:id')
.handle(async ({ params }) => {
const user = await db.users.find(params.id);
return {
...user,
createdAt: formatDate(user.createdAt),
};
});Environment Variables
Workspace-Level Variables
# .env (root)
DATABASE_URL=postgres://localhost:5432/myapp
REDIS_URL=redis://localhost:6379
JWT_SECRET=development-secretApp-Specific Variables
# apps/api/.env.local
PORT=3000
LOG_LEVEL=debug
# apps/web/.env.local
NEXT_PUBLIC_API_URL=http://localhost:3000Variable Resolution
The CLI resolves environment variables in order:
- App-specific
.env.local - App-specific
.env - Root
.env.local - Root
.env - System environment
Turbo Integration
The CLI integrates with Turbo for task orchestration:
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".gkm/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["^build"]
}
}
}Running with Turbo
# Build all packages and apps
turbo run build
# Run tests across workspace
turbo run test
# Development (prefer gkm dev for orchestration)
gkm devDocker Workspace Builds
Multi-App Docker Compose
gkm docker --workspace --services postgres,redisGenerates:
# docker-compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: myapp
ports:
- "5432:5432"
redis:
image: redis:7
ports:
- "6379:6379"
api:
build:
context: .
dockerfile: apps/api/Dockerfile
ports:
- "3000:3000"
depends_on:
- postgres
- redis
environment:
DATABASE_URL: postgres://postgres:postgres@postgres:5432/myapp
REDIS_URL: redis://redis:6379
web:
build:
context: .
dockerfile: apps/web/Dockerfile
ports:
- "3001:3001"
depends_on:
- api
environment:
API_URL: http://api:3000Individual App Dockerfiles
# apps/api/Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
# Copy workspace files
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
COPY package.json ./
COPY apps/api/package.json ./apps/api/
COPY packages/ ./packages/
# Install and build
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm turbo run build --filter=api
# Production
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/apps/api/.gkm/server/dist ./
EXPOSE 3000
CMD ["node", "app.js"]Deployment Strategies
Deploy Individual Apps
# Deploy only API
gkm deploy --app api --stage production
# Deploy frontend
gkm deploy --app web --stage productionDeploy All Apps
# Deploy entire workspace
gkm deploy --stage productionStaged Rollout
# 1. Deploy API first
gkm deploy --app api --stage production
# 2. Verify API health
curl https://api.example.com/health
# 3. Deploy frontends
gkm deploy --app web --app admin --stage productionBest Practices
1. Keep Shared Code Minimal
Only share code that's truly reused:
- Type definitions
- Utility functions
- Constants and configurations
2. Use Explicit Dependencies
Always declare dependencies between apps:
apps: {
web: {
dependencies: ['api'], // Explicit
},
}3. Isolate Environment Variables
Keep app-specific variables in app directories:
apps/api/.env.local # API-specific
apps/web/.env.local # Web-specific
.env # Shared infrastructure4. Use Consistent Versioning
Keep package versions in sync:
// package.json (root)
{
"pnpm": {
"overrides": {
"zod": "^3.22.0",
"hono": "^4.0.0"
}
}
}5. Separate Build and Deploy
# CI: Build all, test all
turbo run build test
# CD: Deploy independently
gkm deploy --app api --stage production
gkm deploy --app web --stage productionTroubleshooting
Port Conflicts
# Check what's using a port
lsof -i :3000
# Use different ports
gkm dev --app api --port 3100Dependency Cycles
If you see "Circular dependency detected":
- Review your
dependsOnconfiguration - Extract shared code to a package instead
- Use event-driven communication
Stale Client Types
If frontend types are out of sync:
# Force regenerate
gkm openapi --app api
gkm generate:react-query --forceBuild Cache Issues
# Clear Turbo cache
turbo run build --force
# Clear gkm artifacts
rm -rf apps/*/.gkm