{"_id":"@1001-digital/ponder-ens","name":"@1001-digital/ponder-ens","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@1001-digital/ponder-ens","version":"0.1.0","license":"MIT","type":"module","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"}},"peerDependencies":{"drizzle-orm":">=0.38.0","hono":">=4.0.0","viem":">=2.0.0","pg":">=8.0.0","@electric-sql/pglite":">=0.2.0"},"peerDependenciesMeta":{"pg":{"optional":true},"@electric-sql/pglite":{"optional":true}},"devDependencies":{"@electric-sql/pglite":"^0.2.13","@types/pg":"^8.11.0","drizzle-orm":"^0.38.0","hono":"^4.5.0","pg":"^8.13.0","tsup":"^8.0.0","typescript":"^5.2.0","viem":"^2.21.0"},"scripts":{"build":"tsup src/index.ts --format esm --dts","dev":"tsup src/index.ts --format esm --dts --watch","typecheck":"tsc --noEmit"},"_id":"@1001-digital/ponder-ens@0.1.0","description":"Reusable ENS profile resolution and caching for [Ponder](https://ponder.sh) indexers. Provides on-demand ENS lookups via viem, 30-day cache TTL, and ready-to-mount Hono API routes.","_integrity":"sha512-Yg0RDnjDVGyG/kVTiZu8fCWPKQwkKa81zw7O5fz+6JVCBC/oXvEiCya3/S8DtV0H3yTe/CuXh9naFIXLSScflg==","_resolved":"/tmp/9617cf9da50335542da44a3f4d769963/1001-digital-ponder-ens-0.1.0.tgz","_from":"file:1001-digital-ponder-ens-0.1.0.tgz","_nodeVersion":"24.11.0","_npmVersion":"11.6.1","dist":{"integrity":"sha512-Yg0RDnjDVGyG/kVTiZu8fCWPKQwkKa81zw7O5fz+6JVCBC/oXvEiCya3/S8DtV0H3yTe/CuXh9naFIXLSScflg==","shasum":"7477d4a2aa3d7cefe8b6fc4e5f7ee0165333878d","tarball":"https://registry.npmjs.org/@1001-digital/ponder-ens/-/ponder-ens-0.1.0.tgz","fileCount":5,"unpackedSize":19942,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCzYM8R0ZxdqgqNxNKyRdghNXTRcOg2cKpSxSw9bNwu4QIgV/x4SLxk0/bql9JgGRwSsK34JwUz/joqdZ3CSDrLCgQ="}]},"_npmUser":{"name":"jwahdatehagh","email":"jalil@1001.digital"},"directories":{},"maintainers":[{"name":"jwahdatehagh","email":"jalil@1001.digital"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ponder-ens_0.1.0_1770981236326_0.24041934093916661"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-13T11:13:56.265Z","0.1.0":"2026-02-13T11:13:56.473Z","modified":"2026-02-13T11:13:56.654Z"},"maintainers":[{"name":"jwahdatehagh","email":"jalil@1001.digital"}],"description":"Reusable ENS profile resolution and caching for [Ponder](https://ponder.sh) indexers. Provides on-demand ENS lookups via viem, 30-day cache TTL, and ready-to-mount Hono API routes.","license":"MIT","readme":"# @1001-digital/ponder-ens\n\nReusable ENS profile resolution and caching for [Ponder](https://ponder.sh) indexers. Provides on-demand ENS lookups via viem, 30-day cache TTL, and ready-to-mount Hono API routes.\n\nWorks with both **PostgreSQL** and **PGlite** (Ponder's default embedded database) — no Postgres required for development.\n\n## Why offchain?\n\nPonder rebuilds its onchain tables from scratch on every reindex. If you were to resolve ENS names inside your event handlers, every restart would re-fetch every profile from the ENS registry — hammering your RPC endpoint and slowing down reindexing dramatically.\n\nThis package sidesteps that entirely by storing ENS profiles in a **separate offchain table** that persists across reindexes. Profiles are resolved lazily on first request and cached with a 30-day TTL. No ENS calls happen during indexing.\n\nYour frontend can query the `/ens/:id` endpoint to resolve ENS names for addresses (or addresses for ENS names). Responses are served from cache when fresh, so many concurrent requests for the same profile result in a single RPC lookup — not one per client.\n\n## Install\n\n```bash\npnpm add @1001-digital/ponder-ens\n```\n\nPeer dependencies (your ponder app should already have these):\n\n```bash\npnpm add drizzle-orm hono viem\n```\n\n## Quick start\n\n### 1. Add Ethereum mainnet to your ponder config\n\nENS resolution requires a mainnet RPC endpoint:\n\n```typescript\n// ponder.config.ts\nexport default createConfig({\n  chains: {\n    ethereum: {\n      id: 1,\n      rpc: process.env.PONDER_RPC_URL_1!,\n    },\n    // ... your other chains\n  },\n  // ...\n});\n```\n\n### 2. Mount the routes\n\n```typescript\n// src/api/index.ts\nimport { db, publicClients } from \"ponder:api\";\nimport schema from \"ponder:schema\";\nimport { Hono } from \"hono\";\nimport { client, graphql } from \"ponder\";\nimport { createEnsRoutes, createOffchainDb } from \"@1001-digital/ponder-ens\";\n\nconst { db: ensDb } = await createOffchainDb();\n\nconst app = new Hono();\n\napp.route(\n  \"/ens\",\n  createEnsRoutes({\n    client: publicClients[\"ethereum\"],\n    db: ensDb,\n  }),\n);\n\napp.use(\"/sql/*\", client({ db, schema }));\napp.use(\"/\", graphql({ db, schema }));\n\nexport default app;\n```\n\nThat's it. You now have:\n- `GET /profiles/:id` — returns cached profile, refreshes if stale (>30 days)\n- `POST /profiles/:id` — force refresh, always fetches from ENS\n\nThe `:id` parameter accepts either an Ethereum address or an ENS name.\n\n## How `createOffchainDb` works\n\n`createOffchainDb()` auto-detects your database setup:\n\n- **With `DATABASE_URL`** (or `DATABASE_PRIVATE_URL`): connects to PostgreSQL, creates the `offchain` schema and `ens_profile` table if they don't exist.\n- **Without `DATABASE_URL`**: uses PGlite (Postgres-in-WASM), stores data in `.ponder/ens/` by default.\n\nBoth paths are fully Postgres-compatible — the same schema and queries work in either mode.\n\n```typescript\n// Auto-detect (recommended)\nconst { db } = await createOffchainDb();\n\n// Explicit Postgres\nconst { db } = await createOffchainDb({ databaseUrl: \"postgresql://...\" });\n\n// Explicit PGlite with custom directory\nconst { db } = await createOffchainDb({ dataDir: \".data/ens\" });\n```\n\n## Using the service directly\n\nFor profile resolution outside of API routes (e.g. in scripts), use `createEnsService`:\n\n```typescript\nimport { createEnsService, createOffchainDb } from \"@1001-digital/ponder-ens\";\nimport { createPublicClient, http } from \"viem\";\nimport { mainnet } from \"viem/chains\";\n\nconst client = createPublicClient({ chain: mainnet, transport: http() });\nconst { db } = await createOffchainDb();\n\nconst ens = createEnsService({ client, db });\n\nconst result = await ens.resolveProfile(\"vitalik.eth\");\n// { address: \"0xd8da...\", ensName: \"vitalik.eth\", cachedProfile: ..., isFresh: true }\n\nconst profile = await ens.fetchProfile(\"0xd8da...\");\n\nawait ens.updateProfile(\"0xd8da...\" as `0x${string}`, \"vitalik.eth\");\n```\n\n## Bring your own database\n\nIf you manage your own offchain database (e.g. with drizzle-kit migrations), skip `createOffchainDb` and pass your drizzle instance directly:\n\n```typescript\nimport { createEnsRoutes } from \"@1001-digital/ponder-ens\";\nimport { getOffchainDb } from \"./services/database\";\n\napp.route(\n  \"/ens\",\n  createEnsRoutes({\n    client: publicClients[\"ethereum\"],\n    db: getOffchainDb(),\n  }),\n);\n```\n\nThe package exports the schema for your drizzle config:\n\n```typescript\n// offchain.schema.ts\nexport { ensProfile } from \"@1001-digital/ponder-ens\";\n```\n\n```typescript\n// drizzle.config.ts\nimport { defineConfig } from \"drizzle-kit\";\n\nexport default defineConfig({\n  schema: \"./offchain.schema.ts\",\n  out: \"./migrations\",\n  dialect: \"postgresql\",\n  dbCredentials: { url: process.env.DATABASE_URL! },\n  schemaFilter: [\"offchain\"],\n});\n```\n\nThen generate and run migrations:\n\n```bash\npnpm drizzle-kit generate\npnpm drizzle-kit migrate\n```\n\n## Cross-schema relations (optional)\n\nTo make ENS profiles queryable via Ponder's GraphQL API alongside your onchain tables:\n\n```typescript\n// combined.schema.ts\nimport { relations } from \"drizzle-orm\";\nimport * as ponderSchema from \"./ponder.schema\";\nimport * as offchainSchema from \"./offchain.schema\";\n\nexport const ensProfileRelations = relations(\n  offchainSchema.ensProfile,\n  ({ one }) => ({\n    account: one(ponderSchema.account, {\n      fields: [offchainSchema.ensProfile.address],\n      references: [ponderSchema.account.address],\n    }),\n  }),\n);\n\nexport const schema = {\n  ...ponderSchema,\n  ...offchainSchema,\n  ensProfileRelations,\n};\n```\n\n## Configuration\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `client` | viem `PublicClient` | Client with ENS support (must reach mainnet ENS registry) |\n| `db` | drizzle instance | For reading and writing profiles. Use `createOffchainDb()` or bring your own. |\n| `cacheTtl` | `number` (ms) | Cache freshness window. Defaults to 30 days. |\n\n## Profile data\n\nEach cached profile stores:\n\n```typescript\n{\n  address: string;        // Lowercase Ethereum address (primary key)\n  ens: string | null;     // ENS name\n  data: {\n    avatar: string;       // ENS avatar URL\n    header: string;       // ENS header text record\n    description: string;  // ENS description\n    links: {\n      url: string;        // url text record\n      email: string;      // email text record\n      twitter: string;    // com.twitter text record\n      github: string;     // com.github text record\n    };\n  };\n  updatedAt: number;      // Unix timestamp (seconds)\n}\n```\n","readmeFilename":"README.md","_rev":"1-9d78cf15dd2a723de5b90b9bd5c2f6c1"}