Skip to main content
ymYash Mittal
All work
Web3TypeScriptSDKnpm

@ravenhouse/omni-sdk: designing an SDK other people build on

The TypeScript SDK that powers the Raven House ecosystem.

Role
Author + maintainer
Year
2024
Category
Web3
Stack
TypeScript, tsup, Aztec.js
@ravenhouse/omni-sdk screenshot

The problem#

Raven Bridge and the wider Raven House ecosystem all talk to the same contracts, the same Aztec node, the same L1 portals. Copy-pasting that wiring into every app is how you end up with three subtly different, all-slightly-wrong versions of the same bridge logic. The fix was to extract one SDK: a single, versioned, published source of truth for how you interact with the ecosystem, good enough that external developers could depend on it too.

My role#

Author and maintainer. I designed the public API, the module boundaries, the build pipeline, and the release process. It is on npm at @ravenhouse/omni-sdk, currently past 60 releases.

Architecture: nine doors, not one#

A framework-agnostic core, a React layer, contract artifacts, and infra plugins do not belong behind a single import. Bundling them would drag React and Aztec artifacts into a backend script that only wanted the Ethereum utils. So the SDK ships nine subpath exports, each independently importable:

package.json (exports map, abridged)
{
  "exports": {
    ".":              "./dist/index.js",          // core RavenBridge client
    "./contracts/l1": "./dist/contracts/l1/...",   // L1 contract bindings
    "./contracts/l2": "./dist/contracts/l2/...",   // L2 contract artifacts
    "./ethereum":     "./dist/ethereum/...",       // viem-based L1 utils
    "./verification": "./dist/verification/...",   // zkPassport IdentityVerifier
    "./react":        "./dist/react/...",          // React component library
    "./config":       "./dist/config/...",         // network + token config
    "./vite":         "./dist/vite.js",            // Vite plugin
    "./styles.css":   "./dist/styles.css"          // Tailwind styles
  }
}

Every entry is emitted as both ESM and CJS with its own .d.ts, built with tsup, so the same package works in a Next.js app, a Node backend, and a plain Vite project without anyone reaching into dist/.

The hardest decision#

The single most consequential API choice was making the core bridge client unopinionated: no API key, no deployment features, no hidden global state, just bridging. An earlier iteration coupled bridging to the ecosystem's auth and deployment tooling, which made the SDK impossible to adopt incrementally. Splitting the opinionated parts into their own modules (./auth, config, React) and leaving the core as a plain client is what made it something an outside developer could actually pick up.

omni-sdk/src/core/RavenBridge.ts
/**
 * Simplified, unopinionated bridge client for L1↔L2 token bridging.
 * No API key required, no deployment features, just bridging operations.
 */
export class RavenBridge {
  constructor(config: RavenBridgeConfig) { /* network only */ }
 
  bridgeL1ToL2(params: BridgeL1ToL2Params): Promise<BridgeResult> { /* … */ }
  bridgeL2ToL1(params: BridgeL2ToL1Params): Promise<BridgeResult> { /* … */ }
}

The types carry the documentation. BridgeL1ToL2Params is fully annotated: what isPrivate does, why secret is human-memorable, that onDepositComplete errors must not abort the bridge. When the type is the spec, consumers get the rules in their editor instead of in a wiki they will never read.

Keeping it honest#

The SDK depends on pinned Aztec 5.x packages and overrides viem to Aztec's fork, which is the kind of dependency pain that only shows up when you publish. Every release runs through tsc --noEmit, vitest, and a clean tsup build before it goes out. Sixty releases is mostly that discipline: small, verified, backwards-compatible increments.

Outcome#

The SDK powers Raven Bridge in production and is consumed by external developers integrating with the Aztec ecosystem. Live version and weekly downloads are on the npm page, and I wrote up the lessons in Publishing my first npm SDK.