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

@odla-ai/db

v0.14.0

Published

Official odla-db admin, realtime, and Preact clients with transaction and schema tooling.

Readme

@odla-ai/db

⚠️ Early access — pre-1.0. Agents work from bounded runbooks; humans approve credentials, production changes, releases, and merges. APIs and exact package availability can change. Review the documented guarantees and limitations; this software is MIT-licensed and provided without warranty.

The official clients for odla-db — a realtime, graph-shaped database on Cloudflare. The package has separate entry points for trusted administration, permission-governed realtime applications, and Preact bindings:

  • @odla-ai/db — backend/admin HTTP client and schema tooling.

  • @odla-ai/db/client — WebSocket subscriptions, optimistic writes, presence, and end-user authentication.

  • @odla-ai/db/preactOdlaProvider, useQuery, and useTransact. The old /react subpath is a deprecated alias to this same Preact build.

  • Isomorphic transport — runs in Node 20+, Cloudflare Workers, and the browser. The root admin entry still belongs only in trusted backends because its full application key bypasses rules.

  • Self-contained — zero runtime dependencies; the wire protocol, the tx builder, and the schema builder are bundled in.

  • Graph-native — entities are nodes, typed links are edges, and Lookup refs give you upsert-by-natural-key (MERGE) for re-ingestion-safe writes.

  • Default-deny permissions — end-users (and rules-scoped credentials) can't read or write a namespace until its CEL rules are set; a missing namespace or action means "no". Full app API keys bypass rules (trusted backends only).

  • Provenance on every node — the server maintains $createdAt / $createdBy / $updatedAt / $updatedBy from the verified writer identity. $-prefixed attrs are reserved: client writes to them are rejected.

  • Runtime-neutral federation signing@odla-ai/db/federation signs and verifies one app-to-app HTTP request with Web Crypto. The envelope binds the asserted sender, timestamp, nonce, method, path/query, and exact body digest; callers exchange neither browser sessions nor database application keys.

Ask the runbooks first. odla's operational procedures live in a database, not in this file: npx @odla-ai/cli runbook ask "<question>" returns the current steps, and unlike anything written here it cannot be out of date. Use it before searching the web or working from memory. This README and the JSDoc in the shipped .d.ts are the version-matched API reference; a runbook is the procedure. Most tasks need an answer from both.

Install

npm i @odla-ai/db

Use

import { initAdmin, tx } from "@odla-ai/db";

const db = initAdmin({
  appId: "myapp",
  adminToken: process.env.ODLA_SK!,          // an app API key: odla_sk_...
  endpoint: process.env.ODLA_URL!,           // your odla-db worker, e.g. https://<worker>.workers.dev
});

// write
await db.transact(
  tx.notes[crypto.randomUUID()].update({ text: "hi", createdAt: Date.now() }),
);

// read
const { notes } = await db.query({ notes: { $: { order: { createdAt: "desc" } } } });

Every query node returns at most 100 rows unless it sets limit; the maximum is 1,000. Queries are also bounded to six relation levels, 32 nodes, 100 where nodes, a 512-character search string, and a 10,000-row candidate scan. Add an indexed filter and paginate instead of relying on an unbounded materialization. Encoded results and aggregate/query materialization are capped at 1 MB; aggregates have the same 10,000-row scan guard. $like is deliberately indexable and case-sensitive: use an exact literal ("Ada") or one trailing wildcard ("Ada%"). Leading/interior % and _ patterns are rejected; use full-text search for substring/token search. Per-entity full-text documents index at most 1 MB of text fields in attribute-name order.

For a direct browser or end-user connection, use a signed-in user token. Never expose an odla_sk_ application key:

import { init } from "@odla-ai/db/client";

const db = init({
  appId: "myapp",
  endpoint: "wss://db.odla.ai",
  getToken: () => auth.getToken(),
});

const unsubscribe = db.subscribeQuery({ notes: {} }, ({ notes }) => render(notes));

That direct connection is safe when the browser presents a signed-in user's JWT and the app's default-deny CEL rules intentionally grant that user the requested rows. It is not a reason to put an odla_sk_ application key in the browser: a full app key bypasses rules and belongs only in a trusted backend.

Worker-authorized realtime without a browser database token

An app that keeps authorization in its Cloudflare Worker can expose a same-origin WebSocket route with AdminDb.realtime.proxy(). The Worker first authenticates its own application session, then grants that connection an exact list of read queries. The app key stays inside the Worker. After the handshake, the database AppDO owns the hibernatable socket to the browser; the Worker does not need to keep a relay socket or invocation alive.

import { initAdmin } from "@odla-ai/db";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const livePath = `/db/app/${env.ODLA_APP_ID}/connect`;
    if (url.pathname !== livePath) return new Response("Not found", { status: 404 });

    // Use your app's normal cookie/session verifier before authorizing data.
    const session = await appAuth.verifyRequest(request, env);
    if (!session) return new Response("Unauthorized", { status: 401 });

    const db = initAdmin({
      appId: env.ODLA_APP_ID,
      adminToken: env.ODLA_ADMIN_TOKEN,
      endpoint: env.ODLA_DB_URL,
    });
    const ownNotes = {
      notes: {
        $: {
          where: { ownerId: session.userId },
          order: { createdAt: "desc" },
        },
      },
    } as const;

    return db.realtime.proxy(request, {
      allowedOrigins: ["https://app.example.com"],
      queries: [ownNotes],
      expiresInSeconds: 300,
    });
  },
};

The helper requires the request's Origin to be an exact member of allowedOrigins; wildcards and a missing Origin are rejected. It ignores the incoming path, query string, and route appId when choosing a database, and always targets the app configured on that AdminDb. Keep the Worker route exact as an additional boundary. One grant may contain at most 16 exact queries with an approximately 4 KiB serialized allow-list, and at least one query is required. expiresInSeconds may be 1–300 seconds and defaults to 300. Because the Worker leaves the frame path after the 101 upgrade, that TTL is also the maximum delay before an app-session or app-key revocation takes effect; choose a shorter value for especially sensitive data.

The browser uses the ordinary reactive client against the same origin and does not provide getToken:

import { init } from "@odla-ai/db/client";

const ownNotes = {
  notes: {
    $: {
      where: { ownerId: currentUser.id },
      order: { createdAt: "desc" },
    },
  },
} as const;

const db = init({
  appId: "myapp",
  endpoint: `${location.origin.replace(/^http/, "ws")}/db`,
  // No getToken: the same-origin Worker authorizes the WebSocket handshake.
});
const unsubscribe = db.subscribeQuery(ownNotes, ({ notes }) => render(notes));

The browser's subscription query must exactly match one of the queries the Worker granted. A query returns the complete matching entities, so authorize every field on every matched root and relation row—not just the fields used by its where or order. This path deliberately bypasses CEL view rules; the Worker's exact query is the authorization boundary. Do not mix fields with different sensitivity on an entity exposed by a proxy grant. This proxy is read-only: only the exact authorized query subscriptions work. It cannot transact, join presence/rooms, subscribe to reserved namespaces, or expand a query after the connection is open. Every reconnect returns through the Worker, revalidates the app session and Origin, and creates a new bounded grant. For mixed or dynamic per-user access, use the normal signed-user-JWT connection under default-deny CEL rules. If all data must remain behind the Worker, use the server-side changes() cursor and stream authorized results to the browser over the app's own SSE route.

import { OdlaProvider, useQuery } from "@odla-ai/db/preact";

Getting a token (for agents) — no secret required

You need an odla-db credential to read/write. An agent should never be handed the platform admin secret. Instead, do a device-authorization handshake at the odla platform: name the existing account by email, show that human a short code, and wait for the same signed-in account to explicitly review and approve it in odla.ai/studio. The email is a non-secret identity hint; never ask for a password, Clerk session, or other human credential. Approval registers a named agent principal and returns a tracked, revocable, short-lived developer token bound to it. The token does not inherit the human's projects or platform-admin role: the human separately grants exact capabilities on exact projects. A repeat device handshake without agentHandle registers a new agent principal. Requesting the same stable handle again and receiving human approval reconnects the replacement credential to the existing principal; the handle is covered by the same immutable digest as the project/capability request.

import { requestToken, initAdmin } from "@odla-ai/db";

const platform = "https://odla.ai";
const endpoint = "https://db.odla.ai";

const { token } = await requestToken({
  endpoint: platform,
  email: process.env.ODLA_USER_EMAIL!,
  agentHandle: "customer-app",
  projectIds: ["my-app"], // exact request; approval cannot rewrite it
  // Required only to create/configure this app or administer its tenants.
  // Baseline collaboration handshakes omit this optional capability.
  optionalProjectCapabilities: ["app.manage"],
  onCode: ({ userCode, verificationUriComplete }) =>
    console.log(`Review ${userCode}: ${verificationUriComplete ?? "https://odla.ai/studio"}`),
});

// `token` (odla_dev_…) can act only on explicitly approved project ids. If
// `my-app` is absent, this approved app.manage request may create that exact app
// once; Registry then binds the grant to the new app incarnation. It cannot
// create another project or perform ownership/lifecycle mutations.
const db = initAdmin({ appId: "my-app", adminToken: token, endpoint });

Under the hood: POST https://odla.ai/handshake with { email, projectIds, optionalProjectCapabilities, ... } → show userCode → the named user signs in and explicitly claims that exact code → poll POST https://odla.ai/handshake/poll with the returned deviceCode until the approval is collected once. Unknown and never-signed-in accounts do not create a claimable request, and their public response is deliberately indistinguishable from a valid start to prevent account enumeration. Only one claimed pending or approved-but-uncollected request may be active per account. Studio shows the immutable request and its fingerprint. The owner either approves it exactly or declines it with feedback returned to the polling client as handshake_denied; changing access requires a fresh request. An approved project id may be new: collection records a one-time exact-id reservation, and app creation consumes it while binding the grant to the new app incarnation. app.manage is an explicit provisioning opt-in, not a baseline grant. The reservation grants no unrelated app administration; existing archived or foreign ids remain unavailable. The constants above are the hosted odla defaults. For a self-hosted or alternate deployment, supply its platform and database endpoints through configuration rather than hard-coding either URL in application logic. Hosted odla-db treats the platform registry as the credential authority: an unknown developer token is rejected and never falls back to a stale local token row. A self-hosted deployment without that registry may retain the local compatibility resolver, but local manager metadata still grants no project access.

Permission rules (required before end-users can touch data)

odla-db is default-deny: until you install rules, end-users see nothing and every end-user write is rejected with permission_denied. Install per-namespace CEL rules with a signed-in human operator, the platform machine, or the exact owner-approved app.manage credential used by provisioning (this replaces the app's rule set). A baseline collaboration credential cannot rewrite rules:

await fetch(`${endpoint}/app/${appId}/admin/rules`, {
  method: "POST",
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
  body: JSON.stringify({
    notes: {
      view: "auth.id == data.ownerId",     // read own rows
      create: "auth.id == data.ownerId",   // may only create rows they own
      update: "auth.id == data.ownerId",
      delete: "false",                     // nobody deletes
    },
  }),
});

Rule context: auth.id/email/signedIn/entitlements/user/claims, auth.kind ("user" | "agent" | "admin"), auth.agent ({ id, label } for scoped keys), data.<field>, newData.<field>, ref('label.field'). Reserved namespaces default to own-rows-only reads ($users, $entitlements, $subscriptions) or fully closed ($files).

Scoped keys — the right credential for an agent

A full odla_sk_ key bypasses rules. A signed-in human, the platform machine, or an exact owner-approved app.manage provisioning credential may mint the app's deployment key. A human or machine may instead mint a key for an active, project-granted agent and pass scopes so it is governed by the rules (auth.kind == "agent" in CEL), optionally read-only and/or restricted to specific namespaces. A baseline agent credential cannot mint a successor. Once a key has a scope object, vault access is also default-deny; list the exact secret names it needs in secrets (or deliberately grant "*"):

await fetch(`${endpoint}/admin/apps/${appId}/keys`, {
  method: "POST",
  headers: { authorization: `Bearer ${operatorToken}`, "content-type": "application/json" },
  body: JSON.stringify({
    label: "kanban-bot",
    scopes: {
      mode: "rules",
      agentId: "agent_palette_scout",
      namespaces: ["cards", "columns"],
      secrets: ["anthropic_api_key"],
    },
  }),
});

Scoped credentials are data-plane only: they can never edit rules, mint keys, or act as operators. A rules/read-only/namespaced key without secrets receives 403 missing_capability from db.secrets.get().

Graph writes — upsert by natural key + link

Lookup refs ({ ns, attr, value }) resolve to an existing entity by a unique attribute, or create one — so re-discovering the same node is idempotent. Emit raw Op literals for full control (node updates first, then links):

await db.transact([
  { t: "update", ns: "company", id: { ns: "company", attr: "slug", value: "acme" }, attrs: { name: "Acme" } },
  { t: "update", ns: "person",  id: { ns: "person",  attr: "slug", value: "ada"  }, attrs: { name: "Ada Lovelace" } },
  { t: "link",   ns: "company", id: { ns: "company", attr: "slug", value: "acme" }, label: "founded_by",
    target: { ns: "person", attr: "slug", value: "ada" } },
], { mutationId: "ingest:acme-foundedby-ada" });   // stable id => exactly-once

Author & push a schema

import { i } from "@odla-ai/db";

const schema = i.schema({
  entities: {
    company: i.entity({ slug: i.string().unique(), name: i.string() }),
    person:  i.entity({ slug: i.string().unique(), name: i.string() }),
  },
  links: {
    founded: {
      forward: { on: "company", has: "many", label: "founded_by" },
      reverse: { on: "person",  has: "many", label: "founded" },
    },
  },
});

await fetch(`${endpoint}/app/${appId}/schema`, {
  method: "POST",
  headers: { authorization: `Bearer ${odlaSk}`, "content-type": "application/json" },
  body: JSON.stringify({ schema: schema.serialize() }),
});

Porting relational code — semantics that differ from SQL

Field notes from porting a production Postgres app (a membership flow) onto odla-db. Each of these is a silent behavior difference; design for them up front rather than discovering them in production:

  • Entity ids are not attributes. where: { id } matches nothing. If you query rows by id (every SELECT ... WHERE id = $1 port does), mirror the id as a unique attr on each row: { t: "update", ns, id: rowId, attrs: { id: rowId, ... } }. Query results are unchanged — hydration returns the same id either way.

  • Null depends on the declared type. JSON attributes can preserve JSON null; a typed scalar attribute rejects it in strict schema mode. Omit an optional scalar on write when absence is the intended state. To CLEAR a scalar that was already written, use the retract op — tx.things[id].retract(["dueAt", "ownerId"]), the scalar sibling of unlink. It removes those attributes' triples (a no-op when already absent) and fails the transaction only if it would leave a required attribute missing.

  • Always pass order on list queries. Unordered results sort lexicographically by entity id: creation order for uuidv7() ids, arbitrary for anything else.

  • Pagination is bounded. Omitted limit means 100; explicit limits may be 0–1,000 and offsets 0–100,000. Each nested relation node is bounded too.

  • Realtime state is bounded. One socket may retain 32 subscriptions, and its total hibernation state (identity, subscriptions, and presence) must fit 16 KiB. Excess operations fail as subscription_limit or session_state_too_large before registering state. The server retains at most 4 MB of prior diff snapshots per app Durable Object; subscriptions over that budget continue with full query-result snapshots instead of diffs.

  • Wire bodies are bounded. JSON HTTP bodies are stream-read; both HTTP bodies and WebSocket messages cap at 1.1 MB (request_too_large / message_too_large). Query results have the stricter 1 MB encoded cap.

  • Uniqueness is single-attr. A composite SQL unique becomes a derived unique attr (for example group_email: "<groupId>|<email>"); a violation aborts the whole transact with unique_violation, exactly like an ON CONFLICT abort in a SQL transaction.

  • A multi-op transact is atomic (one Durable Object transaction), so batch a change and its audit row together. There is no SELECT ... FOR UPDATE: bind a read to its write with guards, which check existence, selected field values, and explicitly absent legacy fields inside the same transaction. A failed guard aborts every op with transact_guard_failed. Pair that compare-and-swap check with a stable mutationId for exactly-once retries:

    const { duplicate } = await db.transact(ops, {
      mutationId: `edit:${id}:${reviewed.version}`,
      guards: [{
        ns: "documents",
        id,
        exists: true,
        equals: { version: reviewed.version },
        absent: ["supersededAt"],
      }],
    });
    const fresh = !duplicate; // first delivery: this call did the write
  • Aggregates are one-shot: await db.aggregate("applications", { count: true }, { where: { status: "pending" } }){ count }.

  • Trusted change cursors are resumable and content-free:

    const checkpoint = await db.changes(); // snapshot boundary; no history replay
    const page = await db.changes({ cursor: checkpoint.cursor, limit: 200 });

    Each transaction reports only its txId, commit time, and touched/created namespace+entity keys—never operation bodies or attribute values. This surface requires a full server app key; rules-scoped credentials are denied. Persist page.cursor only after consuming the page. Restore, copy, or truncated history returns checkpointRequired: true with a replacement checkpoint instead of an apparently valid empty result.

  • Health probes: GET /app/:id/schema (Bearer: the API key) is the cheap connectivity and schema-presence check — compare Object.keys(schema.entities) against the namespaces you expect.

  • Schema pushes validate compatibility. Declared namespaces are strict; unsafe type, required-field, uniqueness, link, and removal changes are rejected before metadata changes. See the database schema contract for strict/schemaless behavior and the accepted migration envelope.

API surface

  • initAdmin(opts) (legacy alias: root init) → HTTP administration client; its application key bypasses rules and belongs only in trusted backends. The returned client includes changes() for server-issued transaction checkpoints and bounded resume pages.
  • @odla-ai/db/client → realtime init, subscriptions, optimistic writes, persistence, aggregate queries, rooms, and content-free connection health under an end-user token. db.connectionHealth() plus onHealth distinguish live/reconnecting/stale/closed state and expose heartbeat RTT and bounded queue depth; onTelemetry emits low-cardinality events without app, user, query, mutation, room, or payload data.
  • @odla-ai/db/preactOdlaProvider, useDb, useQuery, useTransact. transact resolves to { txId, duplicate } (TransactResult).
  • tx, flattenTx, TxChain, uuidv7, PROTOCOL_VERSION.
  • @odla-ai/db/federation (also exported from the trusted root entry) — signFederatedRequest and verifyFederatedRequest, plus stable FEDERATION_HEADERS and FEDERATION_VERSION. Store the shared edge secret in each application's server-side vault; never put it in config or browser data. Verification defaults to five minutes of clock skew and can enforce a sender allowlist. Non-idempotent routes should supply consumeNonce backed by a durable replay gate; idempotent routes may rely on their natural mutation identity.
  • OdlaError — stable code, optional HTTP status/detail/requestId, and retryable; realtime clients surface connection errors through onError.
  • i, OdlaSchema, Attr — schema builder.
  • All wire types: Op, EntityRef, Lookup, InstaQLQuery, QueryResult, Entity, Value, Json, SerializedSchema, …; realtime health types: ConnectionHealth, ClientTelemetryEvent, and FreshnessMode.

0.6 migration notes

  • Queries that omitted limit now return at most 100 rows; paginate explicitly if you previously relied on full-namespace reads. The maximum limit is 1,000.
  • Oversized query shapes and scans fail with stable query_* error codes.
  • $like now accepts only case-sensitive exact or trailing-% prefix patterns; replace substring patterns with full-text search.
  • Realtime sockets cap active subscriptions at 32 and combined hibernation state at 16 KiB. Diff retention exhaustion falls back to full snapshots.
  • JSON wire bodies over 1.1 MB and encoded query results over 1 MB are rejected.
  • HTTP and WebSocket peers negotiate protocol v1; declared mismatches fail with protocol_version instead of being decoded as an unknown response.
  • SDK failures are OdlaError instances. Existing message matching should move to error.code; server detail is preserved when it is JSON-safe.
  • Device authorization uses handshake_denied, handshake_expired, handshake_timeout, aborted, network_error, and protocol_version; these are stable codes, not strings to recover from the message.

Live totals for paginated views

A query node can ask for the count of rows that match it, ignoring its own limit/offset — the number a pager needs:

const { data, totals } = useQuery({
  pm_bug: { $: { where: { appId: "silver-salt" }, limit: 50, count: true } },
});
// data.pm_bug  → the 50-row page
// totals.pm_bug → how many match in total, live

Unlike the one-shot aggregate, this is reactive: the count is recomputed with the rows and delivered on the same frames, including diff frames. A row added past the last page moves the total without moving the page, and that frame is still sent — a pager that showed "of 12" becomes "of 13" on its own.

Counts are taken after view-rule filtering, so they never reveal the size of a result set you are not allowed to read. That is also why count is opt-in rather than automatic: it evaluates rules across every match rather than just the returned page, so the cost scales with matches, not with limit. Ask for it on the views that paginate.