Architecture

For architects.

In short

CryptoRail is one codebase delivered in two forms: a core library that holds the whole domain, and a thin service that hosts the core behind an HTTP API. Everything external (nodes, signers, storage, event delivery, secrets, policy, approval checks) sits behind a port the core defines, with reference adapters you can replace. The same code runs embedded in your application, as one service, as separate API, worker and indexer processes, or run for you. Moving between those shapes is configuration and wiring, not a rewrite.

This page explains the parts, how they are connected and what each shape changes. How to build with it is in the developer documentation.

The core and the service

The core is the domain: the contract registry, the transaction pipeline and its state machine, nonce management, gas, confirmation and finality, the event indexer, sponsored execution, the policy engine, the approval coordinator and the signing gate. It is a TypeScript library for Node.js and it decides everything that matters.

The service is a thin host around the core. It adds what a shared component needs and a library does not: authentication, tenants, roles, HTTP idempotency and an interface document. It decides nothing about transactions itself; it calls the core.

Embedded as a library in your Node.js application, or run as a service with an HTTP API described in OpenAPI and generated SDKs.

A submission is answered at once with the operation's id; progress is read through queries and events, and the API never waits for the chain.

flowchart LR
    subgraph A[Embedded]
        AppA[Your Node.js application] -->|imports| CoreA[core]
    end
    subgraph B[As a service]
        AppB[Your systems, any language] -->|HTTP or generated SDK| Svc[service]
        Svc --> CoreB[core]
    end
    CoreA --> N[(EVM networks)]
    CoreB --> N

Being asynchronous is a design decision, not a detail: an institutional signature or an approval can take hours or weeks, so no call holds a connection open waiting for the chain. Progress arrives through queries, through the state-change events, or through signed webhooks.

Ports and adapters

The core depends only on interfaces (ports). Each external concern is an adapter in its own package that implements one port, and the core never imports an adapter. That boundary is what lets a customer swap a signer, a store or a publisher without forking, and it is what keeps key material out of the core.

Every external concern is a documented port: replace a signer, a store or a publisher with your own, and prove it with the contract tests.

Only the development signer's package may touch key material, enforced by a dependency check on every change.

flowchart LR
    Core[core: the domain, no adapter imported]
    Core --> P1[[ChainProvider]] --> A1[JSON-RPC over HTTP, several endpoints with failover]
    Core --> P2[[BundlerProvider]] --> A2[ERC-4337 bundler JSON-RPC]
    Core --> P3[[Signer]] --> A3[PKCS#11 HSM, AWS KMS, external signer, development signer]
    Core --> P4[[Storage]] --> A4[PostgreSQL, or in memory for development]
    Core --> P5[[EventPublisher]] --> A5[signed webhooks through an outbox]
    Core --> P6[[SecretProvider]] --> A6[environment, file, HashiCorp Vault]
    Core --> P7[[PolicyEngine and ApprovalVerifier]] --> A7[declarative rules, approval proofs]
PortWhat it hidesReference adapters today
Chain providerWhere the node is, provider quirks, failoverStandard Ethereum JSON-RPC over HTTP, several endpoints per network
Bundler providerThe ERC-4337 bundler behind sponsored operationsStandard bundler JSON-RPC
SignerWhere the key is and how it signsPKCS#11, AWS KMS, external signer, development signer
StoragePersistence, locks and the outboxPostgreSQL; in memory for development and tests
Event publisherWhere events goSigned webhooks
Secret providerWhere secrets liveEnvironment, files, HashiCorp Vault
Policy engine, approval verifierHow decisions and approvals are madeDeclarative rules; signature, attestation and on-chain proofs

Works with your own nodes or a provider's, through standard Ethereum JSON-RPC over plain HTTP.

Signing with Azure Key Vault or GCP Cloud KMS, or through Fireblocks. On request

Events delivered to Kafka, NATS or AMQP brokers. On request

Telemetry is deliberately not a port: the core instruments itself with the OpenTelemetry API and the host chooses where the data goes.

Traces, metrics and structured logs through OpenTelemetry, into the monitoring tools you already run.

Four topologies

The same code supports four shapes. They say where the processes run; who operates them, and so what they may load, is the deployment profile, covered in the security model.

flowchart LR
    subgraph T1["1. Embedded library"]
        direction TB
        A1[Your application with the core] --> D1[(PostgreSQL)]
    end
    subgraph T2["2. Single service"]
        direction TB
        A2[Your systems] --> S2[service: API, worker and indexer in one process] --> D2[(PostgreSQL)]
    end
    subgraph T3["3. Split processes"]
        direction TB
        A3[Your systems] --> API3[API]
        API3 --> D3[(PostgreSQL)]
        W3[workers] --> D3
        I3[indexer] --> D3
    end
    subgraph T4["4. Run for you"]
        direction TB
        A4[Your systems] --> H4[CryptoRail operated for you]
        H4 -. bytes to sign .-> K4[your signer or your cloud KMS key]
    end
    T1 ~~~ T2 ~~~ T3 ~~~ T4
TopologyWhen it fitsWhat changes
EmbeddedA Node.js team that wants full control and no extra serviceYour application wires the adapters and calls the core directly
Single serviceTeams on any stack, modest volumeOne process with every role, one database
Split processesProduction at scale, separation of dutiesAPI, workers and indexer as separate processes, each scaled and permissioned on its own
Run for youA customer that wants no operations of its ownCryptoRail’s operator runs the service; signing stays on the customer’s side

One image and one configuration file: each process is started in a role (API, worker, indexer, or all of them at once) and runs only the work of that role.

One process, or separate API, worker, signing gate and indexer processes sharing only the database, each with a database role limited to what it needs.

Several workers on the same database without sending anything twice.

Run for you in production without ever holding a key of its own: signing stays on your side or in your own cloud account. Available for pilots

A pilot environment stood up in minutes with nothing installed on your side.

Storage and process roles

PostgreSQL is the one shared piece. It holds the operations and their history, the nonce sequences and their locks, the identities, the contract registry, the subscriptions and their cursors, and the outbox of events waiting to be delivered. Processes coordinate through it and through nothing else: an API accepts a submission, persists it and answers; a worker claims it later under a lease; an indexer writes outbox entries that a worker may deliver. No process calls another, and none needs to know whether the others are running.

sequenceDiagram
    participant Y as Your system
    participant A as API
    participant DB as PostgreSQL
    participant W as Worker
    participant G as Signing gate
    participant N as Node
    Y->>A: submit, with an idempotency key
    A->>DB: persist the operation as RECEIVED
    A-->>Y: 202 Accepted and the operation id
    W->>DB: claim it under a lease, prepare it, queue a signing request
    G->>DB: claim the request, check it, sign through your signer
    W->>N: broadcast
    W->>DB: persist each change of state
    W->>Y: signed webhook with the change

Each role over PostgreSQL connects with its own database role, granted only what that role needs.

Each process has its own database role: the API cannot allocate a nonce, the worker cannot read API keys, the indexer cannot touch transactions, identities or signing requests, and no process can change the schema.

Runs in your infrastructure, as a container image over your PostgreSQL, with schema migrations applied by an explicit command.

An operation CryptoRail has acknowledged is never lost: not across crashes and restarts, not through a node outage of up to ten minutes.

A stopping process finishes its broadcasts and deliveries in flight within a grace period, and whatever it could not finish is picked up again on restart.

Schema migrations never run when a process starts: a process whose database is behind refuses to start and names the migrations to apply, so a rolling deployment cannot turn into a race between two builds.

What talks to what

ProcessAccepts connections fromReaches out toHolds a signer
APIYour systems, over HTTPPostgreSQL; the nodes, for readiness and read-only checksNo
WorkerNobody: it opens no portPostgreSQL, the nodes, the bundlers, your webhook receiversNo: it queues signing requests
Signing gateNobody: it opens no portPostgreSQL and your signer, nothing elseYes, the only one
IndexerNobody: it opens no portPostgreSQL, the nodes, your webhook receiversNo
flowchart LR
    Y[Your systems] -->|HTTP| API
    API --> DB[(PostgreSQL)]
    W[Worker] --> DB
    IX[Indexer] --> DB
    W --> N[Nodes and bundlers]
    IX --> N
    API -. readiness, reads .-> N
    G[Signing gate] --> DB
    G --> S[Your HSM, KMS or signer]
    W -->|signed webhooks| R[Your receivers]
    IX -->|signed webhooks| R

The API process builds no signer, and a split deployment that configures one for it is refused at startup.

In a split deployment the signing gate is a process of its own, the only one with signer credentials: it reaches the database and your signer and nothing else, and re-checks every request before it signs.

Several node endpoints per network: calls go to the first healthy one, an operation stays on the endpoint it started on, and a dead or lagging node is left aside without losing anything.

Several networks at once, each declared in configuration, with every record carrying the network it belongs to.

What it does not do

  • It is not a set of microservices. It is a modular monolith whose roles can be split into processes; the processes never call each other, and nothing coordinates them but the database.
  • It does not run on a database other than PostgreSQL in production. The in-memory store is for development and tests, and a split deployment on it is refused at startup.
  • It does not run several indexers side by side. A deployment with subscriptions runs exactly one indexer process (or one process with every role); workers scale, the indexer does not.
  • The embedded form is for Node.js only. Other stacks use the service, through HTTP or the generated SDK.
  • Authentication with mutual TLS or OIDC tokens, beside API keys. On request
  • Blockchains outside the EVM family. Not supported

Evidence

Every claim above links to its evidence on the evidence page. The main ones:

SubjectRequirementsJourneys
Library or service, asynchronous APINFR-INT-01, NFR-INT-03, FR-API-01, FR-API-02, FR-API-07, FR-TX-02J23
Ports, contract tests, key boundaryNFR-INT-01, NFR-INT-02, FR-SIG-15, NFR-SEC-01J12
Roles, split processes, several workersFR-OPS-04, FR-OPS-05, FR-OPS-06, NFR-SEC-06J27, J29, J38
Never lost, graceful shutdownNFR-REL-01, NFR-REL-05, FR-OPS-08J15
Failover between endpointsFR-CHN-05, FR-CHN-15J39
Run for you, pilot sandboxNFR-SEC-07, NFR-SEC-10, ADR-0005J42, J43