Secrets Management for Self-Hosted Supabase: A Complete Vault Guide

Learn how to securely store API keys, tokens, and credentials in self-hosted Supabase using Vault with encryption at rest.

Cover Image for Secrets Management for Self-Hosted Supabase: A Complete Vault Guide

Every self-hosted Supabase deployment has secrets: API keys for third-party services, database credentials, OAuth tokens, and encryption keys. The question isn't whether you have secrets—it's where you're storing them and who can access them.

If you're hardcoding API keys in environment variables or worse, committing them to version control, you're one breach away from a security incident. Self-hosted Supabase includes a powerful secrets management solution called Vault that encrypts credentials at rest and makes them accessible only through SQL functions. This guide walks you through setting up and using Vault for production-grade secrets management.

Why Secrets Management Matters for Self-Hosted Supabase

When you deploy Supabase on your own server, you take responsibility for every secret in the system. Unlike Supabase Cloud where infrastructure secrets are managed for you, self-hosted instances require explicit configuration of:

  • JWT_SECRET: The master key for signing authentication tokens
  • ANON_KEY and SERVICE_ROLE_KEY: API keys for client and server access
  • VAULT_ENC_KEY: The encryption key for Supabase's internal secret storage
  • Third-party API keys: Stripe, SendGrid, Twilio, and other service credentials

The challenge? These secrets need to be accessible to your application while remaining protected from unauthorized access, accidental exposure in logs, and leakage through backups.

Traditional approaches fall short:

  • Environment variables: Visible in process listings, often logged, and awkward to rotate
  • Configuration files: Easy to accidentally commit to Git
  • Application code: The worst option—hardcoded secrets are impossible to rotate without redeployment

Supabase Vault solves this by storing encrypted secrets directly in PostgreSQL, making them accessible through SQL while keeping them encrypted at rest.

Understanding Supabase Vault Architecture

Vault is a PostgreSQL extension that uses pgsodium for authenticated encryption. When you store a secret, it's encrypted using Authenticated Encryption with Associated Data (AEAD), which means the data is both encrypted and signed—you can verify it wasn't tampered with.

Here's what happens when you store a secret:

  1. The secret is encrypted with your VAULT_ENC_KEY
  2. The encrypted value is stored in the vault.secrets table
  3. A view called vault.decrypted_secrets provides on-the-fly decryption
  4. Access is controlled through PostgreSQL roles and RLS policies

The key advantage: your backups contain encrypted secrets, and replication streams preserve the encryption. Even if someone gets a copy of your database, they can't read the secrets without the encryption key.

Setting Up Vault for Self-Hosted Supabase

Before using Vault, verify your self-hosted instance has the required extensions enabled. Connect to your database and run:

-- Check if pgsodium and vault are available
SELECT * FROM pg_extension WHERE extname IN ('pgsodium', 'vault');

Both extensions should be listed. If not, enable them:

CREATE EXTENSION IF NOT EXISTS pgsodium;
CREATE EXTENSION IF NOT EXISTS vault;

Configuring the Encryption Key

Your self-hosted Supabase should already have a VAULT_ENC_KEY in the .env file. If not, generate one:

openssl rand -hex 16

Add it to your environment configuration:

VAULT_ENC_KEY=your_32_character_hex_key_here

This key is critical—lose it, and you lose access to all your encrypted secrets. Store a backup in a secure location separate from your server.

Storing Secrets in Vault

Using SQL Functions

The cleanest way to store secrets is through Vault's built-in function:

SELECT vault.create_secret(
  'sk_live_your_stripe_secret_key',
  'stripe_api_key',
  'Stripe API key for payment processing'
);

The function returns a UUID you can use to reference the secret later. The parameters are:

  • The secret value
  • A unique name for retrieval
  • An optional description

Verifying Encryption

Check that your secret is actually encrypted by querying the raw table:

SELECT id, name, secret FROM vault.secrets WHERE name = 'stripe_api_key';

The secret column should show encrypted binary data, not your actual key. To see the decrypted value:

SELECT * FROM vault.decrypted_secrets WHERE name = 'stripe_api_key';

Security Warning: Disable Statement Logging

Here's a critical gotcha that catches many self-hosters: when you run INSERT statements to add secrets, PostgreSQL logs those statements by default. Your secrets appear in plain text in the logs.

Before adding secrets, disable statement logging for your session:

SET log_statement = 'none';
SET log_min_duration_statement = -1;

-- Now insert your secrets
SELECT vault.create_secret('your_secret', 'secret_name', 'description');

-- Re-enable logging if needed
RESET log_statement;
RESET log_min_duration_statement;

For production, consider disabling statement logging entirely for the schema or role that manages secrets.

Using Secrets in Database Functions

The real power of Vault comes from using secrets directly in PostgreSQL functions—no application code changes needed.

Creating a Secrets Helper Function

CREATE OR REPLACE FUNCTION get_secret(secret_name text)
RETURNS text
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
  secret_value text;
BEGIN
  SELECT decrypted_secret INTO secret_value
  FROM vault.decrypted_secrets
  WHERE name = secret_name;
  
  RETURN secret_value;
END;
$$;

-- Restrict access to service_role only
REVOKE ALL ON FUNCTION get_secret FROM PUBLIC;
GRANT EXECUTE ON FUNCTION get_secret TO service_role;

Using Secrets with pg_net for API Calls

A common pattern is combining Vault with pg_net to make secure API calls from database triggers. This is how you'd call an external API without exposing keys:

CREATE OR REPLACE FUNCTION notify_payment_webhook()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
  api_key text;
BEGIN
  -- Retrieve the API key from Vault
  SELECT decrypted_secret INTO api_key
  FROM vault.decrypted_secrets
  WHERE name = 'webhook_api_key';

  -- Make the HTTP request with pg_net
  PERFORM net.http_post(
    url := 'https://api.example.com/webhook',
    headers := jsonb_build_object(
      'Authorization', 'Bearer ' || api_key,
      'Content-Type', 'application/json'
    ),
    body := jsonb_build_object(
      'event', 'payment_completed',
      'amount', NEW.amount,
      'user_id', NEW.user_id
    )
  );
  
  RETURN NEW;
END;
$$;

The API key never leaves the database. Your application code doesn't need to know about it, and it's not exposed in any logs or environment variables.

Accessing Secrets from Your Application

Sometimes you need secrets in your application code—for example, to initialize an SDK. Use RPC calls with the service_role key:

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_SERVICE_ROLE_KEY
);

async function getStripeKey() {
  const { data, error } = await supabase
    .rpc('get_secret', { secret_name: 'stripe_api_key' });
  
  if (error) throw error;
  return data;
}

This approach requires the service_role key, so it's only suitable for server-side code. Never expose the service role key to clients.

Securing Access to Vault

Role-Based Access Control

Limit which database roles can access decrypted secrets:

-- Revoke default access to the decrypted view
REVOKE SELECT ON vault.decrypted_secrets FROM PUBLIC;
REVOKE SELECT ON vault.decrypted_secrets FROM anon;
REVOKE SELECT ON vault.decrypted_secrets FROM authenticated;

-- Only allow service_role and postgres
GRANT SELECT ON vault.decrypted_secrets TO service_role;
GRANT SELECT ON vault.decrypted_secrets TO postgres;

Row-Level Security for Secrets

For fine-grained control, add RLS policies to the vault tables. For example, to allow different teams access to different secrets:

-- Add a team column for categorization
ALTER TABLE vault.secrets ADD COLUMN IF NOT EXISTS team text;

-- Enable RLS
ALTER TABLE vault.secrets ENABLE ROW LEVEL SECURITY;

-- Create policies based on team membership
CREATE POLICY team_secrets ON vault.secrets
  FOR SELECT
  USING (team = current_setting('app.team', true));

Secrets Rotation Strategy

API keys should be rotated regularly—at minimum yearly, but ideally more frequently. Vault makes this straightforward:

-- Update an existing secret
UPDATE vault.secrets
SET secret = 'new_api_key_value'
WHERE name = 'stripe_api_key';

For zero-downtime rotation:

  1. Add the new secret with a versioned name (stripe_api_key_v2)
  2. Update your helper function to use the new version
  3. Test thoroughly
  4. Delete the old secret
-- Step 1: Add new secret
SELECT vault.create_secret('new_key', 'stripe_api_key_v2', 'Rotated key');

-- Step 2: Update function to use new key
-- Step 3: Test

-- Step 4: Delete old secret
DELETE FROM vault.secrets WHERE name = 'stripe_api_key';

Backup Considerations

When you back up your self-hosted Supabase, secrets remain encrypted in the dump. This is good—your backups are safe even if compromised. But remember:

  • Keep your VAULT_ENC_KEY separate from database backups
  • Store the encryption key in a different secure location (HSM, cloud secrets manager, or encrypted file)
  • Test restoration periodically to ensure you can decrypt secrets

If you lose the encryption key, your secrets are gone. There's no recovery option.

Integrating with External Secrets Managers

For enterprise deployments, you might want to integrate with external secrets managers like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault. The self-hosted community has been requesting support for *_FILE environment variable suffixes, which would allow injecting secrets from mounted files.

Until that's officially supported, you can:

  1. Use init containers to populate environment variables from external sources
  2. Create custom scripts that fetch secrets at startup
  3. Use Docker secrets mounted as files and read them during initialization

Check our guide on environment variables for more configuration options.

Managing Secrets with Supascale

Setting up Vault manually involves multiple steps: configuring encryption keys, managing access control, and ensuring backups are handled correctly. Supascale simplifies this by providing a unified dashboard for managing self-hosted Supabase instances, including secure configuration of authentication providers and service credentials.

With Supascale's OAuth provider configuration UI, you can securely set up Google, GitHub, Discord, and other authentication methods without manually editing environment files. The platform handles encryption key management and ensures your backups include all necessary configuration data.

Conclusion

Secrets management is one of those unglamorous but critical aspects of running production infrastructure. Supabase Vault provides a solid foundation for storing credentials securely, but it requires proper configuration—especially around logging, access control, and backup procedures.

Key takeaways:

  • Always disable statement logging when inserting secrets
  • Use helper functions to abstract secret retrieval
  • Restrict access to vault.decrypted_secrets to only necessary roles
  • Keep your VAULT_ENC_KEY backed up separately from your database
  • Rotate secrets regularly and test your rotation process

For teams looking to reduce the operational complexity of self-hosted Supabase, Supascale offers a streamlined approach to deployment and configuration management. Check out our pricing to see how a one-time purchase can simplify your self-hosting journey.

Further Reading