Send big files and secrets from your own S3 bucket. No cloud vendor, no monthly bill, no upload limit but the one you set.
CrabS3 is a self-hosted transfer platform for any S3-compatible storage. Drop files in the browser or push them through the API, get a shareable link back, and let the file delete itself after a deadline or a download count you choose.
It runs on RustFS by default, but any S3-compatible backend works — AWS S3, OVH Object Storage, MinIO, Ceph, etc. CrabS3 never stores your data itself; it only keeps metadata in Postgres.
Upload → hot bucket → share link → N downloads or T days → gone
↓
cold bucket (replicated, optional)| 📦 Your storage | Any S3-compatible backend. Your keys, your bucket, your retention rules. |
| 🚀 Big files | Resumable multipart uploads with live progress — a dropped connection does not restart the transfer. |
| 🔥 Hot & cold | Serve from fast storage, archive to cheap storage. Replication is handled by the S3 Provider. |
| 🗝️ Secrets | Share a password, a token, a note. Password-protected, time-limited, gone after reading. |
| 🗑️ Self-destruct | Set a max download count; the file is deleted from storage automatically when it is reached. |
| 🛡️ Malware scan | Every upload goes through ClamAV; infected files are flagged and blocked from download. |
| 🔒 Real accounts | Invite-only signup, sessions, 2FA (TOTP), per-user storage quotas. |
| 📧 Notifications | Email on upload, download and share; webhooks for your own integrations. |
| 📊 Dashboard | Per-user file list and download stats; admin view for storage, users and audit logs. |
| 🔌 Services | Scoped API keys for other apps and scripts — their own folder, quota and status, issued directly or self-served via an invite code. No user account needed. |
Requires Docker and Docker Compose.
git clone https://github.com/DoctorPok42/CrabS3.git
cd CrabS3
cp .env.example .env # then edit it — see Configuration
docker compose up -dThe interface is on http://localhost:3000. Health check: GET /api/health.
Prefer the published image? docker pull doctorpok/crabs3:latest, then point compose.yml at it instead of build: ..
Running from source (development)
npm install
npx prisma migrate deploy
npm run dev # http://localhost:3000You still need a reachable Postgres instance and an S3 endpoint.
Everything is environment variables — put them in .env, or manage them in Doppler (a doppler.yaml is included and picked up automatically).
Storage — One hot bucket is required; cold bucket is just a class of storage, and can be the same bucket if you want. CrabS3 does not copy bytes between buckets; it relies on your S3 provider to replicate them.
| Variable | Example |
|---|---|
S3_ENDPOINT |
http://192.168.1.100:9000 |
S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY |
your keys |
S3_BUCKET_NAME |
crabs3 |
S3_REGION |
us-east-1 |
EXPIRED_FILE_POLICY |
cold (move) or delete |
App
| Variable | Example |
|---|---|
DATABASE_URL |
postgresql://user:password@db:5432/crabs3 |
NEXT_PUBLIC_BASE_URL |
https://files.example.com — used in share links and emails |
JWT_SECRET |
a long random string |
LOG_MIN_LEVEL |
DEBUG · INFO · WARN · ERROR |
Email & scanning
| Variable | Example |
|---|---|
SMTP_HOST / SMTP_USER / SMTP_PASS |
your mail relay |
SMTP_FROM |
CrabS3 Notifications <bot@example.com> |
CLAMAV_HOST / CLAMAV_PORT |
clamav / 3310 |
CRON_SECRET |
shared secret for the expiry job container |
First user: signup is invite-only. Create the first account from the invite link generated by the admin panel, or seed one directly in the database.
- The browser hashes the file and asks the server if it already has this content; if so, a new share is created without sending a single byte. Otherwise it opens a multipart session and uploads parts in parallel — the server relays each part straight through to your bucket. A dropped connection reattaches to the same session and only sends the parts that didn't land yet.
- Metadata (owner, size, hash, expiry, download quota, password hash) lives in Postgres; file bytes only ever live in your buckets.
- ClamAV scans the object; a hit marks the file infected and download is refused for infected files.
- The cron container calls the expiry endpoint on a schedule. Files past their deadline or download quota are moved to cold storage or deleted, per
EXPIRED_FILE_POLICY. - Duplicate uploads are detected by hash, so the same file is not stored twice.
Files are replicated from hot to cold by the storage layer itself — see RustFS replication. The app never copies bytes between buckets.
Everything the UI does is available over HTTP. Public endpoints need no session; the rest use the session cookie, and admin endpoints additionally require an admin account.
Health · GET /api/health
Upload
POST /api/upload/dedupe-check already have this content? link instead of upload
POST /api/upload/multipart/start open a session
POST /api/upload/multipart/part upload one part
POST /api/upload/multipart/resume reattach after a dropped connection
POST /api/upload/multipart/set-hash attach a content hash to a session
POST /api/upload/multipart/complete finish + attach metadata
POST /api/upload/multipart/abort cancelFiles
GET /api/checkfile is this share link still valid?
POST /api/download/:id metadata for a share link
GET /api/download/:id/stream stream the bytes
DELETE /api/delete remove a file (409 + mode if it shares content with others)Secrets
POST /api/secret/upload store a secret, get a link
POST /api/secret/check exists? password required?
POST /api/secret/get read itServices — scoped API keys for third-party apps and scripts. Two ways to get a token: create a service directly and get its token back immediately, or issue an invite code that a third party redeems for their own token via join — no account needed either way. create/create/invite/update/delete/list need an admin session; upload/download use the service's own bearer token (Authorization: Bearer <token>), not the session cookie.
POST /api/services/create path 1 — create a service + folder, get its token back (admin)
POST /api/services/create/invite path 2 — issue a redeemable invite code (admin)
POST /api/services/join path 2 — redeem an invite code for a token (public)
GET /api/services/:uuid public info about a service
GET /api/services/list list every service (admin)
PUT /api/services/update change status or image (admin)
DELETE /api/services/delete/:id delete a service and its folder (admin)
POST /api/services/upload single presigned PUT — not the multipart flow
GET /api/services/download a share link, or a presigned URL per fileAuth
POST /api/auth/login · logout · signup
GET /api/auth/me · check-invite
POST /api/auth/invite (admin)Dashboard & communication
GET /api/dashboard/files
PATCH /api/dashboard/folders/:id rename
DELETE /api/dashboard/folders/:id delete the folder and every file in it
POST /api/dashboard/coldtohot restore a folder from cold to hot storage
PATCH /api/dashboard/me
GET /api/communication webhook settings
POST /api/communicationAdmin
GET /api/admin/stats storage, files, users
GET /api/admin/users
GET /api/admin/users/:id
DELETE /api/admin/users/:id
PUT /api/admin/users/:id/edit-quota
POST /api/admin/users/:id/reset-password
GET /api/admin/logs filter by level, action, date
PATCH /api/admin/logs set minimum log levelOpenAPI-ish request collections live in doc/api.
Next.js (App Router) · React · Tailwind CSS · Prisma + PostgreSQL · AWS SDK for S3 · ClamAV · Nodemailer · Docker
Issues and pull requests are welcome. Keep changes focused, run npm run lint before opening a PR, and describe what you tested.
Apache 2.0 — see LICENSE and NOTICE.