Secure your self-hosted Supabase installation for production use
The default Supabase Docker configuration is not secure for production. This guide covers essential security measures you must implement before deploying.
Never deploy with default configuration! Default passwords and secrets are publicly known and will be exploited.
-- Enable RLS on tableALTER TABLE posts ENABLE ROW LEVEL SECURITY;-- Users can only read their own postsCREATE POLICY "Users can read own posts" ON posts FOR SELECT USING (auth.uid() = author_id);-- Users can only insert their own postsCREATE POLICY "Users can insert own posts" ON posts FOR INSERT WITH CHECK (auth.uid() = author_id);-- Users can only update their own postsCREATE POLICY "Users can update own posts" ON posts FOR UPDATE USING (auth.uid() = author_id) WITH CHECK (auth.uid() = author_id);-- Users can only delete their own postsCREATE POLICY "Users can delete own posts" ON posts FOR DELETE USING (auth.uid() = author_id);
Tables without RLS policies are accessible to anyone with the service_role key!
-- Revoke unnecessary permissionsREVOKE ALL ON SCHEMA public FROM PUBLIC;GRANT USAGE ON SCHEMA public TO anon, authenticated;-- Limit function executionREVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA public FROM PUBLIC;GRANT EXECUTE ON FUNCTION your_safe_function TO authenticated;-- Disable dangerous extensionsDROP EXTENSION IF EXISTS plpythonu;DROP EXTENSION IF EXISTS plperlu;-- Enable query logging for auditingALTER DATABASE postgres SET log_statement = 'all';ALTER DATABASE postgres SET log_duration = on;
# Restrict redirect URLsADDITIONAL_REDIRECT_URLS=https://app.yourdomain.com,https://staging.yourdomain.com# Disable signups if using OAuth onlyDISABLE_SIGNUP=true# Verify email for OAuth usersGOTRUE_EXTERNAL_EMAIL_ENABLED=trueGOTRUE_MAILER_AUTOCONFIRM=false
-- Create private bucketINSERT INTO storage.buckets (id, name, public)VALUES ('avatars', 'avatars', false);-- Users can upload their own avatarCREATE POLICY "Users can upload avatar" ON storage.objects FOR INSERT WITH CHECK ( bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1] );-- Users can read their own avatarCREATE POLICY "Users can read avatar" ON storage.objects FOR SELECT USING ( bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1] );
-- Track failed login attemptsCREATE TABLE failed_logins ( id BIGSERIAL PRIMARY KEY, email TEXT, ip_address INET, attempted_at TIMESTAMPTZ DEFAULT NOW());-- Alert on multiple failuresCREATE OR REPLACE FUNCTION check_failed_logins()RETURNS TRIGGER AS $$DECLARE failure_count INTEGER;BEGIN SELECT COUNT(*) INTO failure_count FROM failed_logins WHERE email = NEW.email AND attempted_at > NOW() - INTERVAL '15 minutes'; IF failure_count >= 5 THEN -- Block or alert RAISE EXCEPTION 'Too many failed login attempts'; END IF; RETURN NEW;END;$$ LANGUAGE plpgsql;