- Status: Accepted
- Date: 2026-07-24
packages/* currently holds seven packages — api, app, contracts, mobile,
monitoring, sdk, types — managed as a single pnpm workspace (pnpm-workspace.yaml)
plus an independent Cargo workspace for contracts. The rationale for splitting along
these lines, and what's allowed to depend on what, has never been written down. That
makes two things slower than they should be:
- Onboarding: a new contributor has to reverse-engineer from
package.jsonfiles andimportstatements which packages are meant to talk to each other and how. - Dependency decisions: "should this new helper live in
sdkor just inapi?", "canmobileimport fromapp?" — these get decided ad hoc per-PR without a reference to check against.
This ADR documents the intended boundaries and the rules for crossing them. It also records, honestly, where the current codebase already deviates from those rules, so the gap is visible instead of assumed away.
| Package | Language | Role |
|---|---|---|
types |
TypeScript | Shared DTOs and API response types. No behavior, no I/O. The one package everything else may depend on freely. |
sdk |
TypeScript | Single source of truth for talking to Stellar — Horizon REST calls and Soroban contract invocations. Exists so contract-calling logic (URLs, request shapes, error handling) isn't reimplemented per consumer. |
contracts |
Rust (Soroban/WASM) | The on-chain protocol itself: registry, market, dispute, fee_distribution, insurance_pool. Not part of the TypeScript dependency graph — see Contracts is a boundary, not a dependency below. |
api |
TypeScript (Express) | The backend of record: auth, business logic, PostgreSQL persistence via Prisma. The only package with direct database access. |
app |
TypeScript (Next.js) | Web frontend. Talks to api over HTTP and to the user's wallet (Freighter) directly for signing. |
mobile |
TypeScript (Expo/React Native) | Mobile frontend. Same relationship to api as app — a second, independent HTTP client, not a dependent of app. |
monitoring |
TypeScript (Node) | Standalone observer: watches contract events, tracks balances, fires alerts. Reads from Stellar; writes to nothing else in this repo. |
The split follows runtime boundaries, not code-reuse convenience: api/app/
mobile/monitoring are four independently deployed processes, so none of them may
import from another's src/ — only types and sdk are meant to be shared library
code. contracts is its own boundary because it ships as WASM to a different runtime
(the Stellar network) entirely, not as a Node/npm artifact.
types ← anything (leaf; zero workspace dependencies of its own)
sdk ← api, app, mobile, monitoring (sdk itself may depend only on types)
api ← nothing (api is never imported by another package)
app ← nothing
mobile ← nothing
monitoring ← nothing
Concretely:
typeshas zero workspace dependencies. It's TypeScript interfaces/DTOs only — no imports ofsdk, no runtime logic. Anything may depend on it.sdkmay depend only ontypes. It must not import fromapi,app,mobile,contracts, ormonitoring— it's consumed by all of them and can't take on a dependency in the other direction without creating a cycle.api,app,mobile,monitoringmay depend ontypesandsdk, and on nothing else inpackages/*. In particular:appandmobilemust not import from each other or fromapi'ssrc/— they are separate deployables that talk toapiexclusively over HTTP.apimust not import fromappormobile.- No package should reach into another package's
src/via a relative path (../../other-package/src/...) to work around this — if something needs to be shared, it belongs intypesorsdk, not a deep import.
- No package imports Rust source from
contracts, andcontractsimports nothing from the TypeScript workspace — see below. - No circular workspace dependencies.
sdk'stypes-only rule (point 2) is what keeps the graph a DAG; don't introduce an edge that violates it even transitively.
contracts is a separate Cargo workspace, not a pnpm package — it isn't in
pnpm-workspace.yaml and nothing in packages/* has a file:/workspace: dependency
on it. The only interfaces are:
- Build-time:
contractscompiles to WASM (make buildinpackages/contracts) and is deployed independently via the Stellar CLI (see packages/contracts/README.md). - Run-time: consumers hold a deployed contract ID (an env var —
REGISTRY_CONTRACT_ID,MARKET_CONTRACT_ID, etc.) and invoke it over Soroban RPC, ideally throughsdk.
This means contracts has no compile-time relationship to the rest of the repo at all.
Its public interface is documented separately in docs/CONTRACTS.md;
upgrade and migration mechanics are covered in
packages/contracts/UPGRADE_GUIDE.md, not by
this ADR.
graph TD
types["types<br/>(shared DTOs)"]
sdk["sdk<br/>(Stellar/Soroban client)"]
api["api<br/>(Express + PostgreSQL)"]
app["app<br/>(Next.js)"]
mobile["mobile<br/>(Expo)"]
monitoring["monitoring<br/>(alerting)"]
contracts(["contracts<br/>(Rust → WASM, separate Cargo workspace)"])
sdk --> types
api --> types
api --> sdk
app --> types
app --> sdk
mobile --> sdk
monitoring --> sdk
app -. HTTP .-> api
mobile -. HTTP .-> api
sdk -. Soroban RPC + Horizon .-> contracts
monitoring -. Soroban RPC .-> contracts
classDef leaf fill:#e8f4ea,stroke:#2f7a3d;
classDef rust fill:#f4ece8,stroke:#a5522c;
class types leaf;
class contracts rust;
Solid arrows are compile-time (workspace:*) dependencies. Dashed arrows are runtime
calls (HTTP or Soroban RPC) — not a package dependency in the pnpm/Cargo sense, and not
something import-boundary rules apply to.
Written down so the gap is a known, trackable thing rather than a surprise the next time someone reads the source:
apidoes not currently importsdk.api's wallet/Horizon code (packages/api/src/controllers/wallet.ts) reimplements the same Horizon REST callssdk'sHorizonClientalready provides, rather than depending onsdk. Per this ADR, that duplication should be resolved by havingapidepend onsdk.appandmobilecall@stellar/stellar-sdkdirectly rather than going throughsdk.sdkwas written to be their shared client (see its own description: "Single source of truth for Stellar / contract interaction — consumed by API and App") but isn't wired up yet in either.apideclares aworkspace:*dependency ontypes(devDependenciesinpackages/api/package.json) but nothing inpackages/api/srccurrently imports it. Not a rule violation, just currently unused.
None of these are blocking — they're pre-existing state, not something this ADR
introduces or requires fixing immediately. They're recorded here so a future PR that
wires api/app/mobile up to sdk is understood as closing a known gap, not as a
new architectural direction.
Positive:
- A new contributor can answer "where does this code belong?" by checking this table instead of asking in chat or guessing from precedent.
api,app, andmobilestaying import-independent of each other means any one of them can be redeployed, rewritten, or replaced without a source-level ripple effect into the others — the only contract between them isapi's HTTP surface (seepackages/api/README.mdfor its docs, andpackages/api/openapi.jsonfor the generated spec).- Centralizing Stellar interaction in
sdk(onceapi/app/mobileactually depend on it — see deviations above) means a Horizon/Soroban RPC shape change or a bug fix in retry/error handling is a one-package change, not three.
Trade-offs:
sdkbeingtypes-only forces any Stellar-interaction helper that needs request-specific business logic to either live in the calling package or accept a more generic shape insdk— occasional friction, but it's what keeps the graph acyclic.contractsbeing fully outside the TypeScript graph means there's no compiler-enforced link between a contract's function signature and the code that calls it — that safety net ispackages/contracts/UPGRADE_GUIDE.mdplus integration tests, not the type system.
- docs/CONTRACTS.md — contract interface reference
- packages/contracts/README.md — contracts package overview, build/deploy/upgrade
- packages/contracts/UPGRADE_GUIDE.md — contract upgrade runbook
- packages/sdk/package.json — SDK package description and exports
- packages/api/README.md — API docs and endpoint reference