This repository implements the Secure Media Vault spec: signed uploads, row-scoped access, expiring links, single-use upload tickets and MIME sniffing.
- Install Node 18+ and pnpm.
pnpm install- Create Supabase project and a private storage bucket named
private - Copy
.env->.envin these locations and fill values:secure-media-vault/apps/api/.env->secure-media-vault/apps/api/.envsecure-media-vault/apps/web/.env->secure-media-vault/apps/web/.envsecure-media-vault/edge/hash-object/.env->secure-media-vault/edge/hash-object/.env
- Apply SQL in
supabase/migrations/001_init.sqlusing Supabase SQL editor or CLI. - Get user uuid by creating a user in Supabase.This will be used to authenticate user.
- Start servers:
pnpm dev
SUPABASE_URL- your Supabase instance URLSUPABASE_SERVICE_ROLE- service_role key (must not be committed to repo)SUPABASE_ANON_KEY- anon/public keySUPABASE_PRIVATE_BUCKET- name of private bucket (default:private)VITE_GRAPHQL_URL(client) - e.g.http://localhost:4000/graphqlVITE_USER_ID- UUIDEDGE_HASH_URL(optional) - deployedge/hash-objectand set this to call it from API (optional)
Follow these steps to obtain the required keys for configuration:
- Go to your Supabase Dashboard.
- Open your project.
- Navigate to Project Settings → API.
- Copy the Project URL → this is your Supabase URL.
- In Project Settings → API, scroll to Project API keys.
- Copy the anon public key.
- ✅ Safe to use in the frontend.
- In Project Settings → API, under Project API keys, copy the service_role secret key.
⚠️ Use this only in the backend/server.- ❌ Never expose this key in client-side code.
- Go to Authentication → Users in the Supabase dashboard.
- Click Add User and create a new user (email/password or magic link).
- After creating, open the Users table → you will see a column named ID.
- This ID is the User UUID.
Security note: Ensure SUPABASE_SERVICE_ROLE is provided only via environment variables (CI secret or server environment). Do not commit service role keys into source control.
This project protects private binary objects stored in a Supabase private bucket. Threats considered and mitigations:
-
Unauthorized read (stolen access token / public links)
- Mitigation: short-lived signed URLs (90s). Server validates whether requester is owner or has explicit share.
-
Upload tampering / integrity bypass
- Mitigation: two-step upload with single-use upload ticket. Server computes SHA-256 of stored object and compares with client-provided
clientSha256. If mismatch, asset is markedcorrupt.
- Mitigation: two-step upload with single-use upload ticket. Server computes SHA-256 of stored object and compares with client-provided
-
Replay of ticket / double-use
- Mitigation: upload tickets are marked used atomically during
finalizeUpload. Subsequent attempts fail.
- Mitigation: upload tickets are marked used atomically during
-
Path traversal / storage path confusion
- Mitigation: filenames sanitized (
..removed, unsafe chars replaced). Storage path includes a random UUID and owner id. Additional recommendation: apply Unicode normalization and disallow control characters.
- Mitigation: filenames sanitized (
-
Information leakage via share or download links
- Mitigation: shares map to user IDs and are row-scoped; download links are ephemeral. Logging is recorded to
download_auditfor investigation.
- Mitigation: shares map to user IDs and are row-scoped; download links are ephemeral. Logging is recorded to
- Edge hashing vs server hashing
- Edge function reduces load on API (compute & memory) and can be deployed closer to storage. Server-side hashing removes need to deploy the edge function and simplifies local dev. This repo provides both: server computes by default;
EDGE_HASH_URLis available for optional use.
- Edge function reduces load on API (compute & memory) and can be deployed closer to storage. Server-side hashing removes need to deploy the edge function and simplifies local dev. This repo provides both: server computes by default;
- Signed URL TTL
- Short TTL (90s) limits exposure but requires clients to complete downloads quickly. Suitable for direct downloads; longer TTLs increase risk if leaked.
- Two-step upload
- Adds complexity but gives atomic integrity checks and single-use ticket guarantees.
There are Jest tests under apps/api/src/__tests__/. Two acceptance tests were added:
- Version conflict test (ensures resolver rejects stale versions).
- Hash/integrity test (ensures corrupt uploads are handled).
Run tests:
pnpm testAdd your demo video link here after recording the acceptance checks (8 checks mentioned in the spec):
- Demo video: (paste link here)
Apply supabase/migrations/001_init.sql in your Supabase project. That SQL creates asset, upload_ticket, asset_share, download_audit, and RLS policies.