Workspace mode is the default distribution mode in pyrpc. It assumes the server and client project live on the same filesystem — a monorepo, or a repo with both server/ and client/ directories. This is the tRPC-like experience: one command starts everything, types flow automatically.
This post walks through exactly what happens when you run pyrpc devin workspace mode — config resolution, path validation, type generation, and the watcher loop.
The config
{
"version": 1,
"framework": "fastapi",
"entrypoint": "app.main",
"distribution": "workspace",
"client_root": "../frontend"
}The key fields for workspace mode are distribution (must be"workspace") and client_root (a relative or absolute path to the TypeScript client project). Everything else is standard.
Step 1: Config resolution
When pyrpc dev starts, it reads pyrpc.json from the current directory or any parent directory. The client_root path is resolved relative to the config file’s directory, not the current working directory:
def _resolve_client_root(client_root: str, config_dir: str) -> str:
p = client_root if os.path.isabs(client_root) \
else os.path.join(config_dir, client_root)
return os.path.normpath(p)If pyrpc.json lives at /project/server/pyrpc.json andclient_root is "../frontend", the resolved path is/project/frontend. This is platform-aware — Windows absolute paths like C:\\frontend are detected even on Linux.
Step 2: Client root validation
Before doing anything else, pyrpc checks that the resolved client_rootactually exists:
if new_client_root and not os.path.isdir(new_client_root):
console.print("[bold red]Error:[/bold red] Client project not found at:")
console.print(f" {new_client_root}")
console.print()
console.print("Create it first, then re-run [bold]pyrpc dev[/bold].")
console.print()
console.print(" [dim]Examples:[/dim]")
console.print(" npm create vite@latest frontend -- --template react-ts")
console.print(" npx create-next-app@latest frontend --typescript")
console.print(" npx create-react-app frontend --template typescript")
console.print()
raise typer.Exit(code=1)This prevents a common footgun: running pyrpc dev before setting up the frontend project. pyrpc will not create a client project for you — that’s the job of framework scaffolding tools (Vite, Next.js, etc.). Instead, it tells you exactly what’s missing and how to create it.
This follows the same pattern as Prisma, tRPC, and Better Auth: tools that install into existing projects, not tools that create projects.
Step 3: Migration check
If the client_root differs from a previous run, pyrpc handles the migration of existing type files. The logic has three cases:
- Old exists, new missing: Prompt to move the file
- Same SHA256: Auto-cleanup the old copy
- Different SHA256: Prompt: regenerate, keep both, or cancel
This is covered in detail in the migration strategy post.
Step 4: The watcher loop
With validation passed and types output path determined, pyrpc starts a file watcher (via the watchfiles library) that monitors all Python files in the project directory:
watched_dirs = _find_python_dirs(cwd)
def watcher_loop():
for changes in watch(*watched_dirs, ...):
if any(f.endswith(".py") for _, f in changes):
_schedule_regenerate()
def regenerate():
ok = default_router.reload_module(module)
schemas = get_registry_schema(default_router)
save_typescript_client(schemas, types_output)
console.print(f"Types regenerated ({len(schemas)} procs)")When a Python file changes: the router reloads the module, the schema is extracted, and save_typescript_client() writes the generated types to{client_root}/node_modules/@pyrpc/types/src/index.ts.
The TypeScript client picks up the change automatically — Vite’s HMR, Next.js’s Fast Refresh, or a plain tsc --watch all detect the file change and recompile.
Step 5: The dev server
If not running in --types-only mode, a Uvicorn server starts with hot-reload enabled, serving the pyRPC ASGI app. The developer console opens, providing commands like procs, inspect,generate, and types:
> pyrpc dev pyRPC dev server http://127.0.0.1:8000/rpc Types: C:/project/frontend/node_modules/@pyrpc/types/src/index.tstype help for commandspyrpc>
What about CI?
In CI, you typically don’t run pyrpc dev. Instead, you runpyrpc dev --types-only (or the upcoming pyrpc codegen) to generate types once. The server startup, watcher, and console are all skipped. This makes workspace mode CI-compatible without any special config.
The complete flow
pyrpc dev │ ├─ Read pyrpc.json ├─ Resolve client_root (config-relative) ├─ Validate client_root exists ├─ Handle type migration if path changed ├─ Start file watcher ├─ Generate initial types ├─ Start dev server (optional) └─ Open developer console On file change: watcher → reload module → extract schema → write types
That’s the workspace mode flow. It’s the default for a reason: one command, zero client-side config, end-to-end type safety within a single repository.

pyRPC