Send big files and secrets from your own S3 bucket. No cloud vendor, no monthly bill, no upload limit but the one you set.
Quick start Β· Configuration Β· How it works Β· API
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 β bucket (hot) β share link β N downloads or T days β gone
β
marked cold in DB, per your bucket's own lifecycle rule (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 | One bucket. Cold is a class your provider's lifecycle rule assigns; CrabS3 just tracks it β no copying, no second bucket. |
| ποΈ 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: ..
See the full doc by DeepWiki: https://deepwiki.com/DoctorPok42/CrabS3
A community-scripts LXC install is also available β bare-metal (no Docker inside the container), Node.js/PostgreSQL/ClamAV set up automatically:
COMMUNITY_SCRIPTS_URL="https://raw.githubusercontent.com/DoctorPok42/CrabS3/main" \
bash -c "$(curl -fsSL https://raw.githubusercontent.com/DoctorPok42/CrabS3/main/ct/crabs3.sh)"You'll be prompted for your S3 endpoint and keys during install (or export var_s3_endpoint, var_s3_access_key, var_s3_secret_key, var_s3_bucket, var_admin_email beforehand for an unattended run). Everything else lives in /opt/crabs3/.env β edit and systemctl restart crabs3 to apply. There's no TLS in front by default; see COOKIE_SECURE below before exposing it past your LAN.
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 bucket. "Cold" is a storage class your provider's lifecycle rule assigns to a file, not a separate bucket β CrabS3 only records which class a file is in.
| 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 |
COOKIE_SECURE |
true/false β defaults to NODE_ENV === "production" if unset. Set false when serving plain HTTP with no reverse proxy in front, or browsers silently drop the session cookie after login |
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, so nothing lets you in until one admin exists. Seed it once β safe to re-run, it does nothing if an admin is already there:
docker compose exec -T web npx tsx install/seed-admin.mjsPrints the email and generated password once; they are not stored anywhere else. Issue further invites from the admin panel.
- 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 bucket.
- 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 marked cold or deleted, per
EXPIRED_FILE_POLICY. - Duplicate uploads are detected by hash, so the same file is not stored twice.
The hot β cold transition is a storage-class change your bucket's own lifecycle rule performs β see RustFS lifecycle rules for an example. CrabS3 never copies bytes anywhere; it only records which class a file ended up in.
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/finish once per batch, after every file is complete β sends notifications, fires webhooks
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 fileAccess Tokens β personal bearer tokens that act as you, without the session cookie (scripts, cron, CI). Each carries one or more scopes (READ Β· WRITE Β· DELETE Β· ADMIN) and an expiry of 7/30/90/180/365 days, and works as Authorization: Bearer <token> on any endpoint on this page. READ allows GET only, WRITE allows anything but DELETE, DELETE allows DELETE only, ADMIN allows everything. Managed from /me; the token value is shown once, at creation.
GET /api/accessToken list your tokens β name, scopes, expiry, never the value again
POST /api/accessToken create one: name, scopes[], expires_at (days)
DELETE /api/accessToken?id=:id revoke a tokenAuth
POST /api/auth/login Β· logout Β· signup
GET /api/auth/me Β· check-invite
DELETE /api/auth/me delete your own account and everything it owns
POST /api/auth/invite (admin)
POST /api/2fa/create enable TOTP, returns secret + QR uri
GET /api/2fa/disable disable TOTP for the current accountDashboard & communication
GET /api/dashboard/files
GET /api/dashboard/folders folders you own or have files in, with a file count each
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/communication
GET /api/fingerprint/:id download history for a file or folder (?type=file|folder)
GET /api/settings read specific instance settings (?keys=a,b)Admin
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 level
GET /api/admin/settings every instance setting and its current value
PATCH /api/admin/settings update one: { key, value }
DELETE /api/admin/settings reset one to default (?key=:key)
POST /api/admin/settings sync the settings catalog β creates missing rows with defaultsOpenAPI-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.