From CLI to SaaS: How a Rust Monorepo Scales to Cloud
One Rust kernel. Three application targets. 12 crates. How RAPS scaled from CLI tool to SaaS platform without rewriting anything.
One Kernel, Zero Rewrites
RAPSπΌRAPSRust CLI for Autodesk Platform Services.View in glossary started as a CLIπ»CLIText-based interface for running commands.View in glossary tool. A way to talk to Autodesk Platform ServicesβοΈAPSAutodesk Platform Services - cloud APIs for CAD/BIM automation.View in glossary from the terminalπ₯οΈTerminalApplication for running command-line programs.View in glossary without drowning in boilerplate OAuthπOAuthIndustry-standard authorization protocol used by APS.View in glossary flows and undocumented APIπAPIInterface for software components to communicate.View in glossary quirks.
Then it became an MCPπ§ MCPProtocol for AI assistant tool integration.View in glossary server β 114 tools that let AI assistants operate APS directly.
Then a SaaS cloud platform β multi-tenant, PostgreSQL-backed, with encrypted credential storage and WebSocket-driven job progress.
Then a marketplace β Cloudflare Workers, D1, R2, Stripe integration.
Each new surface reused the same core. No forks. No rewrites. No βv2 from scratch.β This is how a Rustπ¦RustSystems programming language known for safety.View in glossary monorepo scales.
The Architecture
The system has three layers. Every dependency arrow points downward. Nothing in a lower layer knows about what sits above it.
- 230+ commands
- 114 MCP tools
- TUI dashboard (7 tabs, 33 views)
- Plugin system
- Skill system
- Axum API gateway
- PostgreSQL + RLS
- AES-256-GCM encryption
- JWTπ«JWTCompact token format for authentication.View in glossary + Argon2 auth
- WebSocket job progress
- Background job runner
- PyO3 extension
- Data science target
- Subset: kernel, ossπ¦OSSAPS cloud storage for files and models.View in glossary, dmπData ManagementAPS service for accessing files in ACC, BIM 360, and Fusion.View in glossary, derivativeπ€DerivativeAny output generated from model translation.View in glossary
depends on
depends on
The rule is absolute: dependency arrows point down, never sideways, never up. raps-kernel has zero internal dependencies. Every API crate depends only on kernel. No API crate depends on another API crate (except raps-admin, which depends on raps-acc for bulk account operations). Applications consume all crates and add no API logic of their own.
Three Decisions That Made This Work
Decision 1: The kernel depends on nothing internal
raps-kernel owns authentication, HTTP transport, configuration, and observability. It has zero internal crate dependencies. Every API crate imports kernel and nothing else.
The consequence: adding a new API crate is an isolated operation. You create a new directory, declare raps-kernel as a dependency, implement your typed client, and register it in the workspace. You cannot accidentally break Object Storage by adding a Reality Capture crate. The dependency graph makes it structurally impossible.
Decision 2: API crates are thin typed clients
Each API crate is a thin layer over the kernelβs HTTP client. They add three things:
- Domain types β Rust structs for API request and response bodies
- Endpoint definitions β URL paths, HTTP methods, query parameters
- Response parsing β deserialization and error mapping
They do not add auth logic. They do not add retry logic. They do not add rate limiting. All of that lives in the kernel. When Autodesk changes a rate limit, the fix lands in one place and every API crate benefits immediately.
The result: consistent behavior across all 16 APIs. The same exponential backoff, the same circuit breaker thresholds, the same token refresh flow β regardless of whether you are uploading a 5GB file to Object Storage or creating an issue in ACC.
Decision 3: Applications compose, they do not extend
raps-cli imports all API crates and maps them to CLI commands. raps-cloud imports all API crates and maps them to HTTP routes and background job types. python-bindings imports a subset and exposes them via PyO3.
No application contains API-specific logic. If a feature works in the CLI, it works in the cloud gateway. If it works in the cloud gateway, it works from Python. The application layer is purely about surface: how you invoke the operation, how you present the result, how you handle the lifecycle.
Workspace Structure
raps/
βββ Cargo.toml (workspace: 12 members + shared deps)
βββ raps-kernel/ (auth, HTTP, config -- 0 internal deps)
βββ raps-oss/ (Object Storage -- depends: kernel)
βββ raps-dm/ (Data Management -- depends: kernel)
βββ raps-derivative/ (Model Derivative -- depends: kernel)
βββ raps-da/ (Design Automation -- depends: kernel)
βββ raps-acc/ (ACC/BIM 360 -- depends: kernel)
βββ raps-webhooks/ (Webhooks -- depends: kernel)
βββ raps-reality/ (Reality Capture -- depends: kernel)
βββ raps-admin/ (Bulk Operations -- depends: kernel, acc)
βββ raps-cli/ (CLI + MCP + TUI -- depends: all)
βββ raps-cloud/ (SaaS Gateway -- depends: all)
βββ python-bindings/ (PyO3 -- depends: kernel, oss, dm, derivative)
βββ deploy/
β βββ docker/ (6 Docker images)
β βββ helm/ (Kubernetes Helm chart)
βββ workers/
β βββ device-auth/ (Cloudflare Worker -- OAuth device flow)
β βββ rapscli-api/ (Cloudflare Worker -- API gateway)
β βββ webhook-gateway/ (Cloudflare Worker -- event relay)
βββ npm/ (6 npm packages for binary distribution)
A single cargo build --workspace compiles every Rust crate. A single cargo test --workspace runs every test. The workspace Cargo.toml pins shared dependency versions so all crates use the same version of tokio, serde, reqwest, and everything else. No version drift. No βworks in this crate but not that one.β
The Distribution Matrix
One repository produces artifacts for eleven distribution channels:
| Channel | What Ships | Target Audience |
|---|---|---|
| GitHub Releases | Binary archives (5 platforms) | Direct download |
| crates.io | 10 Rust crates | Rust ecosystem |
| npm | 6 packages (@dmytro-yemelianov/raps-cli-*) | Node.js ecosystem |
| PyPI | 2 packages (raps, raps-bindings) | Python ecosystem |
| Homebrew | Tap formula | macOS users |
| Scoop | Bucketπͺ£BucketContainer for storing objects in OSS.View in glossary manifestπManifestMetadata about a translated model and its derivatives.View in glossary | Windows users |
| GHCR | 6 Dockerπ³DockerContainer platform for consistent environments.View in glossary images | Containerπ¦ContainerIsolated, portable application environment.View in glossary deployments |
| Helm | Chart + values | Kubernetes |
| Cloudflare Workers | 4 Workers | Edge computing |
| Cloudflare Pages | 2 frontends (marketplace) | Web |
| GitHub Pages | 2 Astro sites (docs, marketing) | Web |
Fourteen CI/CDπCI/CDAutomated build, test, and deployment pipelines.View in glossary workflows orchestrate all of this. A version bump in Cargo.toml triggers a cascade: cross-compile for five architectures, publish crates, build and push Docker images, update Homebrew formula, update Scoop manifest, publish npm packages, build Python wheels, deploy Workers, deploy Pages, deploy Helm chart. One commit. Eleven channels. Fully automated.
The Numbers
The last number is the one that matters most. The same authentication logic β 2-legged, 3-legged, PKCE, device code β runs in the CLI, the cloud gateway, the Python bindings, and the MCP server. It is defined once in raps-kernel and consumed everywhere. When Autodesk changes their OAuth behavior (and they do), the fix is one patch in one crate.
What Rust Gives You Here
Other languages can do monorepos. Rust makes certain properties structural rather than conventional:
Workspace system. Cargo workspaces are real multi-crate dependency management, not a monorepo tool bolted onto a package manager. Dependencies are resolved once across the entire workspace. There is no equivalent of node_modules duplication or Python virtualenv conflicts between packages.
Type safety across crate boundaries. API response types are defined in the API crate and used directly by applications. There is no serialization boundary between crates. If raps-acc changes the shape of an Issue struct, every consumer gets a compile error β not a runtime crash in production.
Single binary output. raps-cli compiles to one file. No runtime. No node_modules. No virtualenv. No DLLs. Copy it to a server, run it. This matters enormously for CLI distribution and Docker image size.
Shared async runtime. The same tokio runtime powers both the CLI and the cloud gateway. Code moves freely between targets. A function that downloads a file in the CLI context works identically when called from an Axum handler in the cloud context.
Feature flags. --features kubernetes compiles in health endpoints and Prometheus metrics. Without the flagπ©FlagOptional parameter modifying command behavior.View in glossary, that code does not exist in the binary. Feature flags let a single crate serve multiple deployment contexts without runtime overhead.
Compile-time dependency validation. If the crate graph has a cycle, it does not compile. If a crate references a type that does not exist, it does not compile. The guarantees are not βwe have a linter that warns you.β They are βthe build fails.β
The Decoupled Edge
Not everything is Rust, and that is deliberate.
The Cloudflare Workers β device-auth, rapscli-api, webhook-gateway, and the marketplace API β are TypeScript running on Hono. They communicate with the Rust backend via HTTP. They have no Rust dependency. They deploy independently, on a separate cadence, to edge locations worldwide.
The marketplace frontend is a React admin panel and an Astro storefront. They talk to the marketplace API Worker, which talks to D1 and R2 and Stripe. None of them know Rust exists.
This decoupling is intentional. Edge infrastructure and commerce logic evolve on different timelines than core platform capabilities. The device-auth Worker might get updated three times a day during an OAuth flow redesign while the Rust kernel stays untouched. The marketplace can add a new payment method without recompiling anything.
The boundary is HTTP. The contract is API schemas. The deployment is independent. This is where a monorepo ends and a distributed system begins.
Lessons for Other Projects
If you are building a developer tool and wondering whether to invest in architecture upfront, here is what we learned:
Start with the kernel. Do not bolt on shared logic after you have three applications duplicating auth code. Extract the kernel first. Make it depend on nothing internal. Every decision downstream becomes simpler.
Keep API crates thin. They are typed HTTP clients, not business logic containers. The moment an API crate starts making decisions β βshould I retry? how long should I wait? is this token expired?β β you have put logic in the wrong place. That belongs in the kernel.
Applications compose, they do not extend. If you find yourself writing if target == "cli" inside an API crate, something has gone wrong. The API crate should not know what is consuming it. Applications import crates and map them to their surface (commands, routes, Python functions). That is all.
Distribution is a feature. Invest in it early. If your tool is only available via cargo install, you have excluded 95% of your potential users. npm, PyPI, Homebrew, Scoop, Docker β each channel opens a new audience. The CI/CD investment pays for itself in adoption.
Decouple what can be decoupled. Edge infrastructure in TypeScript, core platform in Rust, marketplace in its own stack. Not everything needs to be in the same language or the same deployment unit. The question is: do these things change at the same rate, for the same reasons? If not, decouple them.
Architecture Details
RAPS is open source. The monorepo structure, the crate boundaries, the CI/CD workflows β all of it is available for inspection.
The advocacy layer pattern works. One kernel. Thin API crates. Composing applications. And the monorepo scales with it β from a weekend CLI projectπProjectContainer for folders and files within a hub.View in glossary to a multi-tenant SaaS platform, without rewriting a single line of core logic.
Explore the documentation for architecture details, or install RAPS and see the system in action:
# npm
npm install -g @anthropic-ai/raps-cli
# Homebrew
brew install dmytro-yemelianov/tap/raps
# Cargo
cargo install raps-cli
# Or download directly from GitHub Releases