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.

#rust #architecture #cloud #saas #engineering
Dmytro Yemelianov - Author
Dmytro Yemelianov
Autodesk Expert Elite β€’ APS Developer

One Kernel, Zero Rewrites

RAPS started as a CLI tool. A way to talk to Autodesk Platform Services from the terminal without drowning in boilerplate OAuth flows and undocumented API quirks.

Then it became an MCP 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 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.

Layer 3 β€” Applications
raps-cli
  • 230+ commands
  • 114 MCP tools
  • TUI dashboard (7 tabs, 33 views)
  • Plugin system
  • Skill system
raps-cloud
  • Axum API gateway
  • PostgreSQL + RLS
  • AES-256-GCM encryption
  • JWT + Argon2 auth
  • WebSocket job progress
  • Background job runner
python-bindings
  • PyO3 extension
  • Data science target
  • Subset: kernel, oss, dm, derivative

depends on

Layer 2 β€” API Crates
raps-oss
Object Storage
raps-dm
Data Management
raps-derivative
Model Derivative
raps-da
Design Automation
raps-acc
ACC / BIM 360
raps-webhooks
Event Subscriptions
raps-reality
Photogrammetry
raps-admin
Bulk Operations

depends on

Layer 1 β€” Core
raps-kernel
Authentication (2-leg, 3-leg, PKCE, device code)
HTTP client with retry, rate limiting, circuit breaker
Token management (keyring + file storage)
Configuration (env vars, .env, profiles)
Metrics (Prometheus)
Health endpoints

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:

ChannelWhat ShipsTarget Audience
GitHub ReleasesBinary archives (5 platforms)Direct download
crates.io10 Rust cratesRust ecosystem
npm6 packages (@dmytro-yemelianov/raps-cli-*)Node.js ecosystem
PyPI2 packages (raps, raps-bindings)Python ecosystem
HomebrewTap formulamacOS users
ScoopBucket manifestWindows users
GHCR6 Docker imagesContainer deployments
HelmChart + valuesKubernetes
Cloudflare Workers4 WorkersEdge computing
Cloudflare Pages2 frontends (marketplace)Web
GitHub Pages2 Astro sites (docs, marketing)Web

Fourteen CI/CD 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

12
Rust crates in workspace
579
Commits in 2 months
14
CI/CD workflows
11
Components consume raps-kernel
230+
CLI commands
0
Auth logic duplication

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, 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 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