Your Database's Secret Identity Crisis: Secrets for Cron Jobs on Managed Postgres

May 02, 2026 • ArchyPress

null

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

Comparison diagram showing unsafe (secret in GUC) vs safe (secret in Vault) patterns

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:

  1. Create secrets in your platform's vault/store with the same values

  2. Update your SQL functions to read from the vault instead of current_setting()

  3. Verify the cron jobs work with the new secret source

  4. Remove the ALTER DATABASE SET statements

  5. 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:

  1. Don't fight the platform. If ALTER DATABASE says no, it's protecting you from a pattern that was never meant for secrets.

  2. Use the platform's native secret store. Every managed Postgres provider has one.

  3. 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.

© 2026 Meet Archy
Your Database's Secret Identity Crisis: Secrets for Cron Jobs on Managed Postgres | Archy Engineering | ArchyPress Platform