Skip to main content
This page is for whoever runs a Lastward core instance — the open-source server from Self-hosting. It covers how to run and configure it, what it sends and when, how to keep it safe, and the API it speaks.

What your instance is

  • Single-tenant. One instance serves one owner. There are no accounts, sign-ups, plans or limits — every feature is unlocked.
  • The same protocol as the hosted service. The core is a second, independent implementation of the protocol the Lastward cloud speaks, so the Lastward app connects to it unchanged.
  • Content-blind. It stores ciphertext and a passphrase-wrapped content key — never your passphrases or plaintext. The only exceptions are items you deliberately mark readable, and public pages, which are plaintext by nature. It knows only what the firing engine needs: the check-in rhythm, the deadline, recipient addresses, action types and state.
  • AGPL-3.0. Anyone who hosts a modified version for others must share its source — the strongest trust signal for software that holds secrets. The Lastward apps are licensed separately.

Running it

With Docker, docker compose up --build is all it takes (see the quick start). It starts PostgreSQL and the server, applies the database migrations, and serves on http://localhost:8080. The firing engine runs inside the server every SWEEP_INTERVAL_SECONDS (60 by default), so no external scheduler is needed.
Under Docker Compose the server always uses the bundled database and listens on port 8080 inside its container. PORT in .env only chooses the port published on your machine.

Without Docker

You need Node.js 24.16 or newer and your own PostgreSQL database. Put your settings in .env (it is read automatically) — at least DATABASE_URL and OWNER_TOKEN, which the migration step checks too.
To drive the firing engine from cron instead of the built-in interval, set SWEEP_INTERVAL_SECONDS=0 and run one sweep per tick with node dist/sweep.js.

Configuration

.env.example lists every setting. The server refuses to start when a value is invalid.
Use a long, random OWNER_TOKEN (openssl rand -hex 32). Anyone who has it can disarm or reconfigure your switches.

Authentication

Every owner call sends the token as a bearer header:
All /v1/switches… routes require it. The recipient release routes and public pages need no token — they are protected by their unguessable link instead.

What your instance sends

  • Email goes out over SMTP — or to the console when SMTP isn’t configured. Your own warning emails go to OWNER_EMAIL; without it, warnings are only logged.
  • Email is also the channel that carries your recipients’ release links, so configure SMTP before you rely on the instance — in console mode a fired switch reaches the log, not your recipients.
  • Push and SMS are console-only. The core ships no push or SMS provider, so those warning steps are written to the log, where you can wire up a provider of your own. In the Lastward app, check-in reminders still fire on your device.

How a switch fires

Firing is decided on the server, never on a device — your phone may be offline for the whole grace window. Each sweep:
  1. Moves a switch from active to grace once its check-in deadline passes.
  2. Sends the warning steps that are now due.
  3. Once the grace window (never less than 48 hours) has elapsed, fires: publishes any public pages, then emails each recipient a private release link for encrypted items, with any readable messages included in the email.
  4. Records every delivery before sending it, so crashes, overlapping sweeps and catch-up runs never send twice. A failed recipient email is retried on later sweeps for up to a day after firing.
  5. Deletes expired release links, and the payloads of switches that fired more than FIRE_RETENTION_DAYS ago.
A fired switch can’t be checked in again — the API answers already_fired.

Public pages (self-host only)

Lastward has five action types: message, file, secret, private_page and public_page. The hosted service refuses public_page — a truly public page is plaintext by nature and would make the host a publisher. Your instance accepts it. When a switch with a public page fires, its content is published at /p/<slug>. The slug comes from the action’s slug (or its subject, or the switch title); a taken slug gets a short random suffix. The page is public and indexable, and — unlike encrypted payloads — it is never removed by the retention purge: a published page is meant to stay up.
Whatever a public page publishes is your responsibility as the operator of the instance.

Keeping it running

  • Back up regularly. A plain database dump captures every switch, recipient, action and encrypted payload — a complete, portable backup that is enough to stand up an identical instance elsewhere. Because payloads are ciphertext, the dump is safe to keep offsite:
  • Publish your own shutdown protocol. If you will ever stop running an instance, give your recipients advance notice on more than one channel, a final export, and a pointer to the core’s repository so they can keep the switches alive themselves. “A service outlives its operator” is only partly solvable — open source plus export is the escape hatch, not a guarantee.
  • Keep passphrases out-of-band. The server can never recover a zero-knowledge passphrase. Make sure your recipients can get theirs even if you are gone.

API reference

All JSON routes live under /v1. Bodies are camelCase, timestamps ISO-8601, and errors come back as { "code": "...", "message": "..." }.

Owner routes (bearer token)

A switch’s safety settings:
  • cadence — { value, unit } with unit = day, week or month.
  • grace — { value, unit } with unit = hour or day; never less than 48 hours.
  • warnings — escalation steps { offsetHours, channels }, where offsetHours counts from the missed deadline; push and email must both appear across the steps.
Left out, they default to the Balanced preset: every 30 days, 14 days of grace, warnings at 0, 72 and 168 hours. The floors are enforced when you save and again when the sweep runs. A payload is either zero-knowledge — mode: "zk" with ciphertext, wrappedKey, salt, nonce and algo (or a blobRef for a large file) — or mode: "readable" with readableContent, which is allowed only for a message or a public_page. A public_page must be readable; file, secret and private_page are always zero-knowledge.

Public routes (no token)

Reference encryption envelope

The server never decrypts anything, but its release page includes an in-browser decryptor for the envelope algo = "xchacha20poly1305-argon2id-v1" (libsodium):
salt, wrappedKey, nonce and ciphertext are all base64. A client that uses a different envelope can build its own release page on GET /v1/release/:token — the server stores and returns ciphertext unchanged either way.