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
bun install
bun run server.tsOpen 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.
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:
POST /api/reminders→dk.schedule("send-reminder", { key, delay }), then INSERT a row (status=pending). Schedule first so an invalid delay rejects before the table is touched.- The UI reads from
remindersand shows a live countdown toscheduledFor. - The timer fires → handler reads the row, returns early if it's no longer
pending, otherwise UPDATEs toremindedwithfired_at. DELETE /api/reminders/:id→ UPDATE tocancelled, thendk.unschedule(...). The handler's state-check makes the unschedule race harmless.
| 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| 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. |
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.
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.
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.
MIT.