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:
- Open Settings → Backups in the dashboard and find Point-in-time restore.
- 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. - 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
- 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.
- Under Restore to, pick a date and time, then click Restore.
- 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) throughOnFilesystemCreate. Answer it and the history goes to your filesystem; S3 Fallback works this way. - Set it per instance. Write
extensions.pitr.enabledandextensions.pitr.retentioninto 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/pitrfor the status,POST /api/pitr/restorewith{"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.goruns one app with local history. - Litestream continuously copies
data.dbto its destination: the app's own bucket (its PITR settings), else a filesystem of kindlitestream(anOnFilesystemCreatehandler, such as S3 Fallback), elsepb_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 aspb_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
Questions and comments
Comments are a GitHub Discussions thread; sign in with GitHub to post.