Skip to content

delaykit/bun-reminders

Repository files navigation

bun-reminders

A Bun server, one SQLite file, zero infra. Durable reminders that survive restarts.

A polished example app for delaykit — built to show the smallest interesting shape of a real reminders feature on a single Bun process.

Live demo: https://bun-reminders.fly.dev

Run

bun install
bun run server.ts

Open http://localhost:3000. Schedule a reminder, watch the countdown, cancel it, or let it fire. Kill the server with Ctrl-C and restart it — the pending reminders are still there.

No bundler, no Postgres, no Docker. The whole app is one TypeScript file plus its dependencies.

What it shows

The standard two-layer shape every real reminders feature has, regardless of which scheduler library is underneath:

Layer Lives in Role
reminders table The same SQLite file Source of truth for user-visible state — message, status, timestamps
delaykit's job tables The same SQLite file Coordination only — when to wake the handler

Domain state ≠ scheduling state. The app's table holds what users see; delaykit holds the timer between events.

The flow:

  1. POST /api/remindersdk.schedule("send-reminder", { key, delay }), then INSERT a row (status=pending). Schedule first so an invalid delay rejects before the table is touched.
  2. The UI reads from reminders and shows a live countdown to scheduledFor.
  3. The timer fires → handler reads the row, returns early if it's no longer pending, otherwise UPDATEs to reminded with fired_at.
  4. DELETE /api/reminders/:id → UPDATE to cancelled, then dk.unschedule(...). The handler's state-check makes the unschedule race harmless.

API

Method Path Body Returns
GET /api/reminders Array of reminder DTOs (most recent 50)
POST /api/reminders { message, delay } The created reminder, status 201
GET /api/reminders/:id The reminder, or 404
DELETE /api/reminders/:id The reminder (now cancelled), or 404

delay is a duration string: 30s, 5m, 2h, 1d, or compound (1h30m).

Reminders are scoped to the caller's session (an opaque sid cookie issued on first contact), so each visitor only sees their own. From curl, persist the cookie with -c/-b:

curl -c jar -b jar -X POST http://localhost:3000/api/reminders \
  -H "content-type: application/json" \
  -d '{"message":"Send onboarding email","delay":"30s"}'

curl -b jar http://localhost:3000/api/reminders

Configure

Env var Default Purpose
DELAYKIT_DB_PATH ./delaykit.db SQLite file path. Set to a persistent volume mount in cloud deployments.
PORT 3000 HTTP port.

Deploy

SQLite needs a persistent volume. If your platform of choice has ephemeral disk, the database is wiped on every redeploy.

Platform Config Approx cost Notes
Fly.io fly.toml ~$4/mo (often waived under $5) What the live demo runs on. Bun first-class.
Railway railway.toml $5/mo minimum Bun first-class. "Deploy to Railway" button supported.
Render render.yaml $7/mo+ for persistent disk Bun via Docker.
Replit .replit Free with sleep Casual demo target; not for production.

A Mac mini under a desk or a cheap VPS ($4-5/mo on Hetzner) sidesteps all of the volume nuance. SQLite works particularly well in those environments.

More patterns

This app demonstrates send-a-reminder. The same one-file Bun + SQLite shape applies to every other delaykit pattern — agent timeouts, expirations, debounces, retries, drip sequences, polling. Browse the catalog at delaykit.dev/patterns.

When not to use this shape

One Bun process means one machine. If you need horizontal scale, multi-region, or a separate worker fleet, run delaykit on Postgres with multiple consumers — same library, same handler API, different store. The point of this example is that you often don't need any of that.

License

MIT.

About

A Bun server, one SQLite file, zero infra. Durable reminders that survive restarts.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors