Files
2026-04-17 23:26:01 +00:00

4.3 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Read Settings via app.Settings(), Encrypt at Rest with PB_ENCRYPTION HIGH Hardcoded secrets and unencrypted settings storage are the settings, configuration, encryption, secrets, PB_ENCRYPTION, extending

Read Settings via app.Settings(), Encrypt at Rest with PB_ENCRYPTION

PocketBase stores every runtime-mutable setting (SMTP credentials, S3 keys, OAuth2 client secrets, JWT secrets for each auth collection) in the _params table as JSON. Admin UI edits write to the same place. There are two knobs that matter: (1) how you read settings from Go/JS - always via app.Settings() at call time, never captured at startup; (2) how they are stored on disk - set the PB_ENCRYPTION env var to a 32-char key so the whole blob is encrypted at rest. Without encryption, anyone with a copy of data.db has your SMTP password, OAuth2 secrets, and every collection's signing key.

Incorrect (hardcoded secret, captured at startup, unencrypted at rest):

// ❌ Secret compiled into the binary - leaks via `strings ./pocketbase`
const slackWebhook = "https://hooks.slack.com/services/T00/B00/XXXX"

// ❌ Captured once at startup - if an admin rotates the SMTP password via the
//    UI, this stale value keeps trying until restart
var smtpHost = app.Settings().SMTP.Host

// ❌ No PB_ENCRYPTION set - `sqlite3 pb_data/data.db "SELECT * FROM _params"`
//    prints every secret in plaintext
./pocketbase serve

Correct (env + settings lookup at call time + encryption at rest):

# Generate a 32-char encryption key once and store it in your secrets manager
# (1Password, SOPS, AWS SSM, etc). Commit NOTHING related to this value.
openssl rand -hex 16    # 32 hex chars

# Start with the key exported - PocketBase AES-encrypts _params on write
# and decrypts on read. Losing the key == losing access to settings.
export PB_ENCRYPTION="3a7c...deadbeef32charsexactly"
./pocketbase serve
// Reading mutable settings at call time - reflects live UI changes
func notifyAdmin(app core.App, msg string) error {
    meta := app.Settings().Meta
    from := mail.Address{Name: meta.SenderName, Address: meta.SenderAddress}
    // ...
}

// Mutating settings programmatically (e.g. during a migration)
settings := app.Settings()
settings.Meta.AppName = "MyApp"
settings.SMTP.Enabled = true
settings.SMTP.Host = os.Getenv("SMTP_HOST") // inject from env at write time
if err := app.Save(settings); err != nil {
    return err
}
// JSVM
onBootstrap((e) => {
    e.next();

    const settings = $app.settings();
    settings.meta.appName = "MyApp";
    $app.save(settings);
});

// At send-time
const meta = $app.settings().meta;

Secrets that do NOT belong in app.Settings():

  • Database encryption key itself → PB_ENCRYPTION env var (not in the DB, obviously)
  • Third-party webhooks your code calls (Slack, Stripe, etc) → env vars, read via os.Getenv / $os.getenv
  • CI tokens, deploy keys → your secrets manager, not PocketBase

app.Settings() is for things an admin should be able to rotate through the UI. Everything else lives in env vars, injected by your process supervisor (systemd, Docker, Kubernetes).

Key details:

  • PB_ENCRYPTION must be exactly 32 characters. Anything else crashes at startup.
  • Losing the key is unrecoverable - the settings blob cannot be decrypted, and the server refuses to boot. Back up the key alongside (but separately from) your pb_data backups.
  • Rotating the key: start with the old key set, call app.Settings()app.Save(settings) to re-encrypt under the new key, then restart with the new key. Do this under a maintenance window.
  • Settings changes fire OnSettingsReload - use it if you have in-memory state that depends on a setting (e.g. a rate limiter sized from app.Settings().RateLimits.Default).
  • Do not call app.Settings() in a hot loop. It returns a fresh copy each time. Cache for the duration of a single request, not the process.
  • app.Save(settings) persists and broadcasts the reload event. Mutating the returned struct without saving is a no-op.

Reference: Settings · OnSettingsReload hook