For AI agents: the documentation index is at /llms.txt. Markdown versions of pages are available by appending .md to the URL.
Skip to main content

Entities Schema (schema.graphql)

The schema.graphql file defines the data model for your HyperIndex indexer. Each entity type defined in this schema corresponds directly to a database table, with your event handlers responsible for creating and updating the records. HyperIndex automatically generates a GraphQL API based on these entity types, allowing easy access to the indexed data.


Defining Entity Types

Entities in your schema are defined as GraphQL object types:

Example:

type User {
id: ID!
greetings: [String!]!
latestGreeting: String!
numberOfGreetings: Int!
}

Requirements:

  • Every entity must have a unique id field, using one of these scalar types:
    • ID!, String!, Int!, or BigInt!
  • The id field must be non-nullable, must not be a list, and cannot be a @derivedFrom field.

Numeric Entity IDs

ID is the usual choice and behaves as a string. Since v3.5, you can also key an entity on Int or BigInt, which is a better fit when the identifier is genuinely a number — a block number, an auction id, a sequential position:

type Auction {
id: BigInt! # the on-chain auction id, not a stringified copy of it
seller: String!
bids: [Bid!]! @derivedFrom(field: "auction")
}

type Bid {
id: ID!
auction: Auction! # inferred as BigInt to match Auction.id
amount: BigInt!
}

Relationship fields adopt the referenced entity's id type automatically, so Bid.auction above is typed bigint in your handlers rather than string. You don't declare the foreign key type — keep the two sides in sync by changing the referenced entity's id.


Scalar Types

Scalar types represent basic data types and map directly to JavaScript, TypeScript, or ReScript types.

GraphQL ScalarDescriptionJavaScript/TypeScriptReScript
IDUnique identifierstringstring
StringUTF-8 character sequencestringstring
IntSigned 32-bit integernumberint
FloatSigned floating-point numbernumberfloat
Booleantrue or falsebooleanbool
BytesUTF-8 character sequence (hex prefixed 0x)stringstring
BigIntSigned integer (int256 in Solidity)bigintbigint
BigDecimalArbitrary-size floating-pointBigDecimal (imported)BigDecimal.t
TimestampTimestamp with timezoneDateJs.Date.t
JsonJSON objectJsonJs.Json.t

Learn more about GraphQL scalars here.


Working with BigDecimal

The BigDecimal scalar type in HyperIndex is based on the bignumber.js library, which provides arbitrary-precision decimal arithmetic. This is essential for financial calculations and handling numeric values that exceed JavaScript's native number precision.

Importing BigDecimal

// JavaScript/TypeScript
import { BigDecimal } from "envio";

// ReScript
open BigDecimal;

Creating BigDecimal Instances

// From string (recommended for precision)
const price = new BigDecimal("123.456789");

// From number (may lose precision for very large values)
const amount = new BigDecimal(123.45);

// From other BigDecimal
const copy = new BigDecimal(price);

Arithmetic Operations

BigDecimal instances are immutable. Operations return new BigDecimal instances:

// Basic arithmetic
const a = new BigDecimal("123.45");
const b = new BigDecimal("67.89");

const sum = a.plus(b); // 191.34
const difference = a.minus(b); // 55.56
const product = a.times(b); // 8,381.03
const quotient = a.div(b); // 1.81839...

// Power
const squared = a.pow(2); // 15,239.9025

// Square root
const root = a.sqrt(); // 11.11...

// Absolute value
const abs = new BigDecimal("-123.45").abs(); // 123.45

Comparison Methods

const x = new BigDecimal("10.5");
const y = new BigDecimal("10.5");
const z = new BigDecimal("9.9");

x.eq(y); // true (equal)
x.gt(z); // true (greater than)
x.gte(y); // true (greater than or equal)
x.lt(z); // false (less than)
x.lte(y); // true (less than or equal)

// Check for special values
x.isZero(); // false
x.isPositive(); // true
x.isNegative(); // false
x.isFinite(); // true

Rounding and Formatting

const value = new BigDecimal("123.456789");

// Get with specific decimal places
value.dp(2); // 123.46 (rounded)
value.dp(2, 1); // 123.45 (rounded down)

// Format as string
value.toString(); // "123.456789"
value.toFixed(2); // "123.46"
value.toExponential(2); // "1.23e+2"
value.toPrecision(5); // "123.46"

Working with Schema-Defined BigDecimal Fields

When you've defined a BigDecimal field in your schema:

type TokenPair {
id: ID!
name: String!
price: BigDecimal!
volume: BigDecimal!
}

You can use it in your handlers:

// In your event handler
context.TokenPair.set({
id: event.params.pairId,
name: event.params.name,
price: new BigDecimal(event.params.price),
volume: new BigDecimal("0"), // Start with zero volume
});

// Updating a field
const tokenPair = await context.TokenPair.get(pairId);
if (tokenPair) {
const newVolume = tokenPair.volume.plus(new BigDecimal(tradeAmount));
context.TokenPair.set({
...tokenPair,
volume: newVolume,
});
}

Example: Financial Calculation

function calculateFee(amount: BigDecimal, feeRate: BigDecimal): BigDecimal {
// Calculate fee with proper rounding
return amount.times(feeRate).dp(2);
}

const tradeAmount = new BigDecimal("1250.75");
const feeRate = new BigDecimal("0.0025"); // 0.25%
const fee = calculateFee(tradeAmount, feeRate); // 3.13

Best Practices for BigDecimal

  1. Always use strings for initialization when precision matters:

    // Preferred
    const value = new BigDecimal("123.456789");

    // May lose precision
    const value = new BigDecimal(123.456789);
  2. Set precision explicitly when doing division:

    // Set to 8 decimal places for crypto prices
    const price = totalValue.div(tokenAmount).dp(8);
  3. Handle rounding appropriately for financial calculations:

    // Round down (floor) for user-favorable calculations
    const userReceives = amount.dp(2, 1); // ROUND_DOWN

    // Round up (ceil) for protocol-favorable calculations
    const protocolFee = amount.dp(2, 0); // ROUND_UP
  4. Compare with equals method instead of == or ===:

    // Correct
    if (value.eq(new BigDecimal(0))) {
    /* ... */
    }

    // Incorrect - compares object references
    if (value === new BigDecimal(0)) {
    /* ... */
    }
  5. Chain operations carefully, remembering that each operation returns a new instance:

    // Calculate (a + b) * c with proper precision
    const result = a.plus(b).times(c).dp(8);

Enum Types

Enums allow fields to accept only a predefined set of values.

Example:

enum AccountType {
ADMIN
USER
}

type User {
id: ID!
balance: Int!
accountType: AccountType!
}

Enums translate to string unions (TypeScript/JavaScript) or polymorphic variants (ReScript):

TypeScript Example:

import { type Enum } from "envio";

let user = {
id: event.params.id,
balance: event.params.balance,
accountType: "USER" satisfies Enum<"AccountType">, // enum as string
};

ReScript Example:

let user: Types.userEntity = {
id: event.params.id,
balance: event.params.balance,
accountType: #USER, // polymorphic variant
};

Relationships: One-to-Many (@derivedFrom)

Define relationships between entities using the @derivedFrom directive, known as reverse lookups.

Example:

type NftCollection {
id: ID!
contractAddress: Bytes!
name: String!
symbol: String!
maxSupply: BigInt!
currentSupply: Int!
tokens: [Token!]! @derivedFrom(field: "collection")
}

type Token {
id: ID!
tokenId: BigInt!
collection: NftCollection!
owner: User!
}
  • The tokens field in NftCollection is a virtual field, populated automatically when querying the API.
  • Set relationships in your handlers by assigning <field>_id with the related entity's id. For example, create or update a Token entity with collection_id: collectionId.

Field Indexing (@index)

Add an index to a field for optimized queries and loader performance:

type Token {
id: ID!
tokenId: BigInt!
collection: NftCollection!
owner: User! @index
}
  • All id fields and fields referenced via @derivedFrom are indexed automatically.
  • Declare @index for the fields your GraphQL consumers filter and sort by. You don't need it for fields your handlers query with getWhere — since v3.5 HyperIndex creates those indices itself. See Indices created on demand.

Choosing a Storage Backend (@storage)

When you enable more than one storage backend in config.yaml, the @storage directive controls where each entity is written:

# Queryable over GraphQL and mirrored into ClickHouse for analytics
type Transfer @storage(postgres: true, clickhouse: true) {
id: ID!
amount: BigInt!
}

Since v3.2 you can mark a backend as default in config.yaml, and entities without a @storage directive go there — you no longer need the directive on every entity. See storage.

Per-Entity ClickHouse Tuning

Since v3.4, the clickhouse argument also accepts an options object that tunes that entity's ClickHouse history table:

type Transfer
@storage(
postgres: true
clickhouse: {
partitionBy: "toYYYYMM(timestamp)"
orderBy: ["timestamp"]
ttl: "timestamp + INTERVAL 2 YEAR"
}
) {
id: ID!
timestamp: Timestamp!
amount: BigInt!
}
OptionTypeDescription
partitionByClickHouse expressionEmitted as PARTITION BY <expr>. Keeps queries and TTL deletes inside a partition instead of scanning the whole table.
orderBylist of entity field namesFields that lead the table's sorting key, ahead of the default id.
ttlClickHouse expressionEmitted as TTL <expr>. Ages rows out automatically.

A few constraints, all caught at envio codegen rather than at runtime:

  • orderBy takes entity field names, not expressions — unlike partitionBy and ttl, which are ClickHouse expressions passed through as written.
  • orderBy can't list id (already the default sorting key), nor nullable, list or @derivedFrom fields, which ClickHouse doesn't allow in a sorting key.
  • An entity can carry only one @storage directive, and it must enable at least one backend.

Advanced: Precision and Scale (@config Directive)

Customize the precision and scale for BigInt and BigDecimal fields using @config.

Syntax:

  • BigInt (precision only):
amount: BigInt @config(precision: 76)
  • BigDecimal (precision and scale):
price: BigDecimal @config(precision: 10, scale: 2)

Example:

type Payment {
id: ID!
amount: BigInt @config(precision: 76)
price: BigDecimal @config(precision: 10, scale: 2)
}

This controls PostgreSQL storage allocation and numerical accuracy.

Detailed Example with Arrays
type AdvancedEntity {
exampleBigInt: BigInt @config(precision: 76)
exampleBigIntRequired: BigInt! @config(precision: 77)
exampleBigIntArray: [BigInt!] @config(precision: 78)
exampleBigIntArrayRequired: [BigInt!]! @config(precision: 79)
exampleBigDecimal: BigDecimal @config(precision: 10, scale: 5)
exampleBigDecimalRequired: BigDecimal! @config(precision: 12, scale: 4)
}

Documenting Entities, Fields, and Relationships

You can document your entities, fields, and relationships directly in schema.graphql using GraphQL string descriptions. These descriptions are exposed through the generated GraphQL API and appear in introspection, making your API self-documenting.

"""
A token transfer between two accounts
"""
type Transfer {
id: ID!
"The address the tokens were sent from"
from: String!
"The address the tokens were sent to"
to: String!
"The amount transferred, in wei"
value: BigInt!
}

Both single-line ("...") and multi-line ("""...""") descriptions are supported.

note

Only string descriptions are exposed in introspection. Hash (#) comments are ignored by the GraphQL parser and do not appear in the API. Descriptions on entities, fields, and relationships were added in HyperIndex v3.1.


Generating Types

Once you've defined your schema, run this command to generate these entity types that can be accessed in your event handlers:

pnpm envio codegen

Best Practices

  • Use camelCase for field names (latestGreeting, numberOfGreetings).
  • Keep entity and field names clear, descriptive, and intuitive.

You're now ready to define powerful schemas and efficiently query your indexed data with HyperIndex!