Skip to content
DocsDevelopersIndexer & data layer

The Indexer

Reading DAO state directly from the chain is slow and awkward — voting power lives in timestamped checkpoints, proposals span multiple events, and member balances derive from transfers. The DAO Ships indexer solves this by consuming on-chain events and writing structured rows into a Supabase (PostgreSQL) database your frontend can query directly.

The indexer is event-driven: every row derives from on-chain events, with zero RPC calls in its handlers. Counter operations are idempotent (safe to retry), and it indexes 24 distinct event types across the launcher, governance, tokens, navigators, and Poster.

Schema per network

Each network is an isolated PostgreSQL schema — testnet today, mainnet at launch — and every table is prefixed ds_. Point your client at the schema for the network you're reading (see the query examples below).

The ds_* tables

TablePurpose
ds_daosDAO records — governance params, token totals, profile
ds_membersMember share/loot balances, delegation, vote counts
ds_proposalsProposal lifecycle (submit, sponsor, vote, process, cancel)
ds_votesIndividual vote records
ds_navigatorsRegistered navigators with permissions and type
ds_navigator_eventsOnboard events emitted by navigators
ds_ragequitsMember exit records with per-token amounts
ds_recordsPoster metadata (profiles, rationale, announcements)
ds_guild_tokensRegistered ragequit tokens
ds_delegationsDelegation change history
ds_event_transactions / ds_processed_logsDedup tracking for retry idempotency
ds_indexer_stateLast processed block and sync state

NUMERIC(78,0) maps to strings

Token amounts and voting power are uint256 on-chain. In Postgres they are stored as NUMERIC(78, 0) — large enough to hold any uint256. Over PostgREST these come back as strings, not JavaScript numbers, to avoid precision loss. Always parse them into BigInt:

const totalShares = BigInt(dao.total_shares); // dao.total_shares is a string

Smaller integer columns (voting_period, proposal_count, yes_votes, block_number) are BIGINT and are safe as numbers within typical ranges.

Querying with supabase-js

Point the client at the right schema for the network you want:

import { createClient } from "@supabase/supabase-js";
 
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
  db: { schema: "testnet" }, // or "mainnet"
});
 
// List DAOs, newest first
const { data: daos } = await supabase
  .from("ds_daos")
  .select("id, name, total_shares, total_loot, proposal_count, voting_period")
  .order("updated_at", { ascending: false })
  .limit(20);
 
// Active proposals for one DAO
const { data: proposals } = await supabase
  .from("ds_proposals")
  .select("proposal_id, details, sponsored, processed, yes_balance, no_balance, voting_ends")
  .eq("dao_id", daoAddress)
  .order("proposal_id", { ascending: false });
 
// A member's standing
const { data: member } = await supabase
  .from("ds_members")
  .select("shares, loot, voting_power, votes, delegating_to")
  .eq("dao_id", daoAddress)
  .eq("member_address", wallet)
  .single();

ds_records holds Poster metadata. Filter by tag to fetch DAO profiles:

const { data: profile } = await supabase
  .from("ds_records")
  .select("content_json, trust_level, created_at")
  .eq("dao_id", daoAddress)
  .eq("tag", "daoships.launcher.daoProfile")
  .order("created_at", { ascending: false })
  .limit(1);

Treat Poster content as untrusted

ds_records.content is raw on-chain data and is UNTRUSTED — escape it before rendering. Prefer the sanitized content_json column, and respect the trust_level field (VERIFIED, VERIFIED_INITIAL, SEMI_TRUSTED, MEMBER).

Realtime subscriptions

ds_daos, ds_proposals, ds_members, ds_votes, ds_records, ds_navigators, ds_navigator_events, and ds_indexer_state are published to Supabase Realtime. Subscribe to live-update a vote tally:

const channel = supabase
  .channel("votes")
  .on(
    "postgres_changes",
    { event: "INSERT", schema: "testnet", table: "ds_votes", filter: `dao_id=eq.${daoAddress}` },
    (payload) => {
      console.log("new vote", payload.new);
    },
  )
  .subscribe();

Querying over PostgREST directly

supabase-js is a thin wrapper over PostgREST. You can hit the REST endpoint directly — set the Accept-Profile header to choose the schema:

curl "$SUPABASE_URL/rest/v1/ds_daos?select=id,name,total_shares&order=updated_at.desc&limit=10" \
  -H "apikey: $SUPABASE_ANON_KEY" \
  -H "Authorization: Bearer $SUPABASE_ANON_KEY" \
  -H "Accept-Profile: testnet"

For the contracts the indexer watches and the full event list, see Contracts and Architecture. Navigator addresses are discovered dynamically from NavigatorSet events — no static config is needed.