Your Database's Secret Identity Crisis: Secrets for Cron Jobs on Managed Postgres
May 02, 2026 • ArchyPress

3:07 AM. Your cron job fires, tries to authenticate an HTTP request, and fails silently because current_setting('app.settings.cron_secret') throws 'unrecognized configuration parameter'. The cron job has been broken for three days. Nobody noticed.
The Obvious Solution (That Doesn't Work)
You're running pg_cron on a managed Postgres instance (Supabase, RDS, Cloud SQL — pick your flavor). You need your cron job to make an authenticated HTTP request to your application. The cron job needs a secret token.
Every tutorial you find says the same thing:
-- "Just set a GUC parameter!"
ALTER DATABASE postgres
SET app.settings.cron_secret = 'my-secret-token';
And on a self-hosted Postgres instance, this works beautifully. Custom GUC (Grand Unified Configuration) parameters let you stash application settings right in the database config, accessible via current_setting() in any SQL context.
But on managed Postgres? You get this:
ERROR: 42501: permission denied to set parameter "app.settings.cron_secret"
Why Managed Postgres Says No
Managed database providers restrict ALTER DATABASE ... SET for custom parameters because:
The postgres role on managed instances isn't actually a superuser — it's a carefully permissioned role with specific grants
Custom GUC parameters can interfere with platform operations, extensions, and internal tooling
There's no access control on GUC parameters — any role that can connect to the database can read them via current_setting()
The platform can't audit or rotate values stored in GUC parameters
This isn't a bug. It's a deliberate security boundary. GUC parameters were designed for server configuration, not secret storage. The fact that people use them for secrets is a workaround that managed platforms (rightly) shut down.
The Three Approaches (And Which Actually Works)
❌ GUC Parameters
Store the secret in ALTER DATABASE SET, read via current_setting(). Doesn't work on managed instances. Zero audit trail. Zero rotation support.
❌ Inline Secrets
Hardcode the secret directly in the cron job SQL definition. Works but is terrible — visible in pg_cron job listings, no rotation without re-creating the job.
✅ Vault / Secret Store
Store the secret in a dedicated secrets store (Supabase Vault, AWS Secrets Manager, etc.) and read it at runtime from SQL. Works everywhere. Auditable. Rotatable.
The Vault Pattern
On Supabase, the built-in Vault extension provides encrypted secret storage with transparent decryption at query time. Here's how the pattern works:
-- Step 1: Store the secret (run once, manually or via migration)
SELECT vault.create_secret('my-cron-bearer-token', 'cron_secret');
-- Step 2: Read the secret at runtime in your cron job
SELECT cron.schedule('publish-due', '* * * * *',
$$
SELECT net.http_post(
url := 'https://my-app.com/hooks/publish-due',
headers := jsonb_build_object(
'Authorization', 'Bearer ' || (
SELECT decrypted_secret
FROM vault.decrypted_secrets
WHERE name = 'cron_secret'
LIMIT 1
)
)
);
$$
);
The secret lives encrypted at rest. The vault.decrypted_secrets view handles transparent decryption. The cron job never sees a plaintext secret in its definition — it resolves at runtime.
Why This Matters Beyond Cron Jobs
The GUC parameter pattern has a deeper problem even on platforms that allow it: there's no access control. Any role that can execute SQL can call current_setting('app.settings.anything'). That means:
If your application has a SQL injection vulnerability, the attacker gets all your GUC secrets for free
Any Postgres extension or function running in the same database can read the values
There's no audit log of who accessed the parameter or when
Rotation requires ALTER DATABASE + restart/reconnect — no graceful transition
Vault-based storage adds a proper security boundary: encrypted at rest, access controlled by RLS policies, and auditable.
Cross-Platform Equivalents
If you're not on Supabase, the same pattern applies with different secret stores:
Supabase
vault.create_secret() + vault.decrypted_secrets view. Built into every project. RLS-gated access.
AWS RDS
aws_commons.create_aws_credentials() + aws_lambda for fetching from Secrets Manager. Requires the aws_lambda extension.
GCP Cloud SQL
Use Cloud SQL connector with IAM auth. For pg_cron specifically, store secrets in a restricted table with row-level permissions.
Azure Flexible Server
Azure Key Vault integration via azure_keyvault extension (in preview). Alternatively, use a restricted secrets table.
The Migration Path
If you're currently using GUC parameters for secrets (and it's working because you're self-hosted), here's how to migrate:
Create secrets in your platform's vault/store with the same values
Update your SQL functions to read from the vault instead of current_setting()
Verify the cron jobs work with the new secret source
Remove the ALTER DATABASE SET statements
Delete the GUC parameters (ALTER DATABASE RESET)
The migration is non-breaking because both sources return the same value during the transition period.
Operational Benefits You Get For Free
Moving to a proper secrets store isn't just about 'permission denied' errors. You also get:
Secret rotation without restarting connections or redefining cron jobs — just update the vault entry
Audit trail of who created and when they last accessed each secret
Multiple environments without code changes — same SQL reads from vault in dev, staging, and production, each with different stored values
Separation of concerns — the DBA/infrastructure team manages secrets; the cron job SQL doesn't embed them
The Takeaway
If you're building on managed Postgres and your cron jobs need secrets:
Don't fight the platform. If ALTER DATABASE says no, it's protecting you from a pattern that was never meant for secrets.
Use the platform's native secret store. Every managed Postgres provider has one.
Resolve secrets at runtime, not at definition time. Your cron job SQL should query for the secret, not embed it.
The three-AM cron failure that started this post? It was the best kind of production incident — the kind that forces you to adopt a better pattern than the one you had.
Running cron jobs on managed Postgres?
ArchySocial uses Supabase Vault for all cron job secrets — rotatable, auditable, and it works at 3 AM without you.