Repository navigation
Backups and Storage
Where AstroBaaS keeps its data, how to choose a storage driver, and how to back up, restore and recover. It is for operators running an install in production.
The references are STORAGE.md and the backup section of deploy/README.md.
An install's data is three things:
| Part | Where | Override |
|---|---|---|
| The database |
db.json (lowdb), or a SQLite/libSQL database set by DATABASE_URL
|
DB_PATH, DATABASE_URL
|
| Public uploads | public/uploads/<yyyy>/<mm>/<hash>.<ext> |
UPLOADS_DIR |
| Private uploads (files attached to form submissions) |
private-uploads/ beside the database |
PRIVATE_UPLOADS_DIR |
Also keep a copy of your environment file (for example shared/.env). It holds AUTH_SECRET, payment keys and SMTP credentials, which cannot be re-created from the database.
All application code goes through one Storage contract (src/core/storage.ts), so the driver can be swapped without touching routes, plugins or the admin. Pending schema migrations run automatically at startup on whichever driver is active. See Upgrading.
DATABASE_URL |
DATABASE_DRIVER |
Driver | Use for |
|---|---|---|---|
| unset | none |
lowdb (JSON file at DB_PATH) |
Zero-config local development |
file:./data/astrobaas.db |
unset | libSQL doc-blob (local SQLite) | A durable single host |
libsql://<db>.turso.io + DATABASE_AUTH_TOKEN
|
unset | libSQL doc-blob (remote, Turso) | Durable and portable between hosts |
file: or libsql://…
|
relational |
relational libSQL (one row per entity) | Production, multiple writers or hosts |
All three drivers run in CI through the smoke suite (npm run smoke, npm run smoke:libsql, npm run smoke:relational).
- lowdb keeps everything in one JSON file on local disk. It cannot be shared between instances and is lost on ephemeral or serverless filesystems.
- doc-blob stores the whole dataset as one JSON row. It is durable, but writes are last-write-wins across processes.
-
relational (
DATABASE_DRIVER=relational) stores each collection as its own table of(id, data)rows. Two writers touching different rows do not overwrite each other. It is the recommended production driver.
# Production example (Turso, relational)
DATABASE_URL=libsql://my-app.turso.io
DATABASE_AUTH_TOKEN=...
DATABASE_DRIVER=relational
AUTH_SECRET=$(openssl rand -hex 32)
NODE_ENV=productionOn first boot against an empty database, AstroBaaS creates the schema and the initial admin account itself. There is no separate migration step.
Every file: database is opened with journal_mode=WAL, busy_timeout=5000 and synchronous=NORMAL.
- On the relational driver, a second writer (an import CLI, a second replica) waits for the lock rather than failing.
- On doc-blob, a second writer silently loses data, because each write replaces the single row. Stop the site before running anything that writes, or move to the relational driver.
npm run import:woo -- <dir> --applyrefuses a doc-blob database unless you pass--site-stopped. - Readers, backups and the shared rate-limit store are safe beside a running site on either driver.
Admin → Tools → Backup → Download backup calls GET /api/backup/export (admin-only). What you get depends on the driver:
| Install | Archive |
|---|---|
lowdb (no DATABASE_URL) |
JSON: { db, uploads }, the database plus every file under the uploads directory |
A file: DATABASE_URL (doc-blob or relational) |
A VACUUM INTO snapshot of the SQLite file (kind: 'libsql-file'), plus uploads |
A remote libsql:// (Turso) database |
Refused: there is no local file. Use turso db dump or the provider's tools. |
Limits:
- Upload files larger than 10 MB are left out, and the archive says so.
- If the uploads folder is over about 256 MB, uploads are left out with a clear note and only the database is backed up. Sync uploads separately (rsync, rclone).
-
Private uploads are not in the archive. Copy
PRIVATE_UPLOADS_DIRseparately.
Admin → Tools → Restore calls POST /api/backup/import (admin-only). It overwrites the current database and any media files with the same name. The largest accepted archive is 256 MB.
| Archive | Site running on | Result |
|---|---|---|
| JSON (lowdb) | lowdb | Restored |
| JSON (lowdb) | any DATABASE_URL
|
400. libSQL would ignore the file. |
| SQLite snapshot | a file: DATABASE_URL
|
The database file is replaced. Restart the site afterwards: other processes and in-memory caches still hold the old data. |
| SQLite snapshot | lowdb, or a remote libsql://
|
400 |
Restoring an archive also sanitises stored HTML and SVG on the way in. Restore expects a backup from the same version of AstroBaaS.
A backup on the same disk is a copy, not a backup. AstroBaaS can push archives on a schedule to any S3-compatible bucket, such as Backblaze B2, Cloudflare R2, MinIO, Hetzner or Wasabi. Configuration is environment only; nothing about the target is stored in the database.
BACKUP_S3_ENDPOINT=https://s3.eu-central-003.backblazeb2.com
BACKUP_S3_BUCKET=my-shop-backups
BACKUP_S3_KEY_ID=...
BACKUP_S3_SECRET=...
# Optional
BACKUP_S3_REGION=us-east-1
BACKUP_S3_PREFIX=astrobaas/myshop
BACKUP_S3_PATH_STYLE=1 # MinIO and similar
BACKUP_EVERY_HOURS=24
BACKUP_KEEP=14| Variable | Default | Notes |
|---|---|---|
BACKUP_S3_ENDPOINT, BACKUP_S3_BUCKET, BACKUP_S3_KEY_ID, BACKUP_S3_SECRET
|
none | All four are required, or off-site backup stays off. |
BACKUP_S3_REGION |
us-east-1 |
|
BACKUP_S3_PREFIX |
astrobaas |
The key prefix inside the bucket. |
BACKUP_S3_PATH_STYLE |
off |
1 for path-style addressing. |
BACKUP_S3_ALLOW_HTTP |
off | Only https endpoints are accepted unless this is 1 (for a MinIO on your own network). |
BACKUP_EVERY_HOURS |
24 |
Hours between runs (up to 30 days). |
BACKUP_KEEP |
14 |
Archives to keep (up to 365). Older ones are deleted after a successful upload. |
BACKUP_ATTEMPT_TIMEOUT_MS |
3600000 |
How long an unfinished attempt blocks a new one. |
How it behaves:
- It backs up whatever the active driver stores (the same rules as the admin export). A remote Turso database is reported as "not supported for this driver".
- Runs happen on the scheduler, and only one process per database runs them (a lease).
- The history is kept in the database, so a restart does not trigger a fresh backup. A backup cut off by a crash or deploy is retried after an hour.
- The last result is shown on Admin → Background jobs and in
/api/health/deepasoffsite_backup. A warning there means the last backup failed, or the last success is more than two intervals old. -
POST /api/backup/run(admin) runs the same backup now. - Archives from off-site backups can be restored through Admin → Tools → Restore, following the same rules.
Put the bucket with a different provider or in a different region from the server.
If you back up with your own tooling, do not copy a live SQLite file with cp. In WAL mode the newest commits sit in the -wal file. Take a consistent snapshot instead:
sqlite3 data/astrobaas.db "VACUUM INTO '/backups/astrobaas.db'"
# or
sqlite3 data/astrobaas.db ".backup '/backups/astrobaas.db'"For lowdb, a copy of db.json is atomic enough. A nightly copy should cover the database, uploads/ and private-uploads/. Keep the env file separately and encrypted.
The backup tools will not do this. Export depends on the driver and restore works only onto the same kind of driver. That is deliberate: the alternative is a restore that reports success and changes nothing the site reads.
Ways to move content instead:
-
Through the API. Every route uses the same
Storagecontract whatever the driver, so reading from the old install and writing to the new one works. It is the only route that works while both are running. See Headless-and-REST-API. -
Through the importers.
npm run import:wpandnpm run import:woowrite to whateverDATABASE_URLandDATABASE_DRIVERsay. If the content came from WordPress or WooCommerce, re-importing is often less work. (npm run import:md,npm run setupandnpm run reset-passwordrefuse whenDATABASE_URLis set.) See Importing. -
Between two libSQL databases with the same
DATABASE_DRIVER(afile:one and Turso, either way), the schema is the same. Use the database's own tools (turso db dump,VACUUM INTO). Doc-blob and relational are different schemas. -
Uploads always travel separately.
rsyncbothUPLOADS_DIRandPRIVATE_UPLOADS_DIR. File records store paths relative to those directories, so moving each directory whole is enough.
Per-collection CSV export and import of posts, pages and custom content types is also available (/api/transfer/{collection}). Import matches rows by id, then slug, and shows a preview before anything is written.
A backup nobody has restored is a hope. Practise these steps on a scratch install now and then.
- Stand up the same version. Deploy the release that made the backup (or check Upgrading for the path forward). See Installation and Deployment.
-
Restore the environment file. Use the same
AUTH_SECRET; without it, existing sessions are invalid (survivable). Restore payment, SMTP and backup credentials from your encrypted copy. -
Restore the database.
- lowdb: put
db.jsonback, or restore the JSON archive through Admin → Tools → Restore. -
file:SQLite: put the snapshot file at theDATABASE_URLpath while the site is stopped, or restore the snapshot archive through the admin and then restart. - Turso: restore with Turso's tools.
- lowdb: put
-
Restore uploads. Copy
UPLOADS_DIRandPRIVATE_UPLOADS_DIRback into place. With nginx, point/uploads/at the shared directory, not at a release'sdist/client/uploads. -
Start and check.
GET /readyzreturns 503 until the database is at the schema version the build expects. Then checkGET /api/health/deepfor warnings (offsite_backup,email_channel,cors_origins,payment_return_urls). -
Run the mail test (
node dist/mail-test.mjs you@example.com) and place a test order if the shop is on. - Confirm off-site backups resume, and note that the first scheduled run happens once one is due.
-
Deployment: the release layout and
shared/directory - Staging-and-Maintenance: maintenance mode and staging copies
- Upgrading: migrations and upgrade notes
- Security: keeping secrets in the environment
- STORAGE.md
AstroBaaS is free software under GPL-3.0-or-later. This wiki describes the latest release; the repository and its CHANGELOG are the final word. Found a mistake here? Open an issue.
Getting started
Using the CMS
Building with it
Extending
Commerce and email
Operating
Project