npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nokto-labs/relate-d1

v0.9.0

Published

Cloudflare D1 storage adapter for @nokto-labs/relate.

Readme

@nokto-labs/relate-d1

Cloudflare D1 adapter for Relate.

Links

Install

npm install @nokto-labs/relate @nokto-labs/relate-d1

Quick Start

import { defineSchema, relate } from '@nokto-labs/relate'
import { D1Adapter } from '@nokto-labs/relate-d1'

interface Env {
  DB: D1Database
}

const schema = defineSchema({
  objects: {
    person: {
      attributes: {
        email: { type: 'email', required: true },
        name: 'text',
      },
      uniqueBy: 'email',
    },
  },
})

export function makeDb(env: Env) {
  return relate({
    adapter: new D1Adapter(env.DB),
    schema,
  })
}

Call await makeDb(env).migrate() during setup or before your first write.

Wrangler binding

{
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "my-app",
      "database_id": "your-database-id"
    }
  ]
}

What migrate() creates

| Table | Purpose | |-------|---------| | relate_{object} | Object records, for example relate_person | | relate_relationships | Relationship rows | | relate_activities | Activity timeline rows | | relate_lists | List definitions | | relate_list_items | Static list membership | | relate_migrations | Applied migration tracking | | relate_webhooks | Built-in webhook claim, retry, and processed state |

When you add a new attribute to a schema, migrate() adds the corresponding column automatically.

When an object defines uniqueBy, migrate() also creates a unique index for that field.

Type mapping

| Relate type | SQLite type | |-------------|-------------| | text, email, url, select | TEXT | | ref | TEXT | | number | REAL | | boolean | INTEGER | | date | INTEGER |

Notes

  • boolean is stored as 1 / 0
  • date is stored as a millisecond timestamp
  • ref columns are auto-indexed

Ref guarantees on D1

The D1 adapter supports the stronger ref mutation path:

  • Cascade deletes and set_null updates are planned first
  • The full record-mutation plan is committed through a single D1 batch() call
  • Hooks fire only after the batch succeeds

That means ref cascades are atomic on D1.

Atomic batch writes on D1

D1 also powers db.batch() for the common case of "queue these writes and commit them together":

const result = await db.batch((b) => {
  const price = b.price.create({ name: 'VIP', amountCents: 3200 })
  b.ticket.create({ price: price.id, paymentStatus: 'confirmed' })
  return { priceId: price.id }
})

Notes:

  • Relate lowers the queued writes into prepared statements and sends them through one D1 batch() call
  • hooks fire only after the batch commits successfully
  • v1 supports create() and update()
  • the callback is synchronous, so there are no reads or branches on database state inside the builder

This is the D1-safe subset of a transaction. Read-then-write guards such as stock checks still require raw conditional SQL until the Workers binding exposes a stronger primitive.

Webhook helpers on D1

D1 also backs db.webhook() and db.cleanupWebhooks() through the built-in relate_webhooks table:

const result = await db.webhook('stripe:evt_123', async () => {
  await db.person.upsert({ email: '[email protected]' })
  return 'processed'
})

Notes:

  • the first caller claims the webhook key and runs the handler
  • already processed keys are skipped
  • failures clear the claim and record last_error so a later retry can run
  • if a handler outlives its lease, another caller can reclaim the key and the original completion update will be ignored
  • db.cleanupWebhooks() deletes processed rows older than the default retention window, or an explicit cutoff you pass in

This gives you built-in dedup and retry bookkeeping, but it is still not an exact-once transaction across crashes. If your handler can partially succeed before the processed marker is written, keep those writes idempotent too.

Null filters on D1

D1 compiles optional-field null filters to real SQL null predicates instead of = NULL:

await db.order.count({ paymentId: { eq: null } })
await db.order.find({ filter: { paymentId: { in: [null, 'pay_123'] } } })

Notes:

  • field: null and { field: { eq: null } } compile to IS NULL
  • { field: { ne: null } } compiles to IS NOT NULL
  • in: [null, ...] expands into a null-aware SQL clause

Aggregate queries on D1

D1 implements Relate aggregates natively with SQL COUNT(*), SUM(...), GROUP BY, and one-hop ref joins for aggregate sums.

const totals = await db.deal.aggregate({
  count: true,
  groupBy: 'stage',
})

const value = await db.deal.aggregate({
  filter: { stage: 'won' },
  sum: { field: 'value' },
})

const revenueByPrice = await db.ticket.aggregate({
  filter: { paymentStatus: 'confirmed' },
  count: true,
  groupBy: 'price',
  sum: { field: 'price.amountCents' },
})

That means D1 avoids the JavaScript fallback path for:

  • direct count/sum aggregates
  • grouped count + sum aggregates
  • one-hop ref sums such as price.amountCents
  • one-hop ref sums still work when the joined ref is optional, because D1 uses a LEFT JOIN

Tracked migrations

Use applyMigrations() for schema changes that are not simple "add a new column" changes.

import { renameColumn, dropColumn } from '@nokto-labs/relate-d1'

await db.applyMigrations([
  {
    id: '001_rename_tier_to_plan',
    async up(db) {
      await renameColumn(db, 'person', 'tier', 'plan')
    },
  },
  {
    id: '002_drop_legacy_source',
    async up(db) {
      await dropColumn(db, 'person', 'source')
    },
  },
])

Helpers:

  • renameColumn(db, objectSlug, oldName, newName)
  • dropColumn(db, objectSlug, columnName)

Migrations are tracked in relate_migrations and only run once.

Worker example

import { Hono } from 'hono'
import { relate } from '@nokto-labs/relate'
import { D1Adapter } from '@nokto-labs/relate-d1'
import { relateRoutes } from '@nokto-labs/relate-hono'
import { schema } from './schema'

interface Env {
  DB: D1Database
}

const app = new Hono<{ Bindings: Env }>()

app.route('/', relateRoutes({
  schema,
  db: (c: { env: Env }) => relate({
    adapter: new D1Adapter(c.env.DB),
    schema,
  }),
}))

export default app

Good to know

  • Call migrate() during startup or through a setup route before writing records
  • migrate() is additive; renames and drops belong in applyMigrations()
  • The adapter stores schema metadata in memory through setSchema() so reads and writes work before the next migration run
  • Relate stays D1-first here: it uses atomic batch() operations for ref mutation plans and db.batch() write sets, while db.webhook() adds honest claim/retry bookkeeping instead of pretending D1 has a full exact-once transaction primitive
  • D1 is the reference implementation for db.batch(), native aggregates, and the built-in webhook claim table

Companion packages