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

@volter/twin-airtable

v0.1.37

Published

Local Airtable twin - a faithful, stateful local Airtable Web API (/v0) your integration talks to unmodified. Mirror, simulate, and fork. Built on @volter/world-core.

Readme

@volter/twin-airtable

Legacy connector helpers: this package still has callable helpers using the retired v1 syncPull API. Those paths require migration before use on the current kernel; older helper descriptions below do not establish current compatibility. Check the generated index for protocol standing and use the shared model for current state semantics.

The Airtable twin — a local, stateful, vendor-faithful replica of the Airtable Web API (/v0) on the shared @volter/world-core kernel. The unmodified airtable npm client — Airtable's own official JS client — points at it through its own AIRTABLE_ENDPOINT_URL / endpointUrl knob and gets vendor-correct responses.

State lives in the kernel's append-only action/event log — a write appends an action that projects into visible state; reads use the kernel’s tree. No real Airtable is ever called (the only network path is the connector, over an injected client, behind a fail-closed rate budget). Auth is a faked-locally Bearer personal access token — any value is accepted and none is verified, so the twin serves no 401/403.

Usage

bun run src/cli.ts serve            # the Airtable Web API twin (writable)
bun run src/cli.ts serve --read-only
bun run src/cli.ts mirror           # the React base-browser mirror (bases -> tables -> records)
bun run src/cli.ts conformance      # offline endpoint-probe check over the real router

Point an unmodified client at it with the variable Airtable's own client reads:

AIRTABLE_ENDPOINT_URL=http://127.0.0.1:<port> AIRTABLE_API_KEY=patAnythingLocal node app.js

Nothing is implicit. A fresh twin has no bases, so GET /v0/{baseId}/{table} 404s exactly as the vendor does for a base you cannot see. State is created through the vendor's own metadata API — POST /v0/meta/bases (base + tables + fields) and POST /v0/meta/bases/{baseId}/tables — so there are no twin-only seed routes at all.

Coverage

The capability manifest (src/airtable-capabilities.ts) is the real Airtable Web API surface as the denominator, enumerated top-down from Airtable's own OpenAPI document (103 operations across 22 tags; see src/airtable-spec.ts for the provenance and read date). Coverage is honest and partial. Run bun scripts/manifest-baseline-one.ts airtable for the live count.

Modeled (proven done, each with a failable offline verify):

  • Records — list / get / create (single + batch) / update (PATCH merge, PUT destructive, single + batch) / upsert (performUpsert.fieldsToMergeOn, with createdRecords/updatedRecords) / delete (single + batch), POST …/listRecords, and the vendor's {id, createdTime, fields} shape.
  • List parameters — pageSize (capped at the documented 100), the opaque offset cursor, maxRecords, fields[], sort[i][field|direction], view, cellFormat=json|string (with the documented timeZone/userLocale requirement), returnFieldsByFieldId, recordMetadata[]=commentCount.
  • filterByFormula — a REAL evaluator over a stated subset (comparisons, AND/OR/NOT, LOWER/UPPER/TRIM/LEN/FIND, TRUE/FALSE/BLANK, {Field} and bare field refs, the vendor's own truthiness rule). See the honesty note below.
  • Field types + typecast — cells validated against Airtable's closed 34-value Field-Type enum; strict by default, best-effort string conversion under typecast: true; computed fields (formula/rollup/lookup/…) refuse writes.
  • Metadata API — GET /v0/meta/whoami, list bases, create base, base schema, create/update table, create/update field. Cells are stored by field ID, so a field rename never loses a value.
  • Comments — list / create / update / delete, threaded replies via parentCommentId.
  • Protocol — Airtable's {"error": {"type", "message"}} envelope with only the documented type strings, 404 NOT_FOUND for unmodeled ops and unknown bases/tables/records, 422 INVALID_REQUEST_UNKNOWN (with the vendor's verbatim message) for parameter validation, read-only rejects writes.
  • Dirty-state invariants — a deleted record's id is never reissued, and setting a cell back to a value it previously held is a real write rather than a deduped replay.
  • Connector — pull bases + schemas + records (idempotent, cursor-following, moving poll timestamp; a 4xx, a 200-with-error-envelope, and a 200 whose collection array is missing are all REFUSALS that throw rather than folding an empty account over real state). Push confirms only on success and is deliberately narrower than pull: only RECORD writes are pushable, and an update or delete only for a record whose id the vendor issued — a record this twin minted is never addressed by that id, not even after its create push was confirmed, because confirming a create does not make the local id real. A record created and then deleted locally is never materialized in the real account at all, and a destructive local PUT is pushed as a PUT rather than flattened to a PATCH. Reconciling the vendor-minted id back onto a locally created record is a filed gap (airtable.connector.push_id_reconciliation), and it is what would widen this.
  • Rate budget — the live connector path refuses past a ceiling grounded in Airtable's published 5 requests/second per base, proven by counting calls against an injected fake.
  • UI mirror — the bases sidebar, the tables strip, the schema chips and the records grid are all data-coupled (seed through the twin → fetch the same vendor path the screen reads → render the mirror's own exported component → assert the seeded values in the emitted HTML). The screen shows real twin state or says why it cannot: a refusal renders a visible band carrying the twin's own status and error type rather than an empty placeholder, and a grid showing only the first page says so instead of presenting it as the whole table. The mirror port is the same handler as the API port — same world clock, same readOnly, same contained errors.

Todo (real surface, not yet modeled): webhooks (create/list/delete/payloads/refresh/MAC signing), views API (list/get/delete, per-view metadata on a pulled base, and a view's filters actually shaping ?view=), attachment upload, CSV sync, date-dependency metadata, computed-field evaluation, linked-record integrity, workspaces, collaborators/invites/shares, block installations, the enterprise/SCIM/audit-log/ change-event/hyperDB/automation-run families, personal-access-token scope enforcement, and the expanded-record / cell-editing / view-switcher / grid-pagination mirror screens.

Three honesty notes

There is no invented batch-size cap. Airtable's live API is widely reported to reject more than 10 record objects per write. As of 2026-08-31 neither first-party source states it — the OpenAPI document declares no maxItems anywhere, and the create/update/delete-multiple-records pages state no maximum. Serving a rejection the vendor's published contract does not describe would be surface Airtable does not have, so the twin does not enforce one; the gap is filed as airtable.record.batch_size_limit.

The twin never serves a 429. Airtable's limit is 5 requests/second — a wall-clock property, and a twin's served response must be a pure function of (request, stored state). The vendor's rate behavior is modeled where it actually protects someone: the client-side budget on the live connector path (src/airtable-budget.ts), whose ceiling is half Airtable's documented per-base allowance.

filterByFormula refuses what it cannot evaluate. Airtable's formula language is the full field-formula language (~100 functions, dates, regex, arithmetic). The twin implements a real evaluator for the subset documented in src/airtable-formula.ts and answers 422 with a [twin gap] message for anything else. The alternative — ignoring the parameter — would return every record, which a caller reads as "nothing matched your filter": the worst lie available here. The refusal is a property of the FORMULA, decided at parse time: it does not depend on whether any record happened to be iterated, and a whitespace-only formula reaches the parser rather than being skipped. Which 422 you get is itself a claim about whose fault it is: an unmodeled construct (REGEX_MATCH, arithmetic, a wrong arity) is the twin's gap and says [twin gap], while a malformed formula or a reference to a field the table does not have is a rejection real Airtable also gives and carries the vendor's own documented message — blaming the twin for the caller's bad input would be its own small lie.

Planned (todo):

  • Attachment bytes — persist uploaded attachment bytes and serve them at a twin-local URL that resolves, with the documented expiry semantics (same for comment attachments).
  • 401 on a non-matching token — refuse a Bearer token that does not match the world's established personal access token; the twin accepts any value today.

Mirror

Airtable is a vendor people work in — the grid is the product — so this pack ships a mirror. It is archetype A (API passthrough): the mirror server's non-asset requests fall through to the same handleAirtableTwinRequest the API uses, and the React client fetches the vendor's real paths (/v0/meta/bases, /v0/meta/bases/{id}/tables, /v0/{baseId}/{tableId}) on the same origin. There is one code path, so API↔UI parity cannot drift.

The mirror is currently a read-only state visualization of bases, schemas and records, while Airtable's real product UI (grid editing, view configuration, interface designer, automations) is far beyond what it models. ui-scope.json records needsUi: true anyway — Airtable's grid is the product, so an agent standing in for a human working in a base is a real scenario, and grading the census answer on the mirror's current capability would justify needsUi: false for any pack whose mirror happens to be read-only. The two journeys the UI must eventually support are therefore filed as journey debt (a WARN, not a green), alongside the airtable.mirror.cell_editing, airtable.mirror.record_expand and airtable.mirror.url_routing manifest todos they are blocked on — the same shape notion, mixpanel and segment carry.