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

oakstack

v0.6.0

Published

One API key for cron jobs, webhook relay, PDFs, and email. Built for coding agents.

Readme

oakstack

One API key for the boring infrastructure every app needs: scheduled jobs, a webhook relay, PDFs, and email. Built so coding agents (Claude Code, Cursor, and others) can wire it up without anyone reading docs.

npm install oakstack

Get an API key at oakstack.dev/dashboard/api-keys and set it as OAKSTACK_API_KEY.

import { Oakstack } from "oakstack";

const oakstack = new Oakstack(); // reads OAKSTACK_API_KEY

Zero dependencies. Works in Node 18+, Bun, Deno, Vercel Edge, and Cloudflare Workers.

Scheduled jobs: oakstack.clock

Oakstack calls a URL in your app on a cron schedule, retries failures, and logs every run.

const job = await oakstack.clock.jobs.create({
  name: "Nightly cleanup",
  schedule: "0 3 * * *", // minute hour day month weekday
  timezone: "America/Chicago", // default UTC
  url: "https://yourapp.com/api/cron/cleanup",
  body: JSON.stringify({ task: "cleanup" }),
});
// Save job.signingSecret as OAKSTACK_JOB_SECRET to verify calls (below). Each job and
// endpoint has its own secret; with several, name them after what they're for.

await oakstack.clock.jobs.run(job.id); // test it now instead of waiting
const runs = await oakstack.clock.jobs.runs(job.id); // an array, newest first: status, response, error
const jobs = await oakstack.clock.jobs.list(); // an array of jobs, newest first (`{ limit }` up to 100)

await oakstack.clock.jobs.pause(job.id);
await oakstack.clock.jobs.resume(job.id);
await oakstack.clock.jobs.update(job.id, { schedule: "*/15 * * * *" });
await oakstack.clock.jobs.delete(job.id);

Each failed call is retried 3 times (after about 30 seconds, 2 minutes, and 10 minutes). A job that fails 10 runs in a row pauses itself with a pausedReason.

Webhook relay: oakstack.hook

A permanent inbound URL for Stripe, GitHub, or any other sender. Every event is stored, then forwarded to your app with its original headers and body. Delivery is retried for about 11 hours, and any event can be replayed.

const endpoint = await oakstack.hook.endpoints.create({
  name: "Stripe",
  forwardUrl: "https://yourapp.com/api/webhooks/stripe",
});
console.log(endpoint.url); // https://oakstack.dev/h/... : give this to Stripe

const failed = await oakstack.hook.endpoints.events(endpoint.id, { status: "failed" });
const detail = await oakstack.hook.events.get(failed[0].id); // headers, body, every attempt
await oakstack.hook.events.replay(failed[0].id);

await oakstack.hook.endpoints.update(endpoint.id, { status: "paused" }); // holds events, loses none

Your app keeps verifying the sender's own signature (for example Stripe-Signature) exactly as before, because the original headers and the exact body bytes are forwarded unchanged.

PDFs: oakstack.print

HTML or a hosted template (invoice, proposal, report) in, PDF out. Rendered with real Chrome, stored privately, and returned with a download link.

const { example } = await oakstack.print.templates.get("invoice"); // schema + working example
const pdf = await oakstack.print.pdfs.create({
  template: "invoice",
  data: { ...example, invoiceNumber: "INV-1042" },
  filename: "invoice-1042.pdf",
});
pdf.url; // private link, valid for an hour (get(id) gives a fresh one)

const bytes = await oakstack.print.pdfs.download(pdf.id); // Uint8Array

await oakstack.print.pdfs.create({ html: "<h1>Hello</h1>", options: { format: "A4" } });

Files are kept 1 day (Free), 7 days (Builder), or 30 days (Studio). Save the bytes yourself if you need them longer.

Email: oakstack.post

Transactional email from your own domain, with PDFs attached in the same call, plus inbound email delivered to a webhook.

await oakstack.post.domains.create({ domain: "acme.com" }); // returns DNS records; verifies by itself

const email = await oakstack.post.emails.send({
  from: "Acme <[email protected]>",
  to: "[email protected]",
  subject: "Your invoice",
  html: "<p>Thanks for your order.</p>",
  attachments: [{ filename: "invoice.pdf", print: { template: "invoice", data } }],
});
const { status, events } = await oakstack.post.emails.get(email.id); // delivered, bounced, ...

With an ok_test_ key nothing is delivered: emails are checked and logged, and delivery is simulated. Hard bounces and spam complaints are suppressed automatically. Live email to anyone needs a paid plan; oakstack.post.status() says what applies right now.

Verifying requests from Oakstack

Every cron call and forwarded webhook carries an Oakstack-Signature header. Check it with the job's or endpoint's signingSecret:

import { verifySignature } from "oakstack";

export async function POST(req: Request) {
  const body = await req.text(); // the raw body, before parsing
  const ok = await verifySignature(
    process.env.OAKSTACK_JOB_SECRET!,
    req.headers.get("oakstack-signature"),
    body,
  );
  if (!ok) return new Response("bad signature", { status: 401 });
  // ...
  return Response.json({ ok: true });
}

The scheme is the same one Stripe uses: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>." + body>. Signatures older than 5 minutes are rejected.

Usage and limits

const { plan, usage } = await oakstack.usage();
// usage: [{ metric: "clock.jobs", label: "scheduled jobs", used: 2, limit: 3, kind: "count" }, ...]

Plans have hard limits that stop instead of overbilling. Hitting one throws OakstackError with errorName: "usage_limit_reached" and a message that says what to do next.

Errors and retries

  • Every API error throws OakstackError, with statusCode, errorName (machine-readable), and a message that says what to fix.
  • Network errors, 5xx responses, and rate limits are retried automatically, twice by default (maxRetries).
  • Every write carries an Idempotency-Key, so a retry can never create a duplicate. Pass your own to make a call safe across process restarts: oakstack.clock.jobs.create(params, { idempotencyKey: "setup-nightly-v1" }).

Options

new Oakstack({
  apiKey: "ok_live_...", // default: OAKSTACK_API_KEY
  baseUrl: "https://oakstack.dev", // default: OAKSTACK_BASE_URL, then this
  maxRetries: 2,
  timeoutMs: 30_000,
});

For coding agents

  • MCP server: npx oakstack-mcp, or the hosted server at https://oakstack.dev/mcp.
  • Full docs as plain text: oakstack.dev/llms.txt.

MIT licensed.