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:
- The secret is encrypted with your
VAULT_ENC_KEY - The encrypted value is stored in the
vault.secretstable - A view called
vault.decrypted_secretsprovides on-the-fly decryption - 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:
- Add the new secret with a versioned name (
stripe_api_key_v2) - Update your helper function to use the new version
- Test thoroughly
- 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:
- Use init containers to populate environment variables from external sources
- Create custom scripts that fetch secrets at startup
- 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_secretsto only necessary roles - Keep your
VAULT_ENC_KEYbacked 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.
