Skip to content

Fundable-Protocol/stellar_indexer

Repository files navigation

Fundable Indexer

Fundable Indexer is a Bun/Turborepo workspace that reads Soroban contract events, turns them into typed application data, and stores the result in PostgreSQL.

This repository is intentionally split into a small number of clear layers so a new contributor can follow the path from smart contract event to database row without guessing.

If you only remember one line, remember this:

Soroban contract event -> indexer -> Postgres -> API -> client

What This Repo Does

The indexer watches the Fundable Soroban contracts and persists:

  • stream identity records
  • deposit activity
  • withdrawal activity
  • lifecycle events for Flow, Lockup, and Stream NFT contracts
  • cursor state so polling can resume safely
  • indexed-event state so duplicate events are ignored safely

The current runtime is the streams package. The current read layer is the api package. distributions is reserved for later.

Plain-English Flow

Here is the full path in simple terms:

  1. A Soroban contract emits an event.
  2. The indexer asks Soroban RPC for that event.
  3. The indexer checks whether it already processed it.
  4. If it is new, a handler turns it into a database row.
  5. PostgreSQL stores the row.
  6. The API reads that stored row back.
  7. Your app or client asks the API for the data.

That is the whole system.

Project Structure

common

common is shared infrastructure. It should stay reusable and should not contain stream-specific business logic.

It currently owns:

  • environment config loading
  • PostgreSQL pool creation
  • cursor repository and storage
  • indexed-event repository and storage
  • shared Drizzle schema

If a piece of code is needed by more than one indexer domain, it belongs here.

streams

streams is the payment-stream indexer.

It currently owns:

  • event catalog and event shapes
  • Soroban RPC adapter
  • Stellar SDK RPC adapter
  • batch runner
  • runtime wiring
  • domain handlers
  • domain storage
  • executable entrypoint

If a piece of code only makes sense for the stream contracts, it belongs here.

api

api is the read layer.

It currently owns:

  • Apollo server setup
  • GraphQL schema and resolvers
  • HTTP startup for the API process

It reads rows from Postgres and returns them through GraphQL.

The API starts at v1.

The queries exposed under v1 are stream(id), streams, flowDeposits, flowWithdrawals, flowAdjustments, flowEvents, lockupEvents, and nftTransfers.

The stream read model now includes the canonical snapshot fields:

  • streamType
  • startTime
  • endTime
  • startTimeReadable
  • endTimeReadable
  • status
  • remainingAmount
  • snapshotTime
  • snapshotTimeReadable
  • payloadRaw
  • payloadReadable

distributions

distributions is reserved for the later distribution contract indexer. It stays separate so the stream code does not become a catch-all.

Important Directories

common/src

  • config/ - loads and validates environment variables.
  • cursor/ - persists indexer cursor state.
  • db/ - schema, pool, and migrations.
  • events/ - persists indexed-event identities.

streams/src

  • events/ - contract event catalog and event metadata.
  • indexer/ - RPC adapter, SDK client, runner, and shared indexer types.
  • app/ - composes config, DB, source, storage, and handlers.
  • runtime/ - boots the app from runtime dependencies.
  • main.ts - executable entrypoint.
  • handlers/ - event-to-storage mapping logic.
  • streamRecords/ - stream identity persistence.
  • deposits/ - Flow deposit persistence.
  • withdrawals/ - Flow withdrawal persistence.
  • adjustments/ - Flow adjustment persistence.
  • flowEvents/ - Flow lifecycle persistence.
  • lockupEvents/ - Lockup lifecycle persistence.
  • transfers/ - Stream NFT transfer persistence.
  • api/ - GraphQL/Apollo read layer.

CI

Pull requests run the repository checks in GitHub Actions:

  • bun run lint
  • bun run type-check
  • bun run test

Deployment details are intentionally kept out of this public README.

Current Implemented Data Paths

flow_created

This event creates the base stream record.

Flow:

  • contract emits flow_created
  • event source normalizes the event
  • createFlowCreatedHandler parses the payload
  • createPostgresStreamStorage inserts the stream row

Stored data:

  • stream id
  • sender
  • recipient
  • token
  • rate per second
  • snapshot time
  • canonical stream snapshot fields

The stream row is the shared read model for both Flow and Lockup creation now. It carries the current snapshot state alongside the raw and readable payload forms.

flow_deposit

This event records a deposit into an existing Flow stream.

Flow:

  • contract emits flow_deposit
  • event source normalizes the event
  • createFlowDepositHandler parses the payload
  • createPostgresFlowDepositStorage inserts the deposit row

Stored data:

  • stream id
  • funder
  • amount
  • ledger number
  • transaction hash
  • event index

flow_withdraw

This event records a withdrawal from a Flow stream.

Flow:

  • contract emits flow_withdraw
  • event source normalizes the event
  • createFlowWithdrawHandler parses the payload
  • createPostgresFlowWithdrawStorage inserts the withdrawal row

Stored data:

  • stream id
  • recipient address
  • caller address
  • amount
  • ledger number
  • transaction hash
  • event index

flow_adjusted

This event records a rate change on an existing Flow stream.

Flow:

  • contract emits flow_adjusted
  • event source normalizes the event
  • createFlowAdjustedHandler parses the payload
  • createPostgresFlowAdjustmentStorage inserts the adjustment row
  • the same event also continues to be written into flow_events for the lifecycle timeline

Stored data:

  • stream id
  • total debt
  • old rate
  • new rate
  • payload JSON
  • payload readable JSON
  • ledger number
  • transaction hash
  • event index

flow_adjusted, flow_paused, flow_restarted, flow_refunded, flow_voided

These are stored as Flow lifecycle events.

They use the same pattern:

  • event name
  • stream id
  • payload stored as structured JSON text
  • readable payload JSON
  • ledger / transaction / event identity

lockup_created, lockup_withdraw, lockup_canceled, lockup_renounced

These are stored as Lockup lifecycle events.

They follow the same model as Flow lifecycle events.

The lockup creation event also writes the canonical streams row, including:

  • streamType
  • startTime
  • endTime
  • startTimeReadable
  • endTimeReadable
  • status
  • remainingAmount
  • snapshotTime
  • snapshotTimeReadable
  • payloadRaw
  • payloadReadable

transfer

This is the Stream NFT transfer event.

It records ownership movement for the stream NFT that represents the stream recipient rights.

Canonical Stream Snapshot

The streams table is now the canonical snapshot for both Flow and Lockup creation events.

It stores:

  • the stream identity
  • the contract that created it
  • the sender, recipient, and token
  • the stream type
  • the current status
  • the current remaining amount
  • the chain timestamps as raw Unix values and readable ISO strings
  • the raw contract payload and a readable JSON payload

That makes the stream row fast to read for the frontend without losing the original event data.

How The API Layer Fits In

The api package does not invent new data. It reads what the indexer already saved in PostgreSQL.

Today it exposes:

  • v1.stream(id) - returns one stream row
  • v1.streams - returns all stream rows ordered by creation time
  • v1.flowDeposits(streamId: ...) - returns stream deposit history
  • v1.flowWithdrawals(streamId: ...) - returns stream withdrawal history
  • v1.flowAdjustments(streamId: ...) - returns Flow rate-change history
  • v1.flowEvents(streamId: ...) - returns Flow lifecycle history
  • v1.lockupEvents(streamId: ...) - returns Lockup lifecycle history
  • v1.nftTransfers(contractId: ..., tokenId: ...) - returns NFT transfer history

The Stream GraphQL type includes the readable time fields as well as the raw timestamps.

The pattern is always the same:

  1. Add a GraphQL field.
  2. Read the matching row or rows from Postgres.
  3. Convert the database row into the API shape.
  4. Cover it with a test.

New API work should go under v1.

How the Pieces Connect

1. Config

The runtime starts with common/src/config.

It reads environment values such as:

  • DATABASE_URL
  • SOROBAN_RPC_URL
  • STELLAR_NETWORK_PASSPHRASE
  • contract IDs for Flow, Lockup, and Stream NFT

2. Database

common/src/db creates the PostgreSQL pool and exposes the shared schema.

The shared schema includes:

  • indexer_cursors
  • indexed_events
  • streams
  • flow_adjustments
  • flow_deposits
  • flow_withdrawals
  • flow_events
  • lockup_events
  • nft_transfers

3. Event Source

streams/src/indexer/sorobanEventSource.ts converts the configured event catalog into Soroban RPC requests.

That file is the bridge between:

  • your contract event names
  • Soroban RPC response format
  • the internal StreamIndexerEvent type

4. Runner

streams/src/indexer/runner.ts is the polling loop.

It:

  • reads or creates the cursor
  • fetches the next batch of events
  • skips events already recorded in indexed_events
  • looks up the handler by event name
  • runs the handler
  • records the event as indexed
  • advances the cursor after success

5. Handlers

Handlers are small functions that translate one event into one domain write.

Examples:

  • flow_created -> streams
  • flow_deposit -> flow_deposits
  • flow_withdraw -> flow_withdrawals
  • lifecycle events -> flow_events or lockup_events
  • NFT transfer -> nft_transfers

6. Storage

Each domain has a Postgres storage adapter.

The pattern is the same everywhere:

  • findById(id) reads a row
  • insert(input) writes a row
  • if the insert conflicts, the storage reloads the existing row

That gives idempotent writes.

Why There Are So Many Small Files

The code is split this way on purpose.

The reason is simple:

  • easier to find code
  • easier to test one behavior at a time
  • less risk of turning common into a junk drawer
  • cleaner separation between infrastructure and business rules

If you are looking for something:

  • DB connection logic -> common/src/db
  • cursor behavior -> common/src/cursor
  • event dedupe -> common/src/events
  • event matching -> streams/src/events
  • event fetching -> streams/src/indexer
  • event-to-row translation -> streams/src/handlers
  • stream persistence -> streams/src/streamRecords
  • Flow deposit logic -> streams/src/deposits
  • Flow withdrawal logic -> streams/src/withdrawals
  • lifecycle event persistence -> streams/src/flowEvents or streams/src/lockupEvents
  • NFT transfer persistence -> streams/src/transfers
  • API read layer -> api/src

How To Add A New Event

Use the same pattern every time:

  1. Add the event name and topic shape to streams/src/events/catalog.ts.
  2. Add a failing test in the module-local tests/ folder.
  3. Add the storage types in types.ts.
  4. Add the Postgres storage adapter.
  5. Add the handler that maps the raw event to the storage input.
  6. Export the new pieces from the package entrypoint.
  7. Wire the handler into streams/src/handlers/index.ts.
  8. Update this README and docs/stream-indexer.md.

That sequence keeps the codebase consistent and easy to extend.

Working Rules In This Repo

  • Types belong in types.ts files.
  • Tests belong in local tests/ folders next to the code they cover.
  • Shared infrastructure belongs in common.
  • Domain-specific logic belongs in the package that owns it.
  • Use the existing patterns before inventing a new one.

Running The Project

Install dependencies:

bun install

Run checks:

bun run type-check
bun run test
bun run lint

Run migrations:

bun run db:generate
bun run db:migrate

Start the stream indexer:

bun run dev

Documentation

The detailed architecture guide lives in:

That document explains the implementation flow in more detail. This README is the quickest entry point.

About

Stellar Indexer Opensource

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages