Operations
Health checks
astropress doctor # env contract, runtime plan, schema drift warningsastropress doctor --json # machine-readable outputastropress doctor --strict # exit non-zero on any warningastropress services verify # confirm content-services keys + service originbun run audit:security # inline-handler and hardening checksastropress doctor warns on:
- missing or weak
SESSION_SECRET - scaffold-default
ADMIN_PASSWORDstill in use ADMIN_BOOTSTRAP_DISABLEDnot set- missing
.data/directory - projects still using legacy
ASTROPRESS_*PROVIDERvars
Local bootstrap
astropress new my-siteastropress new my-site --app-host vercel --content-services supabaseastropress new my-site --app-host cloudflare-pages --content-services cloudflarecd my-sitebun installastropress doctorastropress services bootstrapastropress services verifyastropress devBackup and restore
# Create a snapshotastropress backup --project-dir <site> --out <snapshot-dir>
# Restore from a snapshotastropress restore --project-dir <site> --from <snapshot-dir>Snapshots are file-based exports. For hosted providers, supplement with provider-native point-in-time restore (Cloudflare D1 time travel, Supabase Point-in-Time Recovery).
Importing content
WordPress
astropress import wordpress --project-dir <site> --source export.xmlOptional flags:
--artifact-dir <dir>— write artifacts here (default:.astropress/import/)--download-media— download attachment assets intoartifacts/downloads/--apply-local— apply staged artifacts into the local SQLite runtime--resume— re-enter a previous import and skip already-downloaded media
The import is staged, not instant. It writes structured JSON artifacts
(content, media, comments, users, taxonomies, redirects) for review before
applying. Run without --apply-local first to inspect what will change.
Wix
Export your site from Wix’s dashboard (Settings → General → Export Site Data), then:
astropress import wix --project-dir <site> --source wix-export.csvSame flags as WordPress: --artifact-dir, --download-media, --apply-local,
--resume.
If you need to download the export programmatically, the Playwright-based
credential fetcher handles authenticated Wix dashboard access — see the
import wix --fetch flag.
Crawling any site
For sites without an export format:
astropress import crawl --project-dir <site> --url https://example.comThe crawler walks links from the start URL, extracts page titles and body HTML, and writes the results as importable content records. Useful for migrating from static generators or hand-authored HTML sites.
Content scheduling
Set a publish time in the post editor (Scheduled Publish Time field) to
queue a draft for future publication.
The SQLite runtime exposes runScheduledPublishes() which promotes all
due posts atomically:
import { runScheduledPublishes } from "astropress/sqlite-admin-runtime";
// Call this on a schedule — e.g., every 5 minutesconst published = runScheduledPublishes(db);console.log(`Published ${published} scheduled posts`);Cloudflare Workers: wire it in a scheduled handler in worker.ts:
export default { async scheduled(_event, env, _ctx) { const runtime = await createRuntime(env); await runtime.content.runScheduledPublishes(); },};GitHub Actions: use a cron workflow to hit a secured API route that
calls runScheduledPublishes().
The posts list shows a Scheduled filter tab. Posts stay draft until
runScheduledPublishes fires; cancelling clears the scheduled time.
Secret handling
Before any real deployment:
- Generate a strong
SESSION_SECRET:SESSION_SECRET=$(openssl rand -hex 32) - Change
ADMIN_PASSWORDandEDITOR_PASSWORDfrom scaffold defaults - Set
ADMIN_BOOTSTRAP_DISABLED=1once named admin accounts exist - Set
TURNSTILE_SECRET_KEYbefore exposing the login form publicly
Secret rotation
Astropress supports a two-phase session-secret rotation window.
Package/runtime-managed sessions
- Deploy the new secret as
SESSION_SECRET - Keep the previous secret in
SESSION_SECRET_PREV - Wait for old sessions to expire naturally, or revoke them explicitly once the migration window is complete
- Remove
SESSION_SECRET_PREVin a follow-up deploy
During the rotation window:
- existing sessions signed with
SESSION_SECRET_PREVremain valid - all newly created sessions are signed only with
SESSION_SECRET
If you use the advanced root-secret override instead of SESSION_SECRET, the same pattern applies with ASTROPRESS_ROOT_SECRET and ASTROPRESS_ROOT_SECRET_PREV.
Cloudflare adapter sessions
- Deploy the new secret as
CLOUDFLARE_SESSION_SECRET - Keep the previous secret in
CLOUDFLARE_SESSION_SECRET_PREV - Wait for old sessions to expire naturally, or revoke them explicitly once the migration window is complete
- Remove
CLOUDFLARE_SESSION_SECRET_PREVin a follow-up deploy
During the rotation window:
- existing sessions signed with
CLOUDFLARE_SESSION_SECRET_PREVremain valid - all newly created sessions are signed only with
CLOUDFLARE_SESSION_SECRET
To revoke all active sessions immediately:
UPDATE admin_sessions SET revoked_at = CURRENT_TIMESTAMPWHERE revoked_at IS NULL;Schema migrations
Framework-managed (SQLite)
Most upgrades require no manual steps for SQLite deployments. On every boot,
Astropress applies any new ALTER TABLE ... ADD COLUMN statements idempotently.
Applied migrations are recorded in schema_migrations.
User-managed migrations
Place numbered .sql files in your migrations/ directory:
migrations/ 0001_add_event_taxonomy.sql 0002_add_event_date_index.sqlastropress db migrate --migrations-dir ./migrationsastropress db migrate --migrations-dir ./migrations --dry-run # preview onlyCompanion .down.sql files are read and stored in schema_migrations.rollback_sql
for safe rollback. To apply the most recent migration’s rollback SQL:
astropress db rollback # apply rollback_sql from the last migrationastropress db rollback --dry-run # preview what would be rolled backCloudflare D1 migrations
astropress db migrate --target=d1 applies pending migration files to your D1
database via wrangler d1 execute. Requirements:
wrangleron PATH:bun add -g wranglerornpm install -g wrangler- Authenticated:
wrangler loginorCLOUDFLARE_API_TOKENenv var CLOUDFLARE_D1_BINDINGenv var set to your D1 binding name (default:DB)
# Preview which files would run (no writes):astropress db migrate --target=d1 --dry-run
# Apply to the remote D1 database:astropress db migrate --target=d1Supabase migrations
Supabase projects use the Supabase CLI for
schema migrations. Astropress migration .sql files are compatible — place them in
supabase/migrations/ and use the Supabase CLI workflow:
# Link to your project (once):supabase link --project-ref <ref>
# Push pending migrations to the remote database:supabase db pushVersion upgrade procedure
- Snapshot:
astropress backup --project-dir <site> --out <snapshot-dir> - Upgrade the package:
bun add astropress@latest - Run
astropress doctor— flags env contract changes and schema drift - For SQLite: run
astropress db migrate(defaults to--target=local) - For D1: run
astropress db migrate --target=d1 - For Supabase: run
supabase db push - Deploy. If migration fails, restore from snapshot and replay in isolation.
Caching
applyAstropressSecurityHeaders() sets Cache-Control by area:
| Area | Value | Effect |
|---|---|---|
public | public, max-age=300, s-maxage=3600, stale-while-revalidate=86400 | 5 min browser / 1 hr CDN |
admin | private, no-store | Never cached |
auth | private, no-store | Never cached |
api | private, no-store | Never cached |
CDN purge on publish: Astropress fires purgeCdnCache(slug, config) on
every content publish — supports Cloudflare Cache API and generic webhook URLs:
registerCms({ cdnPurgeWebhook: "https://api.netlify.com/build_hooks/your-hook-id",});Static host publishing
Full rebuild
astropress build # full Astro build → dist/astropress publish # build + push to configured static hostIncremental rebuild
For sites with large page counts, use --incremental to regenerate only the
pages affected by a given set of slug changes:
astropress publish --incremental --slugs /blog/my-post,/blog/another-postecho -e "/blog/my-post\n/blog/another-post" | astropress publish --incremental --stdin| Mode | Build time | Best for |
|---|---|---|
| Full rebuild | Proportional to site size | Small sites, release deploys |
| Incremental | Proportional to changed pages | Large sites, frequent edits |
For sites with fewer than ~500 pages, full rebuild is simpler and fast enough.
Observability
Structured logs
Set LOG_LEVEL to emit structured JSON lines to stderr. Fields: level,
context, message, timestamp, requestId.
Metrics endpoint
GET /ap-api/v1/metrics Bearer token required (content:read scope)Returns { posts, pages, media, comments, uptime }.
Prometheus metrics
Enable the unauthenticated Prometheus text format endpoint:
registerCms({ monitoring: { prometheusEnabled: true },});GET /ap/metrics No authentication requiredContent-Type: text/plain; version=0.0.4Exported metrics: ap_content_total{kind}, ap_media_total, ap_uptime_seconds.
Incident handling
Auth breach
- Revoke all sessions:
UPDATE admin_sessions SET revoked_at = CURRENT_TIMESTAMPWHERE revoked_at IS NULL;
- Rotate
SESSION_SECRET/SESSION_SECRET_PREVandCLOUDFLARE_SESSION_SECRET/CLOUDFLARE_SESSION_SECRET_PREV - Audit
admin_sessionsandaudit_eventsfor suspicious activity
Data loss
- Restore from the last clean backup
- Run
astropress doctorandastropress services verify - Re-apply content created since the snapshot using audit logs as reference
Admin panel down
- Run
astropress doctor— check env and service health - Check server logs for runtime errors or migration failures
- If a failed migration caused it: restore from snapshot, replay migration in isolation, then redeploy
Disaster recovery
RTO / RPO targets
| Tier | Data store | RPO | RTO |
|---|---|---|---|
| Local + SQLite | SQLite file | Last astropress backup snapshot | < 5 min |
| Cloudflare Pages + D1 | Cloudflare D1 | D1 time-travel (30 days) | < 15 min |
| Supabase | PostgreSQL | Point-in-time recovery (up to 7 days on Pro) | < 30 min |
Post-restore checklist
-
astropress doctor— all checks pass -
astropress services verify— database connectivity confirmed - Admin panel login succeeds
-
GET /ap/healthreturns{ "status": "ok" }