integrations
Supabase Storage signed URLs
A secure file-delivery integration that keeps buckets private while enabling downloads through time-limited signed URLs generated server-side. This guide focuses on the practical steps, the workflows that matter in production, and the verification checks that prevent false confidence.
What “Supabase Storage signed URLs” gives you
A secure file-delivery integration that keeps buckets private while enabling downloads through time-limited signed URLs generated server-side.
Threat model for Supabase Storage signed URLs (what it blocks)
- Direct client access to privileged tables, Storage objects, or RPC functions.
- Authorization logic living only in the UI (bypass via direct API calls).
- Accidental secret leakage (service_role) into browser bundles or public env vars.
- Configuration drift that silently widens access over time (policies, grants, bucket settings).
Prerequisites
- Private Storage buckets (public = false)
- A backend endpoint that can use service_role
- A file ownership model (who is allowed to download what)
Setup steps for Supabase Storage signed URLs
Follow these steps in order. If you skip verification, you can end up with a working UI and an exposed backend.
- Set buckets to private (public = false).
- Remove any public listing rules (bucket/object list access).
- Standardize object names to non-guessable identifiers (e.g., UUIDs).
- Implement an API route that validates user ownership of the object path.
- Generate a signed URL with a short expiration (minutes, not days).
- Return only the signed URL to the client (no keys).
- Add monitoring for repeated signing attempts (abuse patterns).
Implementation notes for Supabase Storage signed URLs (what to change in your app)
Integrations aren’t just configuration — they usually require changing how your app talks to Supabase.
- Replace direct client Supabase calls with backend endpoints for privileged operations.
- Keep service_role and other secrets in server-only environment variables (never
NEXT_PUBLIC_*). - If you return signed URLs, treat them as sensitive: short TTL, no logging, no persistence.
- Add explicit authorization checks in server code (ownership, membership, tenancy).
Best-fit use cases
- User profile photo downloads
- Private invoices and receipts
- Customer documents
- One-time share links with expiration
Workflow examples (real sequences you can implement)
Invoice download
- Client requests /api/invoices/{id}/download.
- Server checks invoice ownership.
- Server signs the storage object path for the invoice PDF.
- Client fetches via signed URL.
Expiring share link
- User clicks “Share”.
- Server generates a signed URL valid for 10 minutes.
- User shares the URL.
- After expiry, the link no longer works.
How to verify Supabase Storage signed URLs (avoid false confidence)
Verification is the difference between “we changed something” and “we reduced exposure.”
- Direct client access to the protected resource should fail (401/403).
- Backend endpoint should succeed for authorized users.
- Secrets (service_role) must not appear in browser bundle, logs, or client env vars.
- Re-scan after implementation to confirm exposure is gone.
Common pitfalls when implementing Supabase Storage signed URLs
- Leaving one direct client code path in place (the exposure still exists).
- Generating signed URLs client-side (turns private files into shareable public links).
- Using long-lived signed URLs and accidentally logging or caching them.
- Revoking grants before the backend endpoint is deployed (causes outages and rollback pressure).
- Fixing dev but forgetting to replicate changes in staging/prod (drift).
Operational checklist (keep Supabase Storage signed URLs safe over time)
- Re-run the verification checks after every migration that touches auth, policies, grants, functions, or storage.
- Audit grants and bucket settings periodically in production (don’t assume they stay correct).
- Monitor for denied access spikes after tightening permissions (often reveals missed app paths).
- Document the “why” and the verification steps so future changes don’t reintroduce exposure.
Rollback plan for Supabase Storage signed URLs (without reopening exposure)
- If something breaks, roll back application behavior first (restore backend endpoint responses) rather than re‑granting direct client access.
- Use logs of denied direct access to identify missed call paths and patch them.
- If you must create a temporary exception, make it narrow and time-limited, then remove it once the backend path is fixed.
Regression tests for Supabase Storage signed URLs (keep it secure after migrations)
Treat this integration like a contract: it should keep working after schema changes and refactors.
- Keep a direct access test (REST/RPC/Storage) that should always fail for client credentials.
- Add a checklist query or scan that flags new public grants, policies, buckets, or EXECUTE permissions.
- Verify secrets are still server-only (no public env vars, no bundle leakage).
- Re-run the workflow examples after major changes to confirm the user-facing flow still works securely.
Security invariants for Supabase Storage signed URLs (non‑negotiables)
If you want this pattern to stay secure over time, a few statements must remain true no matter how your schema evolves.
Use these as invariants in reviews and release checklists:
- The browser cannot reach privileged data surfaces directly (tables, Storage objects, privileged RPC).
- Privileged operations go through server code that enforces authorization (and rate limits where abuse is plausible).
- Secrets used for privileged access remain server-only and are never logged or persisted (especially service_role).
- After migrations, the same direct-access tests still fail for client credentials; drift is caught early.
- When you intentionally allow client access, the decision is documented and tested (not accidental).
If any invariant is violated, treat it as a bug: restore the boundary first, then iterate on features.
Minimal test cases for Supabase Storage signed URLs (fast confidence)
You don’t need a huge test suite to keep this safe. You need a small set of high-signal checks that match attacker behavior.
- Attempt the direct client call (REST/RPC/Storage) for the protected operation and confirm it fails with a clear denial.
- Call the backend endpoint as an authorized user and confirm it succeeds with minimal data returned.
- Call the backend endpoint as an unauthorized user and confirm it fails without leaking sensitive metadata.
- If you use signed URLs: confirm they are short-lived and that re-use after expiry fails.
- Run one drift check after a migration that touches auth, policies, grants, buckets, or functions.
These checks validate the boundary, not just the UI behavior.
Quick recap: Supabase Storage signed URLs
If you remember only one thing, remember the boundary: privileged operations must not be reachable directly from the browser.
- Backend endpoints own privileged reads/writes/downloads.
- Secrets stay server-only.
- Direct client access tests fail after the fix.
- Drift checks run after migrations.
Related glossary terms and templates
- Glossary: Row Level Security (RLS) →
/glossary/row-level-security - Glossary: Public Table Exposure →
/glossary/public-table-exposure - Glossary: Over-permissive RLS Policies →
/glossary/over-permissive-rls-policies - Glossary: Supabase Storage Bucket Privacy →
/glossary/supabase-storage-bucket-privacy - Glossary: RPC EXECUTE Grants →
/glossary/rpc-execute-grants - Glossary: Signed URLs →
/glossary/signed-urls - Glossary: Service Role Key →
/glossary/service-role-key - Glossary: Forced RLS (FORCE ROW LEVEL SECURITY) →
/glossary/force-row-level-security - Glossary: Client Role Grants (anon/authenticated) →
/glossary/client-role-grants - Glossary: Ownership-bound RLS Policies →
/glossary/ownership-bound-rls-policies - Glossary: Storage Object Enumeration →
/glossary/storage-object-enumeration - Glossary: Public RPC Surface Area →
/glossary/public-rpc-surface-area - Glossary: NEXT_PUBLIC Secret Leakage →
/glossary/next-public-secret-leakage - Glossary: Broken Object Level Authorization (BOLA) →
/glossary/broken-object-level-authorization - Glossary: Insecure Direct Object References (IDOR) →
/glossary/insecure-direct-object-references - Glossary: Tenant ID Trusted from Client →
/glossary/tenant-id-trust-in-client - Glossary: Missing WITH CHECK Policy →
/glossary/missing-with-check-policy - Glossary: Broad SELECT for Authenticated Role →
/glossary/broad-authenticated-select - Glossary: Broad UPDATE for Authenticated Role →
/glossary/broad-authenticated-update - Glossary: Broad DELETE for Authenticated Role →
/glossary/broad-authenticated-delete - Glossary: Default Privilege Drift →
/glossary/default-privilege-drift - Glossary: Schema USAGE Granted to PUBLIC →
/glossary/schema-usage-granted-to-public - Glossary: Exposed Materialized Views →
/glossary/exposed-materialized-views - Glossary: Insecure SECURITY DEFINER Functions →
/glossary/insecure-security-definer-functions - Glossary: Mutable Function search_path →
/glossary/mutable-function-search-path - Glossary: Trigger Privilege Escalation →
/glossary/trigger-privilege-escalation - Glossary: Migration Owner Bypass of RLS →
/glossary/migration-owner-bypass - Glossary: Shadow Table Without RLS →
/glossary/shadow-table-without-rls - Glossary: Audit Log Table Publicly Readable →
/glossary/audit-log-public-readable - Glossary: Soft Delete Policy Bypass →
/glossary/soft-delete-policy-bypass - Glossary: UPSERT Policy Gap →
/glossary/upsert-policy-gap - Glossary: Cross-Schema Data Exposure →
/glossary/cross-schema-exposure - Glossary: Unrestricted View Definitions →
/glossary/unrestricted-view-definitions - Glossary: Leaked JWT Signing Secret →
/glossary/leaked-jwt-secret - Glossary: Stale JWT Claims →
/glossary/stale-jwt-claims - Glossary: Auth Role Claim Confusion →
/glossary/auth-role-claim-confusion - Glossary: Magic Link Open Redirect →
/glossary/magic-link-redirect-open-redirect - Glossary: Password Reset Token Leakage →
/glossary/password-reset-token-leakage - Glossary: OAuth Role Mapping Errors →
/glossary/oauth-role-mapping-errors - Glossary: SSO Group Sync Escalation →
/glossary/sso-group-sync-escalation - Glossary: Invite Flow Tenant Escalation →
/glossary/invite-flow-tenant-escalation - Glossary: Membership Race Condition →
/glossary/membership-race-condition - Glossary: Admin Panel Client-Only Auth →
/glossary/admin-panel-client-auth-only - Glossary: Database URL Leaked in Client →
/glossary/leaked-database-url-in-client - Glossary: Secrets in Repository History →
/glossary/secrets-in-repo-history - Glossary: Environment Parity Security Drift →
/glossary/env-parity-security-drift - Glossary: Staging Database Public Exposure →
/glossary/staging-db-public-exposure - Glossary: Test Data Left in Production →
/glossary/test-data-in-production - Glossary: Unencrypted Sensitive Columns →
/glossary/unencrypted-sensitive-columns - Glossary: Missing Key Rotation Policy →
/glossary/key-rotation-policy-missing - Glossary: Service Role Overreach in Cron Jobs →
/glossary/service-role-overreach-in-cron - Glossary: PII in Error Traces →
/glossary/pii-in-error-traces - Glossary: PII in Analytics Events →
/glossary/pii-in-analytics-events - Glossary: Missing Data Retention Policy →
/glossary/data-retention-policy-missing - Glossary: Incomplete GDPR Delete Flow →
/glossary/incomplete-gdpr-delete-flow - Glossary: No Exfiltration Anomaly Detection →
/glossary/no-anomaly-detection-exfiltration - Glossary: Weak Tenant Isolation Tests →
/glossary/weak-tenant-isolation-tests - Glossary: Policy Drift After Schema Rename →
/glossary/policy-drift-after-schema-rename - Glossary: Orphaned Policies After Table Rename →
/glossary/orphaned-policies-after-table-rename - Glossary: Generated Columns Leak Sensitive Data →
/glossary/generated-columns-sensitive-leak - Glossary: View Without Security Barrier →
/glossary/view-without-security-barrier - Glossary: Missing Column-Level Redaction →
/glossary/column-level-redaction-missing - Glossary: Unrestricted PostgREST Origin Proxy →
/glossary/unrestricted-postgrest-origin - Glossary: CORS Misconfiguration in Edge Functions →
/glossary/cors-misconfiguration-edge-functions - Glossary: Missing Webhook Signature Validation →
/glossary/missing-webhook-signature-validation - Glossary: Webhook Replay Attack Risk →
/glossary/webhook-replay-attack-risk - Glossary: Billing Webhook Idempotency Gap →
/glossary/billing-webhook-idempotency-gap - Glossary: Insecure Edge Function Authentication →
/glossary/insecure-edge-function-auth - Glossary: Edge Function Service Role Overuse →
/glossary/edge-function-service-role-overuse - Glossary: RPC Dynamic SQL Injection →
/glossary/rpc-dynamic-sql-injection - Glossary: RPC Missing Input Validation →
/glossary/rpc-missing-input-validation - Glossary: RPC Unbounded Result Sets →
/glossary/rpc-unbounded-result-set - Glossary: RPC Error Message Data Leak →
/glossary/rpc-error-data-leak - Glossary: Public Function Source Disclosure →
/glossary/public-function-source-disclosure - Glossary: Overloaded RPC Signature Miss →
/glossary/overloaded-rpc-signature-miss - Glossary: Unrestricted Admin Search Endpoint →
/glossary/unrestricted-admin-search-endpoint - Glossary: Bulk Export Endpoint Overexposure →
/glossary/bulk-export-endpoint-overexposure - Glossary: CSV Import Trusts Client Columns →
/glossary/csv-import-trusts-client-columns - Glossary: Row Ownership Transfer Without Recheck →
/glossary/row-ownership-transfer-without-recheck - Glossary: File Upload MIME Spoofing →
/glossary/file-upload-mime-spoofing - Glossary: Storage Upload Size Abuse →
/glossary/storage-upload-size-abuse - Glossary: Missing Malware Scanning on Uploads →
/glossary/missing-malware-scanning-uploads - Glossary: Public Backup Bucket Leak →
/glossary/public-backup-bucket-leak - Glossary: Expired Signed URL Caching Leak →
/glossary/expired-signed-url-caching-leak - Glossary: Object Path Predictability Risk →
/glossary/object-path-predictability-risk - Glossary: Storage Lifecycle Policy Missing →
/glossary/storage-lifecycle-policy-missing - Glossary: Bucket LIST Permission Too Broad →
/glossary/bucket-list-permission-too-broad - Glossary: Realtime Channel Authorization Gap →
/glossary/realtime-channel-authorization-gap - Glossary: Realtime Presence Data Leak →
/glossary/realtime-presence-data-leak - Glossary: Publication Includes Sensitive Tables →
/glossary/publication-includes-sensitive-tables - Glossary: Replication Role Overgrant →
/glossary/replication-role-overgrant - Glossary: Unbounded Pagination Enumeration →
/glossary/unbounded-pagination-enumeration - Glossary: Guessable Primary Keys →
/glossary/guessable-primary-keys - Glossary: Missing Rate Limits on Write Paths →
/glossary/rate-limit-missing-on-write-paths - Glossary: Missing CAPTCHA on Sensitive Flows →
/glossary/missing-captcha-sensitive-flows - Glossary: Insecure Feature Flag Disclosure →
/glossary/insecure-feature-flag-disclosure - Glossary: No Two-Person Review for Privilege Changes →
/glossary/no-two-person-review-privilege-changes - Glossary: Dependency Drift Misses Security Updates →
/glossary/dependency-drift-security-updates-missed - Glossary: Private Key Material in Logs →
/glossary/private-key-material-in-logs - Glossary: API Cache Leaks Private Data →
/glossary/api-cache-private-data-leak - Glossary: Data API Public Schema Exposure →
/glossary/data-api-public-schema-exposure - Glossary: Data API Custom Schema Misconfiguration →
/glossary/data-api-custom-schema-misconfiguration - Glossary: pg_graphql Extension Exposure →
/glossary/pg-graphql-extension-exposure - Glossary: Realtime Public Channel Mode →
/glossary/realtime-public-channel-mode - Glossary: Realtime Topic Policy Mismatch →
/glossary/realtime-topic-policy-mismatch - Glossary: Realtime Broadcast Overexposure →
/glossary/realtime-broadcast-overexposure - Glossary: Edge Function JWT Verification Gap →
/glossary/edge-function-jwt-verification-gap - Glossary: Service Role Authorization Header Override →
/glossary/service-role-authorization-header-override - Glossary: Publishable vs Secret Key Scope Confusion →
/glossary/publishable-secret-key-scope-confusion - Glossary: Missing Network Restrictions →
/glossary/missing-network-restrictions - Glossary: IPv6 Allowlist Gap →
/glossary/ipv6-allowlist-gap - Glossary: Default Function EXECUTE to PUBLIC →
/glossary/default-function-execute-to-public - Glossary: Untrusted Language Function Risk →
/glossary/untrusted-language-function-risk - Glossary: Storage Authenticated Endpoint Overtrust →
/glossary/storage-authenticated-endpoint-overtrust - Template: Lock down a public table (backend-only access) →
/templates/access-control/lock-down-public-table - Template: Make a bucket private + serve files with signed URLs →
/templates/storage-safety/make-bucket-private-signed-urls - Template: Lock down RPC: revoke EXECUTE from public roles →
/templates/rpc-functions/lock-down-rpc-execute - Template: Remove over-permissive RLS policies (adopt deny-by-default) →
/templates/access-control/remove-over-permissive-policies
FAQ
Is this pattern compatible with RLS?
Yes. Backend-only access works with any RLS posture. Many teams use RLS as a safety gate while keeping most access logic in server endpoints.
What’s the main failure mode of this integration?
Accidentally leaking secrets to the browser (service_role) or leaving a direct client path in place. Verify by testing direct access and auditing your build output.
How do I know this isn’t overkill?
If the resource is sensitive (user data, billing, exports, private files), backend-only access is usually the simplest path to predictable security.
Next step
If you want to confirm which surfaces in your project need this integration, run a scan in Mockly and follow the linked remediation steps.
Explore related pages
sibling
Astro API Rate Protection Supabase security integration/integrations/astro-api-rate-protection-supabase-security
sibling
Astro Auth Context Validation Supabase security integration/integrations/astro-auth-context-validation-supabase-security
cross
Make a bucket private + serve files with signed URLs/templates/storage-safety/make-bucket-private-signed-urls