Web-standard building blocks for any JavaScript runtime.
The web's missing standard library: fast, lightweight, composable packages, so you can write your code once and run it in browsers, Cloudflare Workers, Node 24+, Bun, and Deno.
Save a streamed OpenAI chat completion to a file as JSON Lines:
import * as Jsonl from '@portable/jsonl';
import * as Sse from '@portable/sse';
import * as Streams from '@portable/streams';
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
body: JSON.stringify({ model, messages, stream: true }),
});
// `transcript` is a WritableStream<Uint8Array> that writes to a file.
await response.body!
.pipeThrough(new Sse.DecoderStream())
.pipeThrough(Streams.filter((event) => event.data !== '[DONE]'))
.pipeThrough(Streams.map((event) => JSON.parse(event.data)))
.pipeThrough(new Jsonl.EncoderStream())
.pipeTo(transcript);Strip photo metadata (GPS, camera, dates) before uploading:
import * as Exif from '@portable/exif';
import * as ImageSize from '@portable/image-size';
// `file` is a JPEG, PNG or WebP File from a file input or a drop.
const photo = Exif.strip(new Uint8Array(await file.arrayBuffer()));
const { width, height } = ImageSize.parse(photo);
await fetch(`/upload?width=${width}&height=${height}`, { method: 'POST', body: photo });| Streams and bytes | |
|---|---|
| @portable/streams | Collects, combines, and transforms web streams and iterables, and converts Node streams to web streams. |
| @portable/lines | Splits a stream of bytes or text into lines. |
| @portable/binary | Reads and writes integers, varints, and strings in binary protocols, and splits streams into length-prefixed frames. |
| @portable/bytes | Hex encoding, concatenation, and constant-time comparison of Uint8Arrays. |
| @portable/errors | The error classes every package throws. Errors.is and instanceof match them even when two copies of a package are installed. |
| @portable/compress | gzip and deflate on the platform's CompressionStream, a decompression stream with an output limit, and CRC-32. |
| Encodings | |
| @portable/base64 | base64 and base64url, with MIME line wrapping and streams. |
| @portable/quoted-printable | Quoted-printable encoding and decoding, as functions and streams. |
| @portable/charset | Encodes text into legacy charsets such as Shift_JIS, GBK, Big5, and windows-1251. |
| @portable/idna | Converts domain names between Unicode and ASCII by UTS #46. |
| Data formats | |
| @portable/csv | Decodes and encodes CSV, as functions and streams. |
| @portable/jsonl | Decodes and encodes JSON Lines streams. |
| @portable/json-stream | Selects values from a large JSON document by JSONPath, and writes JSON from a tree of promises and streams. |
| @portable/mail | Builds a raw email message, with attachments and inline images, as a stream. |
| @portable/mime-headers | Encoded words, folding, parameters, and address lists in email headers. |
| @portable/mime-stream | Splits a MIME message into a stream of headers and bodies and writes it back byte for byte. |
| @portable/smtp | Sends email over SMTP or LMTP from any runtime with a TCP socket API. |
| @portable/dkim | Signs and verifies DKIM signatures. |
| @portable/spf | Checks whether a client may send mail for a domain, using SPF. |
| @portable/dmarc | Checks a message against the DMARC policy of its From domain. |
| @portable/arc | Seals and verifies ARC sets. |
| @portable/mail-auth | Checks a received message's SPF, DKIM, DMARC, and ARC in one call, writes the Authentication-Results header, and ARC-seals it. |
| DNS and networking | |
| @portable/dns | Looks up DNS records over DNS-over-HTTPS, behind a resolver interface that node:dns/promises also matches. |
| @portable/dns-packet | Encodes and decodes DNS messages in wire format. |
| @portable/ip | Parses, formats, compares, and classifies IPv4 and IPv6 addresses and CIDR blocks. |
| @portable/rdap | Looks up who holds a domain name, an IP address, or an AS number over RDAP. |
| @portable/sockets | The WinterTC Sockets API types for TCP and TLS clients, and a connect for Node, Bun, and Deno with the same signature as connect from cloudflare:sockets. |
| Crypto and auth | |
| @portable/web-crypto | Bytes-in, bytes-out helpers over crypto.subtle: digests, HMAC, HKDF, keys from seeds for Ed25519 and X25519, keys from PEM text, and ECDSA signature conversion. |
| @portable/password | Hashes and verifies passwords with PBKDF2 on WebCrypto. |
| @portable/webhooks | Verifies and signs webhooks from Standard Webhooks senders, Stripe, GitHub, Slack, and other HMAC-SHA256 senders. |
| @portable/oauth1 | Signs OAuth 1.0a requests. |
| @portable/random | Unbiased random integers, strings, shuffles, and bytes, and a seeded generator for tests. |
| Realtime | |
| @portable/websockets | A WebSocket that reconnects, with a readable stream of incoming messages and a writable stream for outgoing ones. |
| @portable/sse | Reads, writes, and reconnects server-sent event streams. |
| @portable/rpc | Typed JSON-RPC 2.0 calls over a MessagePort, a window, a WebSocket, or HTTP, with streaming results. |
| @portable/web-push | Sends Web Push notifications with payload encryption and VAPID. |
| Images | |
| @portable/image-size | Reads an image's type and dimensions from its header, from bytes or a stream. |
| @portable/exif | Reads EXIF metadata from JPEG, TIFF, PNG, WebP, HEIC, and AVIF files, and strips it from JPEG, PNG, and WebP. |
| @portable/png | Decodes PNG files to RGBA pixels and encodes pixels as PNG. |
| Concurrency and timing | |
| @portable/queue | Limits how many async tasks run at once, with priorities and cancellation. |
| @portable/timers | Sleep, retry, poll, debounce, and throttle, all cancellable with an AbortSignal. |
| @portable/rate-limit | Rate limiting per key with GCRA, in memory, Workers KV, a Durable Object, or Redis over HTTP. |
| Logging | |
| @portable/log | Logs by namespace and level. Turn namespaces on with debug's DEBUG patterns, and change them without a reload. |
Every package follows the same conventions, so once you've used one, you know how the others work:
- Import a package as a namespace, such as
import * as Csv from '@portable/csv', and callCsv.decodeornew Csv.DecoderStream(). - Bytes go in and come out as
Uint8Array, and a NodeBufferis accepted. Functions that take text also take its bytes. - A streaming transform is a
TransformStreamthat you pass topipeThrough, as withTextDecoderStream. - Every async function and every stream takes a
signal. Aborting it stops thefetchcalls, timers, and nested streams underneath, and the call rejects withsignal.reason, asfetchdoes. - An object that stays open, such as an SMTP client, a queue, or a WebSocket, has a
close()method, asignaloption that closes it, or both. It works withawait usingwhere the runtime hasSymbol.asyncDispose. - Options that take a time, such as
now, accept aDate, or aTemporal.Instant. Times in results areDateobjects. - Errors extend the classes in @portable/errors.
InvalidInputErrormeans the data was malformed, andTypeErrororRangeErrormeans an argument was wrong.
The tests run in Chromium, Firefox, WebKit, Node 24, Node 26, Bun, and Deno. A check of every package also runs in workerd, the Cloudflare Workers runtime. Every package runs in Chrome 116, Firefox 124, and Safari 17.4 or later. Ed25519 signatures, in @portable/web-crypto, @portable/dkim, @portable/arc, and @portable/mail-auth, need Chrome 137 and Firefox 129, and X25519 keys in @portable/web-crypto need Chrome 133 and Firefox 130. @portable/smtp and @portable/sockets need a TCP socket API, so they run in Workers, Node, Bun, and Deno but not in browsers. The packages don't include polyfills, so if you support older browsers, you'll need to add them yourself.
MIT