In a video game, a collision engine answers one question: do these two objects overlap?
Scheduling is the same problem in one dimension. A booking is a segment range on the number line of unix time. Two bookings conflict when their segments collide. That's the entire model.
──────[ Flight ]───────────────────────────────────────
────────────[ Dentist ]────────────────────────────────
──────────────────────[ Hotel ]────────────────────────
Everything in scheduling is just an extension of this primitive:
| Concept | What it means |
|---|---|
| Booking | A segment placed on the timeline |
| Conflict | Two segments that overlap |
| Capacity | How many segments can stack on the same point |
| Buffer | A forced gap after each segment |
| Rule | A region of the line marked open or closed |
| Hold | A segment with a self-destruct timer |
| Availability | The gaps between everything already placed |
deltat is a database built entirely around this primitive. It operates purely on raw i64 Unix milliseconds. No time zones, no calendars, no date formats. No pricing, no user profiles, no business data. Just segments on a number line.
Scheduling has historically been painful because it was forced into relational databases that weren't designed for it: time intervals, collision detection, and availability queries bolted onto tables built for storing rows. deltat inverts that. Time isn't a column in someone else's schema. It's the entire database.
Human-readable time is a display concern that belongs in the client. The database just sees numbers on a line, which means it will work unchanged as long as 64-bit integers exist.
A single Rust binary that speaks PostgreSQL. No Postgres underneath: it's a purpose-built in-memory engine backed by an append-only WAL. Connect with any Postgres client.
The PostgreSQL wire protocol is the current transport; a v2 framed protocol (+ HTTP/MCP) is planned (see
docs/REQUIREMENTS.md).
- Sub-millisecond availability queries
- Hierarchical resources (Flight → Cabin → Seat)
- Capacity, buffers, holds with auto-expiry
- Atomic batch bookings (all-or-nothing)
- Real-time LISTEN/NOTIFY events
deltat is the reference implementation of TAP, the Time Allocation Protocol: the open standard for scheduling, booking, and availability. Working in TypeScript? TAP's SDK, @open-deltat/client, lets you write resources, holds, and availability instead of SQL.
docker run -p 5433:5433 -e DELTAT_PASSWORD=secret -v deltat-data:/data \
ghcr.io/open-deltat/deltat:0.5.0Or install the crate, which needs a Rust toolchain:
cargo install deltat
DELTAT_PASSWORD=secret deltatThere is a docker-compose.yml in the repo for a persistent single-node setup, and
cargo install --git https://github.com/open-deltat/deltat.git builds an unreleased main.
Without DELTAT_PASSWORD, deltat generates a random password and prints it once at startup.
Anyone holding the password has full read/write access to every tenant (see DELTAT_TENANT_PASSWORDS
below to scope credentials per tenant), and auth is cleartext unless TLS is configured, so on
anything but localhost set a strong password and DELTAT_TLS_CERT/DELTAT_TLS_KEY.
Connect on port 5433:
psql -h localhost -p 5433 -U deltatAnything bookable. Resources form a tree: children inherit availability from their parent.
INSERT INTO resources (id, parent_id, name, capacity, buffer_after)
VALUES ('01J...', NULL, 'Flight AA-100', 1, 2700000);
INSERT INTO resources (id, parent_id, name)
VALUES ('01J...', '01J_FLIGHT...', 'Seat 1A');SELECT * FROM resources WHERE parent_id IS NULL; -- roots
SELECT * FROM resources WHERE parent_id = '01J...'; -- childrenWhen a resource is open or closed.
-- Available 9am-5pm
INSERT INTO rules (id, resource_id, start, "end", blocking)
VALUES ('01J...', '01J...', 1706000000000, 1706028800000, false);
-- Blocked for maintenance
INSERT INTO rules (id, resource_id, start, "end", blocking)
VALUES ('01J...', '01J...', 1706028800000, 1706032400000, true);Place a segment on the line.
INSERT INTO bookings (id, resource_id, start, "end", label)
VALUES ('01J...', '01J...', 1706000000000, 1706003600000, 'Team Meeting');
-- Batch: all or nothing
INSERT INTO bookings (id, resource_id, start, "end")
VALUES ('01J_A...', '01J_SEAT1...', 1706000000000, 1706003600000),
('01J_B...', '01J_SEAT2...', 1706000000000, 1706003600000);
DELETE FROM bookings WHERE id = '01J...';Temporary segments that auto-expire. expires_at is a request: the server clamps it to its own
clock plus DELTAT_MAX_HOLD_TTL_MS (default 1 hour), so a skewed client clock can never squat a
span indefinitely. Read the hold back with SELECT * FROM holds to see the effective expiry.
-- Hold for 15 minutes
INSERT INTO holds (id, resource_id, start, "end", expires_at)
VALUES ('01J...', '01J...', 1706000000000, 1706003600000, 1706000900000);
DELETE FROM holds WHERE id = '01J...';
-- Commit the hold: atomically convert it into a booking on the held span.
-- One lock, one WAL record; there is no release-then-rebook gap for a
-- competing booker to steal, and a crash keeps all of it or none. label is optional.
UPDATE holds SET booking_id = '01J...', label = 'seat 14F' WHERE id = '01J...';
-- Hold a kit across resources: every row is held, or none is.
INSERT INTO holds (id, resource_id, start, "end", expires_at)
VALUES ('01J_H1...', '01J_BODY...', 1706000000000, 1706003600000, 1706000900000),
('01J_H2...', '01J_LENS...', 1706000000000, 1706003600000, 1706000900000);
-- Book every hold at once: each booking takes its hold's resource and span.
-- All of them are booked, or none are.
INSERT INTO bookings (id, hold_id, label)
VALUES ('01J_B1...', '01J_H1...', 'body'),
('01J_B2...', '01J_H2...', 'lens');Find the gaps.
SELECT * FROM availability
WHERE resource_id = '01J...'
AND start >= 1706000000000
AND "end" <= 1706086400000;
-- Minimum 1-hour slots
SELECT * FROM availability
WHERE resource_id = '01J...'
AND start >= 1706000000000
AND "end" <= 1706086400000
AND min_duration = 3600000;
-- Multi-resource: when are all 3 free?
SELECT * FROM availability
WHERE resource_id IN ('01J_A...', '01J_B...', '01J_C...')
AND start >= 1706000000000
AND "end" <= 1706086400000;
-- When are at least 2 of 3 free?
SELECT * FROM availability
WHERE resource_id IN ('01J_A...', '01J_B...', '01J_C...')
AND start >= 1706000000000
AND "end" <= 1706086400000
AND min_available = 2;Real-time notifications via LISTEN/NOTIFY.
LISTEN resource_01J...;
UNLISTEN resource_01J...;Your data is the WAL under DELTAT_DATA_DIR (/data in the image). Upgrading replaces the
binary and leaves the log alone:
docker compose pull && docker compose up -dThe compose file pins a version rather than tracking latest, so a restart never moves you to
a release you did not choose. Set DELTAT_VERSION to pick another one.
The WAL carries a format version, and a binary refuses to open a log written by a newer deltat rather than misread it. Logs written before 0.3.0 have no header, are still read as they are, and pick one up the first time they are compacted. Downgrading is the case to be careful with: an older binary will not open a log a newer one has stamped, so keep a copy of the directory before stepping backwards.
Before 1.0 the storage format can still change. Every such change lands in the changelog with what it means for existing data.
All times are Unix milliseconds. Intervals are half-open [start, end), so adjacent segments don't collide.
| Env var | Default | Description |
|---|---|---|
DELTAT_PORT |
5433 |
Listen port |
DELTAT_BIND |
0.0.0.0 |
Bind address |
DELTAT_DATA_DIR |
./data |
WAL storage directory |
DELTAT_PASSWORD |
generated | Connection password. Unset means a random one is generated and printed once at startup |
DELTAT_TENANT_PASSWORDS |
unset | Per-tenant credentials as comma-separated tenant:password pairs (passwords may contain colons, not commas). A tenant listed here accepts only its own password; unlisted tenants use DELTAT_PASSWORD |
DELTAT_MAX_CONNECTIONS |
256 |
Concurrent connection cap |
DELTAT_COMPACT_THRESHOLD |
1000 |
WAL appends before a compaction runs |
DELTAT_GC_RETENTION_MS |
604800000 |
Age (7 days) past which finished bookings and expired holds are collected |
DELTAT_MAX_HOLD_TTL_MS |
3600000 |
Ceiling (1 hour) on hold lifetime: a hold's requested expires_at is clamped to server now + this |
DELTAT_METRICS_PORT |
unset | Prometheus /metrics port; metrics are off when unset (see docs/OBSERVABILITY.md) |
DELTAT_LOG_FORMAT |
text |
Log output format. json (case-insensitive) emits newline-delimited JSON for log collectors; anything else means human-readable text |
DELTAT_SLOW_QUERY_MS |
0 |
Statements taking at least this many ms are logged at warn and counted. 0 disables the slow-query log |
DELTAT_COUNTER_OFFER |
3 |
How many alternative spans a refused hold or booking carries in its DETAIL field. 0 disables counter-offers |
RUST_LOG |
info |
Log level filter in tracing EnvFilter syntax, e.g. deltat=debug. Unset or unparseable falls back to info |
DELTAT_TLS_CERT |
unset | PEM certificate path; TLS is off unless both cert and key are set |
DELTAT_TLS_KEY |
unset | PEM private key path; TLS is off unless both cert and key are set |
src/
engine/ Availability computation, conflict detection, state mutations, queries
model.rs Core types: Span, Interval, ResourceState, Event
command.rs Transport-neutral command vocabulary (the v2 protocol target)
sql.rs SQL parser, producing commands
wire.rs pgwire protocol (simple + extended query)
wal.rs Append-only write-ahead log with group commit
tenant.rs Per-tenant engine + WAL, lazily created
notify.rs LISTEN/NOTIFY broadcast
reaper.rs Background hold expiration, WAL compaction, interval GC
clock.rs Injected clock seam (deterministic in tests)
auth.rs Cleartext password authentication
tls.rs Optional rustls TLS
observability.rs Prometheus metrics (opt-in)
limits.rs Hard caps (resources, intervals, query windows)
main.rs TCP listener and configuration
TypeScript: @open-deltat/client
The tap repo includes a Next.js demo app with interactive examples spanning seat maps (airline, theater, stadium), multi-resource availability, recurring schedules, capacity pools, and hold-to-book flows.
