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]
| Port | What it hides | Reference adapters today |
|---|---|---|
| Chain provider | Where the node is, provider quirks, failover | Standard Ethereum JSON-RPC over HTTP, several endpoints per network |
| Bundler provider | The ERC-4337 bundler behind sponsored operations | Standard bundler JSON-RPC |
| Signer | Where the key is and how it signs | PKCS#11, AWS KMS, external signer, development signer |
| Storage | Persistence, locks and the outbox | PostgreSQL; in memory for development and tests |
| Event publisher | Where events go | Signed webhooks |
| Secret provider | Where secrets live | Environment, files, HashiCorp Vault |
| Policy engine, approval verifier | How decisions and approvals are made | Declarative 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
| Topology | When it fits | What changes |
|---|---|---|
| Embedded | A Node.js team that wants full control and no extra service | Your application wires the adapters and calls the core directly |
| Single service | Teams on any stack, modest volume | One process with every role, one database |
| Split processes | Production at scale, separation of duties | API, workers and indexer as separate processes, each scaled and permissioned on its own |
| Run for you | A customer that wants no operations of its own | CryptoRail’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
| Process | Accepts connections from | Reaches out to | Holds a signer |
|---|---|---|---|
| API | Your systems, over HTTP | PostgreSQL; the nodes, for readiness and read-only checks | No |
| Worker | Nobody: it opens no port | PostgreSQL, the nodes, the bundlers, your webhook receivers | No: it queues signing requests |
| Signing gate | Nobody: it opens no port | PostgreSQL and your signer, nothing else | Yes, the only one |
| Indexer | Nobody: it opens no port | PostgreSQL, the nodes, your webhook receivers | No |
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:
| Subject | Requirements | Journeys |
|---|---|---|
| Library or service, asynchronous API | NFR-INT-01, NFR-INT-03, FR-API-01, FR-API-02, FR-API-07, FR-TX-02 | J23 |
| Ports, contract tests, key boundary | NFR-INT-01, NFR-INT-02, FR-SIG-15, NFR-SEC-01 | J12 |
| Roles, split processes, several workers | FR-OPS-04, FR-OPS-05, FR-OPS-06, NFR-SEC-06 | J27, J29, J38 |
| Never lost, graceful shutdown | NFR-REL-01, NFR-REL-05, FR-OPS-08 | J15 |
| Failover between endpoints | FR-CHN-05, FR-CHN-15 | J39 |
| Run for you, pilot sandbox | NFR-SEC-07, NFR-SEC-10, ADR-0005 | J42, J43 |