Running it yourself

Ontoplano is a SvelteKit server over one SQLite file, with two small timers beside it: one asks the app to deliver reminders that have fallen due, the other to send the weekly review mail. The timers hold no data and no logic — each is one POST to the app's own job endpoint — and which of them a route installs is under the two jobs.

It reaches the network in three places, and you decide on all three: SMTP, if you want confirmation and password-reset mail; the browser vendor's push service, which is how a reminder arrives on a phone with the app closed; and whatever calendar feed or webhook address you type into it yourself. There is no telemetry, no licence check and no update ping.

What it needs

Route What has to be there
.deb, .rpm Linux with systemd. The Node runtime is inside the package.
Arch recipe Linux with systemd, and the distribution's nodejs, 20 or newer.
Docker Docker. Nothing else — no systemd, no Node on the host.
From source Node 22 and yarn.

Optional on any of them: a reverse proxy, which is where TLS belongs, and an SMTP account. Without SMTP, confirmation and password-reset links are written to the log instead.

Building it yourself wants memory — make build gives Node a 2GB heap, and vite build uses most of it. On a VPS with less, build on another machine and copy build/ across.

The quickest start: a package

One command, a service that starts on boot, and upgrades through the package manager you already use.

Debian, Ubuntu, Mint, Pop!_OS, Raspberry Pi OS

curl -LO https://github.com/ontoplano/ontoplano/releases/download/v0.186.10/ontoplano_0.186.10_amd64.deb
sudo apt install ./ontoplano_0.186.10_amd64.deb

Fedora, RHEL and its rebuilds, openSUSE

curl -LO https://github.com/ontoplano/ontoplano/releases/download/v0.186.10/ontoplano-0.186.10-1.x86_64.rpm
sudo dnf install ./ontoplano-0.186.10-1.x86_64.rpm     # or: sudo zypper install ./ontoplano-0.186.10-1.x86_64.rpm

Arch, Manjaro, EndeavourOS — a recipe, built on your machine. It is not on the AUR yet, so the two files it needs come from the release itself:

mkdir ontoplano && cd ontoplano
curl -LO https://github.com/ontoplano/ontoplano/releases/download/v0.186.10/PKGBUILD
curl -LO https://github.com/ontoplano/ontoplano/releases/download/v0.186.10/ontoplano.install
makepkg -si

Checking what you downloaded

Every release carries a SHA256SUMS covering each file attached to it. In the directory you downloaded into:

curl -LO https://github.com/ontoplano/ontoplano/releases/download/v0.186.10/SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS

--ignore-missing checks the files you took rather than complaining about the ones you did not.

The recipe is covered twice over: SHA256SUMS covers the PKGBUILD you downloaded, and that file names the release's own source tarball and carries its checksum, so makepkg refuses to build anything else.

A checksum served from the same page as the download proves the file arrived whole. It is not a signature. The Android package is signed, and Android itself refuses an update signed by a different key.

Then, on any of them:

sudo ontoplano config                        # set ORIGIN to the address you will type
sudo systemctl enable --now ontoplano

That is the whole installation. It listens on 127.0.0.1:1493, so nothing off the machine reaches it until you put a reverse proxy in front, which is where TLS belongs.

What the package sets up

What Where
ontoplano a system account with no shell and no password, which the service runs as
/etc/ontoplano/ontoplano.env your settings — the origin, mail, who may register
/etc/ontoplano/secret.env the session secret, generated once on install
/var/lib/ontoplano/ontoplano.db the database. This file is your data.
ontoplano.service a system unit, enabled by you, restarted on failure

It is a system service rather than a user one because a user service stops at logout unless lingering is enabled, and a package cannot enable one on anybody's behalf. The service account reaches /var/lib/ontoplano and nothing else on the machine; systemctl cat ontoplano shows the restrictions. On a machine you develop on, a user unit is the right answer, and that is what make install-service sets up.

Upgrading is the same command over the newer file — apt install ./ontoplano_*_amd64.deb, dnf install ./ontoplano-*.x86_64.rpm, or makepkg -si from the new release's recipe. The database is migrated before the new version starts, your settings file is left as you edited it, and the session secret is not regenerated, so everybody stays signed in.

Removing the package leaves /var/lib/ontoplano behind, because it is your data rather than the package's. apt purge ontoplano takes it too.

The commands it installs

ontoplano status                # is it running, and since when
ontoplano start|stop|restart
ontoplano logs                  # follow the journal
sudo ontoplano config           # open the settings file
sudo ontoplano migrate          # bring the database up to the installed version
ontoplano version

Why the .deb and the .rpm are large

They carry their own Node, which is most of their size. The database driver is a native module compiled against one Node ABI, and these distributions ship several Node versions between them — a package built against one of them would install cleanly everywhere and start nowhere. The Arch package bundles nothing, because it is built on the machine that will run it.

Or with Docker

One container, one volume. Migrations run at startup, so there is no first-run step.

docker run -d --name ontoplano -p 1493:1493 \
  -v ontoplano-data:/data \
  -e ORIGIN=http://localhost:1493 \
  -e BETTER_AUTH_SECRET="$(openssl rand -base64 32)" \
  ontoplano/ontoplano:latest

Or with compose:

curl -O https://raw.githubusercontent.com/ontoplano/ontoplano/master/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/ontoplano/ontoplano/master/.env.example
# set ORIGIN and BETTER_AUTH_SECRET in .env
docker compose up -d

docs/DOCKER.md has the reverse proxy, upgrading and backups.

On Windows

There is no Windows installer yet: no MSI, no winget package, no service wrapper. Docker Desktop works, and so does WSL2 — inside WSL the .deb above installs as it does on Ubuntu, which is the shortest route from Windows today.

An installer is a self-contained piece of work if you want to write one: the app is a Node server over one SQLite file, so what is missing is the packaging around it. CONTRIBUTING.md says how the repository works.

From the source

git clone https://github.com/ontoplano/ontoplano.git
cd ontoplano
yarn
cp .env.example .env      # set ORIGIN and BETTER_AUTH_SECRET
yarn db:migrate           # create the database
yarn dev                  # http://localhost:1493

For a box you keep, without Docker:

make install-service      # build, migrate, install and start a systemd user unit
make deploy-local         # after changes: rebuild, migrate, redeploy, restart

That unit runs as your own user, keeps running after logout, and snapshots the database before every migration.

The first account

Register at /login. The first account is allowed whatever the registration setting says, and it owns the instance. The first screen asks for your timezone and which day your week starts on, and offers a starter week.

Who may register

Closed by default. After the first account, /settings/instance decides: closed, by invitation, or open. An invitation is a code that works once, and that page makes and revokes them. Keep it closed or on invitation for an instance reachable from the internet.

Where your data lives

Nothing is inside the repository, and everything is created on first run.

Path What
~/.local/share/ontoplano/ontoplano.db the database — this one file is your data
~/.config/ontoplano/config.toml bind address, port, database path, who may register
~/.config/ontoplano/env ORIGIN, BETTER_AUTH_SECRET, SMTP, backups

Installed from a package, the same three live under the system paths instead — /var/lib/ontoplano/ontoplano.db, and /etc/ontoplano/ for both config files. Docker puts all of them in the volume mounted at /data.

Back up the first one and you have backed up everything. sqlite3 … .backup works on a running instance, a file copy works while it is stopped, and docs/BACKUP.md has a Litestream setup for continuous replication.

The two jobs

Settings → Instance lists both and when each last ran.

Reminders. The app keeps its own timer, so a reminder fires on the second with nothing else installed. The packages and make install-service also install a minute timer that asks /api/jobs/reminders; it covers the case where the app was restarted across the moment a reminder fell due. The Docker image makes the same request from inside the container.

The weekly review mail. On Monday, one message saying how much of last week you planned and did, linking to /calendar/review. It goes to accounts with a confirmed address, and every message carries a link that turns it off without signing in. It needs SMTP, so it is not installed by default: from source, make install-mail-service; on a packaged install, your own timer or cron line, with systemd/ontoplano-weekly-review.service as the template. The Docker image asks for it already.

Anything that can make an hourly HTTP request will do — the timers only ask the app's own endpoint:

5 * * * * curl -fsS -m 30 -X POST -H "x-health-token: $ONTOPLANO_HEALTH_TOKEN" http://127.0.0.1:1493/api/jobs/weekly-reviews

Hourly rather than daily because each account is checked against its own timezone, and seven in the morning is a different instant for each of them. Asking twice in an hour is safe.

Watching it

With ONTOPLANO_HEALTH_TOKEN set, /metrics answers in the Prometheus format to anything that sends the token — Prometheus, VictoriaMetrics, any agent that scrapes:

scrape_configs:
  - job_name: ontoplano
    static_configs: [{ targets: ['127.0.0.1:1493'] }]
    authorization: { credentials: 'the token' }

Beside the process's own numbers (CPU, memory, the event loop's lag), it counts every request by route, with how much of its time was spent in SQLite, and every SQL statement by its shape — so a slow route says whether the time is in the database or in the code around it. Two gauges say who is here: ontoplano_users_online (accounts that made a request in the last five minutes) and ontoplano_live_streams (tabs holding a live connection open). Without the token the route does not exist.