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.
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
@spy4x/validationarktype validate with one result shape, and form-validation stateimport { 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@spy4x/platformhelpers, command and event bus, cache, API result, tokens, rate limiting
shared, split by entry
@spy4x/platformhelpers, command and event bus, cache, API result, tokens, rate limitingimport { 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@spy4x/serversign-in, accounts, Postgres, Redis, outbox, file storage, env encryption
server
@spy4x/serversign-in, accounts, Postgres, Redis, outbox, file storage, env encryptionimport { 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@spy4x/netSSRF guard, redirect-safe fetch, size-capped body readers, CIDR checks
server
@spy4x/netSSRF guard, redirect-safe fetch, size-capped body readers, CIDR checksimport { 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@spy4x/integrationsntfy and healthchecks.io clients, webhook signature check, retry backoff
server
@spy4x/integrationsntfy and healthchecks.io clients, webhook signature check, retry backoffconst { 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@spy4x/timeIANA time-zone arithmetic on Intl, plain-date math, .ics writer
shared
@spy4x/timeIANA time-zone arithmetic on Intl, plain-date math, .ics writerimport { 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@spy4x/emailSMTP sender, address parsing, HTML mail wrapper, DKIM checker
server
@spy4x/emailSMTP sender, address parsing, HTML mail wrapper, DKIM checkerimport { 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@spy4x/realtimehint-only WebSocket transport: registry, heartbeat, reconnect, cursor sync
both halves
@spy4x/realtimehint-only WebSocket transport: registry, heartbeat, reconnect, cursor syncimport { 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@spy4x/billingcheckout, customer portal and verified webhook events; Stripe adapter
server
@spy4x/billingcheckout, customer portal and verified webhook events; Stripe adapterimport { 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@spy4x/caldavCalDAV client, sync engine and write transport, WebDAV multistatus reader, URL rules
shared
@spy4x/caldavCalDAV client, sync engine and write transport, WebDAV multistatus reader, URL rulesimport { 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@spy4x/tasksthe to-do model of the CalDAV task apps: task, list views, quick-add, reminder runner
shared
@spy4x/tasksthe to-do model of the CalDAV task apps: task, list views, quick-add, reminder runnerimport { 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/tasksWhy ts-libs
Web standards first
fetch, Web Crypto,ReadableStream,Intland ES modules, with Deno as the runtime.Expected failures are data
validatereturns{ error, data },sendresolves withok: falseinstead of throwing, and@spy4x/platformhasResult,okanderrfor your code.Few dependencies
@spy4x/netand@spy4x/timehave 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
Seed, my SaaS starter, imports nine of the packages.
Seed's imports in deno.jsoncMy meeting scheduler mig imports six of them.
mig's imports in deno.jsonThis site imports six of them.
This site's imports in deno.json
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.
Have me build it
Build: MVP
From $8,000,From 3 weeks
More tools
preact-components
server-rendered Preact and Tailwind components.
The design system, the component styling and the original markup are by Eirene. Eirene on GitHub · isorokina.com
Betamig
tiny self-hosted meeting scheduler.
In useSeed
SaaS starter.
WIP
