Skip to content

Indexer & API ​

The Registry's backend is one Fastify process that runs in two modes, API server and indexer, chosen by the INDEXER environment variable. They share the same SQLite database.

Source: apps/api/ in freecodexyz/registry.

The indexer ​

The indexer is a polling loop. On each tick (every ~12 seconds) it:

  1. Reads last_block from the indexer_state table.
  2. Asks the RPC for the current head block.
  3. Calls getLogs against the RIK contract for the RepoRegistered event over (last_block + 1, head].
  4. For each event, inserts a row into the repos table.
  5. Best-effort enrichment for each row: fetch the GitHub repo metadata (full name, description, language, stars, URL) and the owner's GitHub username via the GitHub REST API. Upsert into the github_meta table.
  6. Emits a repo event on the in-process registryEvents emitter; the API server's SSE endpoint relays it to connected clients.
  7. Writes the new last_block back to indexer_state.

Tunables and behavior:

SettingDefaultSource
Poll interval12 secondshardcoded
Initial backfill rangelast 50,000 blockshardcoded DEFAULT_LIST_BLOCK_RANGE
ChainBase Sepoliahardcoded; matches the deployed contract

Caches ​

Two in-process caches sit above SQLite to absorb load:

  • Repo cache. Per-repoId, holds the enriched GitHub metadata and the owner's username. TTL: 5 minutes.
  • Event cache. Holds the most recent event list. TTL: 10 seconds.

SQLite remains the durable layer; the caches just keep restarts from immediately fanning out to the RPC and GitHub API again.

Endpoints (current shape) ​

The exact route table is still evolving as the app is being built; treat this as the design intent rather than a contract.

RoutePurpose
GET /reposPaginated list of registered repositories with metadata. Supports `sort=registered_at_desc
GET /repos/:repoIdA single registration, with the full enriched payload.
GET /eventsSSE stream of new registrations as the indexer picks them up.
POST /auth/nonceIssue a SIWE nonce for a given address.
POST /auth/verifyVerify a SIWE message and start a session.
POST /auth/logoutEnd the session.
GET /gateCheck whether the current session holds the minimum gate-token balance.

The exact response shapes are defined inline in apps/api/src/index.ts. The same source defines the SSE payload shape (RepoStreamPayload).

Storage ​

SQLite (better-sqlite3). Tables:

TablePurpose
reposOne row per RepoRegistered event. Holds repo_id, registrant, github_owner_id, registered_at, block_number, transaction_hash, chain_id.
github_metaOne row per repo_id. Holds enriched metadata: full_name, description, language, stars, html_url, owner_name, cached_at.
indexer_stateKey/value pairs for indexer bookkeeping (last_block).

Environment ​

VariableRequiredDescription
CONTRACT_ADDRESSyesThe RIK contract address to index.
RPC_URLnoRPC endpoint. Defaults to https://base-sepolia-rpc.publicnode.com.
CHAIN_IDnoSQLite default chain ID. Defaults to 84532.
INDEXERnoSet to 1 or true to run the indexer loop alongside the server.
GITHUB_TOKENyesUsed for GitHub REST API enrichment.
SIWE_DOMAINnoThe domain used in SIWE messages. Defaults to localhost:5173.
SESSION_KEYno32-byte key used to sign session cookies. Generated per-process if missing.
SESSION_COOKIE_SECUREnoSet to false to allow non-HTTPS cookies (dev).
GATE_TOKEN_ADDRESSyesERC-20 used for the access gate. See Access Gating.
GATE_TOKEN_MIN_BALANCEnoMinimum balance required to pass the gate. Defaults to 1.

Running locally ​

bash
CONTRACT_ADDRESS="0xc03a52cD0EB2d5d456e64bda0557Db04608d1eac" RPC_URL="https://base-sepolia-rpc.publicnode.com" CHAIN_ID=84532 pnpm dev

This runs both the API and the indexer (and the web app). To run just the API:

bash
pnpm --filter @freecodexyz/api dev:api

And the indexer in a separate process:

bash
pnpm --filter @freecodexyz/api dev:indexer

Both share the SQLite file under apps/api/data/.

Continue ​

Released under the Apache 2.0 License.