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_ENCRYPTIONenv 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_ENCRYPTIONmust 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_databackups. - 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 fromapp.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