Skip to main content
Photo of Anton ShubinAnton ShubinContact

ts-libs tested TypeScript building blocks for web-standards back ends Ready

Small TypeScript libraries on web standards, for the server and the browser.

The clip shows one meeting in each person's own time zone, money kept in whole cents, and form input checked against a schema.

Three packages, run for real: one meeting shown in each person's own time zone, money kept in whole cents with each currency's decimals, and what a user typed checked against a schema, with one plain message per field.

Run the example

# arktype at the exact version @spy4x/validation pins, so both see one copy of its types
deno add jsr:@spy4x/validation jsr:@spy4x/time jsr:@spy4x/email npm:arktype@2.2.3
# save the README's example as main.ts, then:
SMTP_PASSWORD=not-real deno run --allow-net --allow-env --allow-sys main.ts
curl -d '{"email":"nope","date":"2026-08-28","time":"10:00"}' localhost:8000
# {"email":[{"code":"pattern","path":"email","message":"must be an email address (was \"nope\")"}]}

main.ts is the example at the top of the README.

Pick a package

11 packages on JSR, each installed on its own. Each opens to an example from its own README.

@spy4x/validationarktype validate with one result shape, and form-validation state

shared
import { type } from "arktype"
import { isValid, validate, validateSchema } from "@spy4x/validation"

const userSchema = type({ name: "1 <= string <= 10", joinedAt: "string.date.iso.parse" })

const { error, data } = validate(userSchema, input)
if (error) return error.description
await save(data) // `data` is parsed, so `joinedAt` is a `Date`
deno add jsr:@spy4x/validation

On JSR: @spy4x/validation

@spy4x/platformhelpers, command and event bus, cache, API result, tokens, rate limiting

shared, split by entry
import { type } from "arktype"
import { UUID_V7_PATTERN, uuidV7 } from "@spy4x/platform/universal/uuid"

const noteSchema = type({ id: UUID_V7_PATTERN, text: "string" })
const note = noteSchema.assert({ id: uuidV7(), text: "milk" })
deno add jsr:@spy4x/platform

On JSR: @spy4x/platform

@spy4x/serversign-in, accounts, Postgres, Redis, outbox, file storage, env encryption

server
import { createStorage } from "@spy4x/server/storage"

const storage = await createStorage() // reads Deno.env, returns undefined if unconfigured
await storage?.upload("media/photo.png", bytes)
deno add jsr:@spy4x/server

On JSR: @spy4x/server

@spy4x/netSSRF guard, redirect-safe fetch, size-capped body readers, CIDR checks

server
import { validatePublicUrl } from "@spy4x/net/url-policy"

async function fetchUserSuppliedUrl(userInput: string) {
  const safeUrl = await validatePublicUrl(userInput) // throws on a private/loopback/link-local target
  return fetch(safeUrl)
}
deno add jsr:@spy4x/net

On JSR: @spy4x/net

@spy4x/integrationsntfy and healthchecks.io clients, webhook signature check, retry backoff

server
const { publicKey, keys } = await generateVapidKeyPair() // once; store `keys`, serve `publicKey`
const sender = await createWebPushSender({
  vapidKeys: keys,
  subject: "mailto:ops@example.com",
  store,
})
const result = await sender.send(user.id, { title: "Backup done", body: null, url: null })
deno add jsr:@spy4x/integrations

On JSR: @spy4x/integrations

@spy4x/timeIANA time-zone arithmetic on Intl, plain-date math, .ics writer

shared
import { formatDateTimeLong, zonedDateTime } from "@spy4x/time/tz"

// A stored booking: a wall clock, the zone it was made in, the guest's zone.
const instant = zonedDateTime("2026-08-28", "10:00", "Europe/Berlin")
formatDateTimeLong("2026-08-28", "10:00", "Europe/Berlin") // "Friday, 28 August 2026 at 10:00"
deno add jsr:@spy4x/time

On JSR: @spy4x/time

@spy4x/emailSMTP sender, address parsing, HTML mail wrapper, DKIM checker

server
import { createSmtpSender } from "@spy4x/email/smtp"
const sender = createSmtpSender({
  host: "smtp.example.com",
  port: 587,
  user: "user@example.com",
  pass: "test-password-not-real",
  from: "Booking <booking@example.com>",
})

const result = await sender.send({
  to: ["guest@example.com", "host@example.com"],
  subject: "Booking confirmed",
  text: "Your meeting is booked.",
})
if (!result.ok) console.error(result.error, result.rejected)
deno add jsr:@spy4x/email

On JSR: @spy4x/email

@spy4x/realtimehint-only WebSocket transport: registry, heartbeat, reconnect, cursor sync

both halves
import { AggregateNotifier, ConnectionRegistry, createSystemClock } from "@spy4x/realtime"

const clock = createSystemClock()
const registry = new ConnectionRegistry({
  clock,
  maxConnectionsPerUser: 20,
})

// From the upgrade handler: authenticate once at the upgrade, then attach. `attach` returns `null`
// — and has already closed the socket — once the user is at `maxConnectionsPerUser`.
const handle = registry.attach(userId, adaptSocket(await upgrade(request)))
if (!handle) logger.warn("refused a socket: user already at the connection cap", { userId })
deno add jsr:@spy4x/realtime

On JSR: @spy4x/realtime

@spy4x/billingcheckout, customer portal and verified webhook events; Stripe adapter

server
import { BillingEventType, createStripeBilling } from "@spy4x/billing"

const billing = createStripeBilling({
  secretKey, // read from your own config; the library never reads the environment
  webhookSecret,
  plans: [{ planId: "pro", priceId: "price_…" }],
})

// Checkout: redirect the customer to `result.value.url`.
const result = await billing.createCheckout({
  planId: "pro",
  successUrl: "https://app.example.com/billing/done",
  cancelUrl: "https://app.example.com/billing",
  reference: account.id, // comes back as `subscription.reference` on every event
})
deno add jsr:@spy4x/billing

On JSR: @spy4x/billing

@spy4x/caldavCalDAV client, sync engine and write transport, WebDAV multistatus reader, URL rules

shared
import { CalDavErrorCode, createCalDavClient } from "@spy4x/caldav"

const client = createCalDavClient({
  serverUrl: "https://dav.example.com/", // the bare host or `…/dav/cal/` both work
  auth: { username: "user@example.com", password: "…" },
})

const found = await client.discover() // .well-known → current-user-principal → calendar-home-set
if (!found.success) throw new Error(found.error.message)
const calendars = await client.listCalendars(found.output.homeUrls[0])
// Each: url, displayName, components, color?, ctag?, syncToken?
deno add jsr:@spy4x/caldav

On JSR: @spy4x/caldav

@spy4x/tasksthe to-do model of the CalDAV task apps: task, list views, quick-add, reminder runner

shared
import { parseTask } from "@spy4x/tasks/model"
import { SortMode, sortTasks, todayView } from "@spy4x/tasks/view"
import { parseQuickAdd } from "@spy4x/tasks/quick-add"

const zone = "Europe/Berlin"
const tasks = resources.map(parseTask).flatMap((result) => result.success ? [result.output] : [])

const byDue = sortTasks(tasks, SortMode.Due, zone)
const { overdue, today } = todayView(tasks, new Date(), zone)

const typed = parseQuickAdd("Call Anna @phone #work tomorrow 3pm !high", {
  now: new Date(),
  timeZone: zone,
})
typed.title // "Call Anna"
deno add jsr:@spy4x/tasks

On JSR: @spy4x/tasks

Why ts-libs

  • Web standards first

    fetch, Web Crypto, ReadableStream, Intl and ES modules, with Deno as the runtime.

  • Expected failures are data

    validate returns { error, data }, send resolves with ok: false instead of throwing, and @spy4x/platform has Result, ok and err for your code.

  • Few dependencies

    @spy4x/net and @spy4x/time have none. Shared ones (arktype, Hono, Postgres) are pinned to exact versions.

  • Tested against the real thing

    Unit tests use fakes; an integration tier runs against a real Postgres, S3-compatible store, mail server and Redis, and fails instead of skipping. CI runs it on Postgres 18 and 17; the local containers run 18.

Where I use it

Where it stands

Works today

  • Every package is tested on Deno.
  • @spy4x/time, @spy4x/validation and @spy4x/caldav use no Deno.* API, so they also run in a browser, untested there.

Not built yet

  • Tested Node.js support.

The rest is on the open issues.

Use it if

  • You build on Deno or ship a browser bundle and want small, typed primitives without a framework.

Skip it if

  • You need tested Node.js support or UI components (those live in preact-components).

Two ways to use it

Use it yourself

The code is on GitHub under the MIT licence.

Run the example

# arktype at the exact version @spy4x/validation pins, so both see one copy of its types
deno add jsr:@spy4x/validation jsr:@spy4x/time jsr:@spy4x/email npm:arktype@2.2.3
# save the README's example as main.ts, then:
SMTP_PASSWORD=not-real deno run --allow-net --allow-env --allow-sys main.ts
curl -d '{"email":"nope","date":"2026-08-28","time":"10:00"}' localhost:8000
# {"email":[{"code":"pattern","path":"email","message":"must be an email address (was \"nope\")"}]}

main.ts is the example at the top of the README.

View on GitHub

Have me build it

Build: MVP

From $8,000,From 3 weeks

More tools

All tools