Skip to content

Persistence

Persistence

How Pvdify stores state and manages data.

Overview

Pvdify separates:

  • Control plane state — Managed by pvdifyd on VPS
  • Application data — External databases (apps are stateless)

Control Plane Persistence

State Directory

/var/lib/pvdify/
├── apps/ # App slot definitions
│ ├── myapp.yaml
│ ├── myapp-staging.yaml
│ └── myapp-pr-123.yaml
├── releases/ # Release history
│ └── myapp/
│ ├── v42.yaml
│ ├── v41.yaml
│ └── v40.yaml
├── config/ # Encrypted config versions
│ └── myapp/
│ ├── v3.sops.yaml
│ ├── v2.sops.yaml
│ └── v1.sops.yaml
├── tunnels/ # Tunnel configurations
│ └── pvdify-apps.yml
└── pvdifyd.db # SQLite for fast queries

SQLite Database

pvdifyd uses SQLite for fast queries and indexing:

-- Apps table
CREATE TABLE apps (
name TEXT PRIMARY KEY,
environment TEXT NOT NULL,
status TEXT NOT NULL,
image TEXT,
bind_port INTEGER,
created_at DATETIME,
updated_at DATETIME
);
-- Releases table
CREATE TABLE releases (
id INTEGER PRIMARY KEY,
app_name TEXT NOT NULL,
version INTEGER NOT NULL,
image TEXT NOT NULL,
config_version TEXT,
status TEXT NOT NULL,
created_at DATETIME,
created_by TEXT,
UNIQUE(app_name, version)
);
-- Domains table
CREATE TABLE domains (
domain TEXT PRIMARY KEY,
app_name TEXT NOT NULL,
status TEXT NOT NULL,
cf_record_id TEXT,
created_at DATETIME
);

YAML Files

Human-readable state for debugging and backup:

/var/lib/pvdify/apps/myapp.yaml
name: myapp
environment: production
status: running
image: ghcr.io/org/app:v1.2.3
bind_port: 3001
processes:
web:
command: npm start
count: 2
worker:
command: npm run worker
count: 1
domains:
- myapp.com
- www.myapp.com
resources:
memory: 512M
cpu: 0.5
healthcheck:
path: /health
interval: 30s
created_at: 2026-01-01T10:00:00Z
updated_at: 2026-01-05T10:30:00Z

Secrets (SOPS)

Config secrets encrypted with SOPS:

/var/lib/pvdify/config/myapp/v3.sops.yaml
NODE_ENV: production
DATABASE_URL: postgres://...
API_KEY: ENC[AES256_GCM,data:...,iv:...,tag:...]
sops:
kms: []
age:
- recipient: age1...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-01-05T10:30:00Z"
mac: ENC[...]
version: 3.8.1

Backup Strategy

Automated Backups

Terminal window
# Daily backup of /var/lib/pvdify/
/root/claude_backups/scripts/backup_pvdify.sh
# Contents:
tar -czf /root/claude_backups/pvdify/pvdify-$(date +%Y%m%d).tar.gz \
/var/lib/pvdify/
# Retention: 7 daily, 4 weekly

Pre-Change Snapshot

Before any system changes:

Terminal window
/root/claude_backups/create_snapshot.sh
# Includes: /var/lib/pvdify/, systemd units, tunnel config

Disaster Recovery

  1. Restore /var/lib/pvdify/ from backup
  2. Rebuild SQLite index: pvdifyd rebuild-index
  3. Recreate systemd units: pvdifyd regenerate-units
  4. Verify tunnel config: cloudflared tunnel ingress validate

Application Data

Apps are stateless by design. Data stored externally:

Data TypeRecommended Service
PostgreSQLPlanetScale, Supabase, Neon
MySQLPlanetScale
RedisUpstash, Redis Cloud
Files/BlobsAWS S3 (pvd-cdn-assets)
SearchAlgolia, Typesense
AnalyticsPostHog (data.philoveracity.com)

Database Connections

Apps receive connection strings via config:

Terminal window
pvdify config:set myapp \
DATABASE_URL=postgres://user:pass@db.example.com/myapp \
REDIS_URL=redis://user:pass@redis.example.com:6379

File Storage

Use S3 for user uploads and assets:

Terminal window
pvdify config:set myapp \
AWS_ACCESS_KEY_ID=AKIA... \
AWS_SECRET_ACCESS_KEY=... \
S3_BUCKET=pvd-cdn-assets

Systemd Units

Process state managed by systemd:

/etc/systemd/system/
├── pvdifyd.service # Control plane daemon
├── pvdify-myapp-web@.service # App web process template
├── pvdify-myapp-worker@.service # App worker template
└── cloudflared-pvdify.service # Tunnel service

Unit Template

# /etc/systemd/system/pvdify-myapp-web@.service
[Unit]
Description=Pvdify myapp web process %i
After=network.target
[Service]
Type=simple
User=pvdify
ExecStart=/usr/bin/podman run --rm \
--name pvdify-myapp-web-%i \
-p 3001:3000 \
--env-file /var/lib/pvdify/config/myapp/current.env \
ghcr.io/org/app:v1.2.3 \
npm start
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

Podman Storage

Container images and layers:

/var/lib/containers/storage/
├── overlay/ # Image layers
├── overlay-images/ # Image metadata
└── overlay-containers/ # Container state

Image Cleanup

Automated cleanup of old images:

Terminal window
# Weekly cron
podman image prune -a --filter "until=168h"

Logging

systemd Journal

All logs aggregated in journald:

Terminal window
# pvdifyd logs
journalctl -u pvdifyd -f
# App logs
journalctl -u pvdify-myapp-web@1 -f
# All pvdify logs
journalctl -u 'pvdify-*' -f

Log Retention

/etc/systemd/journald.conf
[Journal]
SystemMaxUse=2G
MaxRetentionSec=7day

Metrics

Basic metrics via systemd and podman:

Terminal window
# Process status
systemctl status pvdify-myapp-web@1
# Container stats
podman stats pvdify-myapp-web-1
# Resource usage
podman pod stats

Future: Prometheus metrics endpoint in pvdifyd.

State Diagram

┌─────────────────────────────────────────────────────────────┐
│ pvdifyd │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ SQLite │ │ YAML │ │ SOPS (secrets) │ │
│ │ (index) │ │ (state) │ │ (encrypted) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ └────────────────┼─────────────────────┘ │
│ │ │
│ ▼ │
│ /var/lib/pvdify/ │
└─────────────────────────────────────────────────────────────┘
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ systemd │ │ Podman │ │cloudflared│
│ units │ │ images │ │ tunnel │
└──────────┘ └──────────┘ └──────────┘