PITR

Point-in-time restore: your database as it was at any moment in the last few days, restored from the dashboard. History is recorded with Litestream, next to your data or in an S3-compatible bucket.

Install · Configure · Use · Integrations · Example

Install

PITR is built into pocketone and on by default. Every app it serves starts recording history the first time it serves; there's nothing to install.

To run without it:

pocketone serve --pitr=false

In your own Go build, register it before the app starts:

app := pocketbase.New()
pitr.MustRegister(app, pitr.Config{})
app.Start()

Configure

On, off and how far back

Set it in pocketone.yaml:

pitr:
  enabled: true          # false turns it off
  retention: 72h         # how far back a restore can go
  snapshot_interval: 24h # how often a full copy is taken

Or with flags, which win over the file:

Flag Default What it does
--pitr true Record history while serving. --pitr=false turns it off.
--pitrRetention 72h How far back a restore can go.

A shorter snapshot_interval makes restores faster and uses more storage.

Where the history is stored

By default the history is kept next to your database, in pb_data/litestream/. That protects you from mistakes in your data, but not from losing the disk. To keep it somewhere else, use your own S3-compatible bucket:

  1. Open Settings → Backups in the dashboard and find Point-in-time restore.
  2. Tick Store the history in my own S3 bucket and fill in the endpoint, bucket, region, access key and secret. Tick Force path-style addressing for MinIO and similar servers. The prefix defaults to litestream.
  3. Click Test connection to check that the bucket accepts a write, then Save.

From then on the history is recorded in the bucket, starting with a fresh copy, so restores go back to the moment you saved. The secret is never shown again after saving. If the app runs with an encryption key (--encryptionEnv), the secret is stored encrypted.

pocketone.yaml reference

pitr:

Point-in-time restore: on by default, retention, snapshot interval. The --pitr and --pitrRetention flags win.

Key Type Default Description
enabled bool On unless false. The --pitr flag wins.
retention duration How far back a restore can go. Default 72h. The --pitrRetention flag wins.
snapshot_interval duration How often a full copy is taken. Default 24h. Shorter: faster restores, more storage.

Use

What's covered

Your database: records, collections, settings and superusers. Not uploaded files, pb_hooks, pb_public or the logs database. Use PocketBase's backups for those.

History starts with the first change after PITR is turned on, and goes back as far as the retention allows (72 hours by default).

Restore from the dashboard

  1. Open Settings → Backups. The Point-in-time restore section shows how far back you can go, how big the history is and where it's stored.
  2. Under Restore to, pick a date and time, then click Restore.
  3. The app keeps serving while the old database is rebuilt, then restarts with it.

Only superusers see this section.

Undo a restore

The database you replaced is kept in pb_data/litestream/before-restore.db, and history keeps recording, so a restore can be undone. After a restore, the section shows when you restored and to what time, with an Undo this restore button.

From the command line

pocketone pitr status
pocketone pitr restore --at 2026-11-11T14:05:00Z --to /tmp/then.db   # a copy, any time
pocketone pitr restore --at 2026-11-11T14:05:00Z                     # in place

--at takes an RFC 3339 time. With --to, you get a copy of the database as it was, to inspect or pull records from; the app keeps running. Without --to, run it while the server is stopped: the restore is swapped in when it next starts.

What it costs

About 176 KiB of memory per serving app, and the storage for your history. Replication runs only while the app is serving, never during CLI commands.

Integrations

Works with

Extension How they fit
Pocker Every instance on the host gets PITR. The pitr section sets the defaults for all of them; each instance can override enabled and retention (below). A restore restarts only that instance, and sleeping instances cost nothing.
S3 Fallback Mount litestream on a backend and every app without its own bucket keeps its history there, under its own prefix. An app's own bucket (Settings → Backups) still wins.
Multitenant (planned) Sets each instance's enabled and retention from its plan, for example a plan without PITR.
PocketBase backups pb_data/litestream/ is left out of backup zips, and restoring a zip doesn't wipe it.

Per-instance settings (pocker)

Each instance can override enabled and retention in its extensions.pitr settings, from instances.yaml or a provider:

instances:
  - id: abc
    extensions: { pitr: { retention: 168h } }
  - id: free-plan-app
    extensions: { pitr: { enabled: false } }

Each instance's history goes to instances/<id>/litestream/ in the shared bucket, or its own pb_data/litestream/. A bucket in an instance's settings is refused, so credentials never sit in instance data.

Building on PITR

  • Store history somewhere else. PITR asks PocketBase for a filesystem of kind litestream (pitr.Kind) through OnFilesystemCreate. Answer it and the history goes to your filesystem; S3 Fallback works this way.
  • Set it per instance. Write extensions.pitr.enabled and extensions.pitr.retention into an instance's settings; PITR reads them when the instance starts. Multitenant will work this way.
  • Go API. (*pitr.Extension).Status(ctx, app) returns the restorable window, size, destination and last restore; Restore(ctx, app, at) restores an app to a time.
  • REST API (superusers): GET /api/pitr for the status, POST /api/pitr/restore with {"at": "<RFC 3339 time>"} to restore.

Example

example/main.go runs one app with point-in-time restore on, keeping three days. Run it with go run ./example serve from this folder.

// Command example runs one PocketOne app with point-in-time restore: Litestream copies data.db continuously into
// pb_data/litestream/, and Settings → Backups gets a "Point-in-time restore" section.
//
//	go run ./example serve
//	go run ./example pitr status
//	go run ./example pitr restore --at 2026-11-11T14:05:00Z --to /tmp/then.db
//
// Logins (ext/devusers, local testing only): dashboard [email protected] / password, app user [email protected] / password.
package main

import (
	"log"
	"time"

	"ganigeorgiev.com/pocketbase/pocketbase"
	"github.com/pockethost/pocketone/ext/devusers"
	"github.com/pockethost/pocketone/ext/pitr"
)

func main() {
	app := pocketbase.New()
	devusers.Register(app) // local testing: superuser [email protected] and user [email protected], password "password"

	pitr.MustRegister(app, pitr.Config{
		Retention: 72 * time.Hour, // on by default; Enabled: false turns it off
		// Bucket: &pitr.Bucket{Endpoint: "...", Bucket: "...", Region: "auto", AccessKey: "...", Secret: "...", Label: "EU"},
	})

	if err := app.Start(); err != nil {
		log.Fatal(err)
	}
}

How it works

  • Go module github.com/pockethost/pocketone/ext/pitr, Litestream v0.5.18 (pinned). example/main.go runs one app with local history.
  • Litestream continuously copies data.db to its destination: the app's own bucket (its PITR settings), else a filesystem of kind litestream (an OnFilesystemCreate handler, such as S3 Fallback), else pb_data/litestream/. Buckets go through PocketBase's own S3 client, not the AWS SDK. Each LTX file's timestamp is kept in object metadata, so restoring to a time doesn't depend on upload times.
  • Replication runs only while the app serves (OnServe), never for CLI commands. On terminate it's flushed and detached, so under pocker a sleeping instance costs nothing.
  • A restore is built into a side file while the app keeps serving, then swapped in by app.Restart(). The replaced database is kept as pb_data/litestream/before-restore.db.
  • Apps with different retention get separate Litestream stores, since Litestream applies retention per store.
  • Cost per serving app: about 176 KiB of memory, 4 goroutines, 5 file descriptors. Binary: about +4 MiB.

License

MIT