Data Storage

NexusPanel stores all persistent data as JSON files in the data/ directory. There is no database requirement for the panel itself (PostgreSQL is optional, used only for the Database Manager module).


Data File Inventory

FileFormatMax SizeLockingAtomic Write
data/users.jsonObject (keyed by username)UnlimitedIn-memory boolean (5s timeout)Yes
data/settings.jsonObjectUnlimitedNoneYes
data/audit.jsonArray10,000 entriesNoneNo (buffered flush)
data/notifications.jsonArray500 entriesNoneNo
data/tokens.jsonArrayUnlimitedNoneNo
data/api-tokens.jsonArrayUnlimitedNoneYes
data/domains.jsonObject (keyed by domain)UnlimitedIn-memory boolean (5s timeout)Yes
data/apps.jsonArrayUnlimitedIn-memory boolean (5s timeout)Yes
data/deployments.jsonArrayUnlimitedIn-memory boolean (5s timeout)Yes
data/deploy_keys.jsonArrayUnlimitedIn-memory boolean (5s timeout)Yes
data/deploy_env_vars.jsonObjectUnlimitedIn-memory boolean (5s timeout)Yes
data/mime-types.jsonObjectUnlimitedNoneYes
data/terminal-presets.jsonArrayUnlimitedNoneNo
data/update-history.jsonArrayUnlimitedNoneYes
data/panel-version-cache.jsonObjectUnlimitedNoneYes
data/metrics/history.jsonlJSON LinesUnlimitedNoneAppend-only
data/filebin/Directory treeDisk spacePer-fileN/A
data/avatars/Image filesUnlimitedNoneN/A

Atomic Write Pattern

Most services use the atomic write pattern to prevent file corruption:

function saveData(data) {
  const tmpFile = DATA_FILE + '.tmp';
  fs.writeFileSync(tmpFile, JSON.stringify(data, null, 2), 'utf-8');
  fs.renameSync(tmpFile, DATA_FILE);
}

On POSIX filesystems, rename() is an inode-level atomic operation. The target file is either the old complete version or the new complete version — never a half-written state.

Variations

Some services append a PID or timestamp to the temp filename to avoid collisions:

// Updates module
const tmpFile = HISTORY_PATH + '.tmp.' + process.pid;

// MIME types module
const tmpFile = DATA_FILE + '.tmp.' + Date.now();

File Locking Strategies

Strategy 1: In-Memory Boolean Lock

Used by: users.js, domains.js, backups.js, virusscanner.js

let writeLock = false;
const LOCK_TIMEOUT = 5000;

function acquireLock() {
  return new Promise((resolve, reject) => {
    const start = Date.now();
    const wait = () => {
      if (!writeLock) { writeLock = true; return resolve(); }
      if (Date.now() - start > LOCK_TIMEOUT) return reject(new Error('Write lock timeout'));
      setTimeout(wait, 10);
    };
    wait();
  });
}

function releaseLock() { writeLock = false; }
  • Scope: Process-level (single Node.js process)
  • Polling: Every 10ms
  • Timeout: 5 seconds
  • Safety net: Some services add automatic lock release via setTimeout

Strategy 2: Lock File on Disk

Used by: cron.js

function acquireFileLock(filePath) {
  const lockFile = filePath + '.lock';
  if (fs.existsSync(lockFile)) {
    const content = fs.readFileSync(lockFile, 'utf8');
    const lockTime = parseInt(content);
    if (Date.now() - lockTime < CRON_LOCK_TIMEOUT) {
      throw new Error('File is locked, please retry');
    }
  }
  fs.writeFileSync(lockFile, String(Date.now()), { mode: 0o600 });
}

function releaseFileLock(filePath) {
  const lockFile = filePath + '.lock';
  try { fs.unlinkSync(lockFile); } catch {}
}
  • Scope: Cross-process (any process can check the lock file)
  • Stale detection: Locks older than timeout are overridden
  • Permissions: 0o600 (owner-only read/write)

Strategy 3: No Locking

Used by: audit.js, notifications.js, settings.js, tokens.js

These services perform read-modify-write cycles without any locking. They rely on the atomic write pattern to prevent partial writes, but concurrent modifications could cause data loss (last-writer-wins).


Data File Formats

users.json

{
  "admin": {
    "username": "admin",
    "password": "$2a$10$...",
    "role": "admin",
    "email": "admin@example.com",
    "displayName": "Administrator",
    "avatar": "data:image/png;base64,...",
    "twoFactorSecret": "JBSWY3DPEHPK3PXP",
    "createdAt": "2026-01-01T00:00:00.000Z",
    "lastLogin": "2026-07-28T12:00:00.000Z",
    "loginCount": 42
  }
}

audit.json

[
  {
    "id": "a_1690000000000_abc12345",
    "timestamp": "2026-07-28T12:00:00.000Z",
    "user": "admin",
    "role": "admin",
    "ip": "192.168.1.100",
    "action": "file.create",
    "method": "POST",
    "path": "/api/files/create",
    "details": { "path": "/var/www/html/index.html", "type": "file" }
  }
]

Max entries: 10,000 (oldest trimmed)
Flush interval: 5 seconds (buffered)
Shutdown hooks: process.exit, SIGINT, SIGTERM

notifications.json

[
  {
    "id": "n_1690000000000",
    "type": "info",
    "title": "Backup Complete",
    "message": "System backup completed successfully",
    "timestamp": "2026-07-28T12:00:00.000Z",
    "read": false
  }
]

Max entries: 500 (oldest trimmed from front)

tokens.json (Bearer Auth)

[
  {
    "id": "tk_1690000000000",
    "userId": "admin",
    "label": "CI/CD Pipeline",
    "scope": "admin",
    "hash": "$2a$8$...",
    "prefix": "npt_abc1234",
    "createdAt": "2026-07-28T12:00:00.000Z",
    "lastUsed": "2026-07-28T12:30:00.000Z"
  }
]

Token format: npt_ + 48 hex chars (crypto.randomBytes(24).toString('hex'))
Storage: Bcrypt hash (cost 8)

api-tokens.json (Settings Tokens)

[
  {
    "id": "tok_1690000000000_abcdef1234567890",
    "name": "External Integration",
    "scope": "read",
    "secret": "nxs_64hexchars...",
    "createdAt": "2026-07-28T12:00:00.000Z",
    "revoked": false
  }
]

Token format: nxs_ + 64 hex chars
Storage: Plaintext (local access only)

settings.json

{
  "panelName": "NexusPanel",
  "serverLocation": "Amsterdam, NL",
  "defaultPage": "dashboard",
  "sessionTimeout": 60,
  "idleTimeout": 30,
  "language": "en",
  "timezone": "UTC",
  "enable2FA": false,
  "loginNotifications": true,
  "ipWhitelist": [],
  "theme": "dark",
  "sidebarPosition": "left",
  "fontSize": "medium",
  "accentColor": "#10b981",
  "desktopNotifications": false,
  "updateAlerts": true,
  "emailNotifications": false,
  "notifyOn": {
    "updates": true,
    "security": true,
    "errors": true
  },
  "debugMode": false,
  "logRetentionDays": 30,
  "autoUpdate": false,
  "updateChannel": "stable",
  "lastUpdateCheck": null,
  "lastUpdateResult": null
}

On load(), the file is merged with defaults via spread: { ...DEFAULTS, ...data }.

domains.json

{
  "example.com": {
    "domain": "example.com",
    "port": 3000,
    "ssl": true,
    "nginxConfig": "server { ... }",
    "createdAt": "2026-07-28T12:00:00.000Z"
  }
}

metrics/history.jsonl

Each line is a JSON object representing a metrics snapshot:

{"timestamp":"2026-07-28T12:00:00.000Z","cpu":45.2,"memory":{"used":1234567890,"total":4294967296,"percent":28.7},"disk":{"used":12345678901,"total":59874012345,"percent":20.6},"network":{"rx":123456789,"tx":987654321}}

Data Directory Structure

data/
├── users.json                 # User accounts
├── settings.json              # Panel configuration
├── audit.json                 # Audit log
├── audit-backup-*.json        # Audit log backups (on clear)
├── notifications.json         # Notifications
├── tokens.json                # Bearer auth tokens
├── api-tokens.json            # Settings-managed API tokens
├── domains.json               # Domain/vhost config
├── apps.json                  # Installed applications
├── deployments.json           # Git deployment records
├── deploy_keys.json           # Encrypted SSH deploy keys
├── deploy_env_vars.json       # Encrypted deploy env vars
├── deploy/                    # Deployment logs (one .log per deploy)
├── mime-types.json            # Custom MIME types
├── terminal-presets.json      # Terminal command presets
├── update-history.json        # Update history
├── panel-version-cache.json   # Panel version check cache
├── metrics/
│   └── history.jsonl          # Metrics time series
├── filebin/                   # Trash/recycle bin
│   └── batch_<timestamp>/
│       ├── manifest.json      # Batch metadata
│       └── <files>            # Deleted files
└── avatars/                   # User avatar storage
    └── <username>.<ext>       # Avatar images

Metrics Time Series

The metrics module appends snapshots to data/metrics/history.jsonl (JSON Lines format).

Retention

  • Default: 30 days
  • Configurable: Via Settings → logRetentionDays
  • Cleanup: Old entries are pruned on each metrics write

Data Points

MetricSourceResolution
CPU usage/proc/stat or topPer sample
Memory usagefree -bPer sample
Disk usagedf -B1 /Per sample
Network I/O/proc/net/devPer sample (delta)

Querying

GET /api/metrics/history?period=24h    # Last 24 hours
GET /api/metrics/history?period=7d     # Last 7 days
GET /api/metrics/history?period=30d    # Last 30 days

File Bin (Trash)

Deleted files are moved to data/filebin/ instead of being permanently deleted.

Batch Structure

data/filebin/
└── batch_1690000000000/
    ├── manifest.json
    ├── file1.txt
    └── directory/
        └── file2.txt

Manifest Format

{
  "timestamp": "2026-07-28T12:00:00.000Z",
  "files": [
    {
      "originalPath": "/var/www/html/index.html",
      "binPath": "batch_1690000000000/index.html",
      "size": 1234,
      "isDirectory": false
    }
  ]
}

Operations

OperationDescription
RestoreMove files back to original paths
Permanent deleteRemove files from bin
Empty binRemove all batches

Backup Data

Backups are stored outside the data/ directory (configurable path). Each backup includes:

Backup Metadata

{
  "timestamp": "2026-07-28_12-00-00",
  "type": "full",
  "status": "completed",
  "size": 123456789,
  "files": 42,
  "duration": 120,
  "targets": ["system", "databases", "webfiles"]
}

Schedule Format

{
  "id": "sched_<timestamp>",
  "name": "Daily System Backup",
  "type": "full",
  "schedule": "0 2 * * *",
  "retention": 7,
  "enabled": true,
  "targets": ["system", "databases"]
}

Part of NexusPanel Documentation

← Back to Documentation