Skip to content
Zeitlines

Self-hosting Zeitlines

Node.js puts your timelines on screen from a filesystem or a database. Local JSON and Markdown sources stay editable on your machine. PostgreSQL keeps shared plans editable after deployment and available to remote MCP clients.

Requirements

Option 1: Filesystem

git clone https://github.com/zeitlines/zeitlines.git
cd zeitlines
npm install
npm run dev

A timeline here is a file you own: one *.json in data/, named after the file, or a folder holding one Markdown note per item with a timeline.json beside them. Example timelines are committed, so there is something on screen before you have written anything.

The dev server can write the file it read, so the timeline is editable there: drag a bar and the JSON, or the note's frontmatter, changes on disk. A deployment serves it read-only, because no deployment writes files, neither a static host nor npm start. Edits then happen where the data is, from an editor, from Git or through the MCP server, and reach the site with the next build.

This is the path the project's own continuous integration runs, so it stays working for somebody who has just cloned the repository and has no credentials at all.

A folder you already own, including an Obsidian vault

data/ inside the checkout is the default and not the limit. TIMELINES_LOCAL_ROOT takes an absolute path (~ is expanded) and moves the source root anywhere on disk, which is what lets a timeline be a directory that exists for its own reasons: a notes folder, an Obsidian vault, the folder somebody is writing a novel in.

export TIMELINES_LOCAL_ROOT=~/Documents/Vault
npm run dev

Every folder under that root carrying a timeline.json becomes a timeline, and each Markdown note in it becomes an item, its frontmatter supplying the date, the group, the status and whatever else you already write. That file may be {}: its job is to mark which folder you meant. Folders without one are passed over, so a vault gives you the timelines you asked for rather than one per directory.

Nothing is imported and nothing is copied, and the tools' own directories (.obsidian, .trash, .git) are left out. The notes stay the thing you edit; the timeline is a way of looking at them. While npm run dev runs it goes both ways: a note you save in Obsidian reaches the timeline, an edit in the timeline reaches the note, so the two windows can stay open beside each other.

The folder declares how it is read

A folder of meeting notes and a folder holding a novel answer the same questions differently, so the answers live in that folder's timeline.json rather than in an environment variable. Moving or copying the folder takes them along.

{
  "name": "Book one",
  "scan": {
    "dateFields": [],
    "groupFromFolder": true,
    "linkEdges": true
  }
}
KeyWhat it decides
dateFields Frontmatter keys tried in order for an item's start, by default date, scheduled, created. Set it to [] for a vault that stamps created on every note, which would otherwise put every item on the day it was typed.
filenameDatePatterns Date patterns in the filename, tried when the frontmatter carries none, so a daily note called 2026-01-15-standup.md lands on the right day.
groupFromFolder Take an item's group from the subfolder it sits in when its frontmatter names none. Off by default.
linkEdges Read the [[wikilinks]] you already wrote as relations, which become the Gantt chart's arrows and the graph's edges. Off by default, because in most folders a link is a reference rather than a dependency.

How each of them resolves in detail is in the local-sources chapter.

Reading is not writing

TIMELINES_LOCAL_READONLY=1 refuses every write for a whole instance, and the interface stops offering the editing controls. That is the setting for a folder you write prose in, where a mis-drag must not reach a note.

Where writes are allowed, an edit changes the one frontmatter key it has to and leaves the rest of the file exactly as it was, body included. Deleting an item moves the note to .trash/, Obsidian's own convention, rather than deleting a file the tool did not create.

Option 2: Database

Option 1 is editable where the files are, which is your own machine. A database is what carries that to a URL: the timeline stays editable after it is deployed, for everybody who opens it, and their edits reach each other while they work. It is additive, so a filesystem timeline keeps working beside a database one on the same instance.

It runs on PostgreSQL. Both drivers below speak Postgres and Supabase is Postgres, so a managed project and a container you started yourself are the same timeline as far as the application is concerned.

# 1. a database: any Postgres will do
docker run -d -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:16

# 2. point at it
export TIMELINES_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/postgres

# 3. schema
npm run db:migrate

# 4. run
npm run dev

The migration runner needs no vendor CLI, and the dev command refuses to start while a migration is pending, so the application never quietly talks to an older schema than the code expects.

To put existing data in, npm run db:import -- your-timeline reads data/your-timeline.json straight into the database.

Two drivers, one seam

DriverSelected bySuits
postgres.js, native TCP TIMELINES_DATABASE_URL Any PostgreSQL you run yourself
supabase-js, HTTP TIMELINES_SUPABASE_URL + service key A Supabase project, and the hosted deploy

Both sit behind one interface, so nothing above them knows which is in use. A Supabase-backed install needs a direct connection for schema work on top, which is what TIMELINES_MIGRATE_DATABASE_URL is for; it does not change which driver serves the application.

Live updates

Other people's edits arrive either over a websocket, when the database offers one, or by polling a small watermark endpoint. Set TIMELINES_DB_LIVE=poll for the second, which is the setting a plain self-hosted Postgres wants. What the person looking at the timeline sees is the same either way.

When the database is unreachable

The page says so and shows nothing. There is deliberately no cached snapshot to fall back to, because a stale copy of a plan is indistinguishable from a current one and gets mistaken for it. A broken credential therefore produces an error message rather than a plan that quietly stopped updating three weeks ago.

Configuration

Server-side variables are read from the environment first, then .env.local, then any file you name explicitly, so credentials can live outside the repository without any of them being read by default. A fresh checkout reads nothing outside its own folder.

VariablePurpose
TIMELINES_LOCAL_ROOTWhere local sources are discovered, default data/. An absolute path points the instance at a notes folder or Obsidian vault you already own
TIMELINES_LOCAL_READONLYRefuses every write to a local source, even where the runtime could perform one
TIMELINES_SOURCES_SUBDIRScope which timelines a deployment serves
TIMELINES_DATABASE_URLPostgres connection string; selects the native driver
TIMELINES_MIGRATE_DATABASE_URLConnection used only for schema work
TIMELINES_SUPABASE_URL / …_SERVICE_KEYSupabase project and its service-role key
TIMELINES_DB_LIVEpoll for live updates without a realtime service
AUTH_REQUIRED / ALLOWED_EMAIL_DOMAINSThe sign-in gate and the domains allowed through it
VITE_JIRA_BASE_URLBase URL for JIRA links; empty renders keys as plain text

Deploying it

npm run build produces a static site. The repository also carries a Netlify configuration with three edge functions, the auth gate, the timelines API and the public pricing API, so a deployment can sit behind Google sign-in with an allowed-domain list while the pricing endpoint stays open.

Nothing forces that host. A build with no database configured is static files that any web server will serve, and npm start serves the same build plus the API from one Node process. The edge functions are what add the sign-in gate on Netlify.

Related

Frequently asked questions

How do I self-host a Gantt chart tool?
Clone the Zeitlines repository, run npm install and npm run dev. That serves every timeline in the data folder with no database and no configuration. A deployment serves those files read-only, so a database is what keeps a timeline editable once it is on a URL.
Can I point Zeitlines at my Obsidian vault without copying it?
Yes, and that is the intended way to use it. Point TIMELINES_LOCAL_ROOT at the vault, and every folder in it holding a timeline.json becomes a timeline whose items are its Markdown notes. There is no import step and no second copy of your notes.
Will Zeitlines change my notes?
Only if you edit the timeline yourself, on a machine that has the vault. An edit then changes the one frontmatter key it has to and leaves the rest of the file exactly as it was; deleting an item moves the note to .trash/. TIMELINES_LOCAL_READONLY=1 refuses every write for the whole instance, which is the setting for a folder you write prose in.
What are the requirements?
Node.js 22 or newer. Node 22 is a hard floor rather than a preference: the test runner relies on glob expansion that older versions do not do. PostgreSQL is optional, and needed only for a timeline that stays editable in a deployment.
Does Zeitlines require Supabase?
No. Supabase is one of two interchangeable database drivers. The other talks to any PostgreSQL over a plain connection string, which is what a self-hosted install typically uses. The application code never sees which one is in play.
Can I get live updates without Supabase Realtime?
Yes. Run the server with TIMELINES_DB_LIVE=poll and the client polls a cheap watermark endpoint instead of holding a websocket open. Same effect for the person looking at the timeline, no realtime service required.
Can I put the deployment behind a login?
Yes, on Netlify: set AUTH_REQUIRED=true and list the e-mail domains allowed to sign in. An empty list means nobody passes, which is the safe direction to fail in. The public pricing endpoint stays reachable, by design.
What happens if the database is unreachable?
The page says so. There is deliberately no cached or committed snapshot to fall back to, because a stale copy of a plan is indistinguishable from a current one and reliably gets mistaken for it.