Serverless file sharing on AWS: direct-to-S3 uploads (no size cap), short-lived presigned downloads, email sharing with expiring links, optional per-user KMS encryption, and a sleek React front end. Authentication is delegated to a separate auth microservice (e.g. CognitoApi) - Enclave only knows its endpoint.
Note: this project was formerly
small-file-sharing. Uploads now stream directly to S3 via presigned URLs, so the old ~10 MB limit is gone - hence the rename to Enclave.
- π Microservice-decoupled auth - Enclave validates and resolves the caller by calling the auth
service's
/v1/userinfo. No identity store, SDK, or infrastructure is shared; the only auth config is just an endpoint. - β¬οΈ Direct-to-S3 uploads - the browser PUTs straight to S3 against a presigned URL (no size cap, real progress) using SigV4 on the regional endpoint.
- β¬οΈ Owner downloads & email sharing - short-lived (1 hour) presigned links; share to anyone by email via SES.
- π Optional per-user KMS encryption -
kms_modeofs3(free),shared(one customer key), orper_user(one KMS key per user - true cryptographic isolation). - ποΈ Rich metadata - name, size, content type, upload date and status in DynamoDB.
- π§± One-command IaC -
make applybuilds + deploys everything;make destroyremoves it all, including the runtime KMS keys and every stored object. No orphans. - π₯οΈ Modern front end - dark, brand-matched React 19 + Vite + Tailwind app (sign-up, MFA login, dashboard).
Browser ββ presigned PUT (SigV4) ββββββββββΆ S3 (private, optional SSE-KMS)
β β²
β REST (Bearer IdToken) β presigned GET (download / share)
βΌ β
API Gateway βββΆ Lambda (Python) βββΆ DynamoDB (file metadata + per-user key lock)
β β
β ββββΆ KMS (per-user key, created once via a DynamoDB lock)
β
ββββΆ Auth microservice /v1/userinfo (validate token, resolve user)
ββββΆ SES (share emails)
The API methods are open (no API key); the bearer token is validated in the Lambda by calling the auth service - there is no Cognito authorizer, so Enclave is independent of the auth implementation.
All endpoints require Authorization: Bearer {IdToken}. The Lambda resolves user_id
from the auth service and enforces that the path user matches the token.
| Action | Request | Response |
|---|---|---|
| Create upload | POST /v1/users/{user_id}/files{file_name, content_type, size} |
{file_id, upload_url, upload_headers, status:"UPLOADING"} |
| (browser) | PUT {upload_url} with the bytes + Content-Type (+ upload_headers for SSE-KMS) |
S3 stores the object |
| Complete upload | POST /v1/users/{user_id}/files/{file_id}/complete |
{file_id, size, status:"READY"} |
| List files | GET /v1/users/{user_id}/files |
{user_id, user_files:[{file_id, file_name, content_type, size, uploaded_at, status}]} |
| Download | GET /v1/users/{user_id}/files/{file_id}/download |
{file_id, file_name, download_url} (1 h) |
| Share | POST /v1/users/{user_id}/files/{file_id}/share{share_with:[emails]} |
{file_id, file_name, status:"SHARED"} + emails a 1 h link |
| Delete | DELETE /v1/users/{user_id}/files/{file_id} |
{file_id, file_name, file_status:"DELETED"} |
Objects are stored under the key {user_id}/{file_id}/{file_name}.
Provisions the S3 bucket (+ CORS), DynamoDB, the 6 Python Lambdas + dependency layer, API Gateway
(routes), IAM, KMS (per kms_mode), and an optional custom domain - all from
terraform/. Terraform packages the Lambdas itself (no manual build step).
Prerequisites: Terraform β₯ 1.5, AWS CLI configured, Python 3 + pip + zip, a running auth
microservice, and a verified SES identity for the sender address.
cd terraform
cp terraform.tfvars.example terraform.tfvars # then fill in the values
make apply # builds lambdas + layers, deploys everything
make output # api_url, bucket, tableKey terraform.tfvars values:
| Variable | Purpose |
|---|---|
auth_endpoint |
the auth microservice this API delegates token validation to |
files_bucket_name |
globally-unique S3 bucket |
sender_email, sender_ses_arn |
verified SES identity for share emails |
allowed_origins |
frontend origins allowed to upload/download directly to S3 |
kms_mode |
s3 | shared | per_user (see below) |
api_domain_name, route53_zone_id |
optional custom domain (Enclave issues its own ACM cert) |
make destroy tears everything down β including the per-user KMS keys created at runtime (a
destroy-time hook deletes their aliases and schedules the keys for deletion) and all stored objects
(force_destroy). Nothing is left orphaned. (KMS enforces a 7-day minimum deletion window; keys are
disabled immediately, so no further access or billing.)
| Mode | What it does | Cost |
|---|---|---|
s3 (default) |
SSE-S3 (AES-256) | free |
shared |
one customer-managed KMS key, set as the bucket default | ~$1/mo + requests |
per_user |
one KMS key per user, created on first upload and reused thereafter | ~$1/user/mo + requests |
In per_user mode each user's objects are encrypted under their own key, so one user's data can
never be decrypted with another's. The key is created exactly once per user, guarded by an atomic
DynamoDB conditional write (no races, no orphaned keys), and the browser PUT carries the SSE-KMS headers
returned in upload_headers.
A standalone, deployable product (not a demo): a dark, brand-matched React app - landing page, sign-up (create account β email confirmation β mandatory TOTP MFA) and sign-in, then a dashboard with drag-and-drop uploads (live progress), a type-aware file library with search/sort, and Download / Share / Copy-link / Delete.
It's an independent microservice client: its dependencies (the auth service and the Enclave File API) are wired in at build time via env vars - there is no runtime "connect your API" step.
cd web
npm install
cp .env.example .env.local # fill in your endpoints
npm run dev # http://localhost:3001 (landing -> sign up / sign in -> app)Configuration (set before npm run build, e.g. in CI):
VITE_AUTH_URL=https://auth.example.com # auth microservice
VITE_FILE_URL=https://files.example.com # Enclave File API (make output); omit if same gateway
npm run build produces a static dist/. Host it on S3 + CloudFront + ACM + Route53 at your domain
(e.g. https://share.example.com):
- Build with the env vars above baked in.
- Upload
dist/to a private S3 bucket fronted by CloudFront (OAC), with a custom error response403/404 -> /index.html(SPA routing) and an ACM cert for the domain. - Point a Route53 alias at the distribution.
- Add that domain to the backend
allowed_originsand re-runmake applyso S3 CORS permits it.
The product name lives in web/src/brand.js (one constant).
- Mandatory MFA on the auth side; the bearer token is validated on every request via the auth service.
- Files are private; access is only ever granted through short-lived (1 h) presigned URLs.
per_userKMS gives per-user cryptographic isolation and crypto-shredding (deleting a user's key makes their files unrecoverable).
- Infrastructure as Code (Terraform) for the whole stack
- Per-user customer-managed KMS encryption
- Multipart uploads for very large files
- Share management (revoke links, see who has access)
MIT - see LICENSE.
Tarek CHEIKH - @TocConsulting Β· tocconsulting.fr
