Production-ready file sharing platform (fluxsolutions) for VPS deployment with Docker Compose v2.
- Email/password auth with
argon2. - JWT access + refresh tokens in
httpOnlycookies. - CSRF protection via double-submit token.
- Refresh token rotation with revoke/replacement tracking.
- Upload up to
1GBper file by default (MAX_FILE_SIZE_BYTESconfigurable). - Resumable upload via S3 multipart to MinIO (direct browser -> object storage).
- File/folder management, search by filename, rename/move/delete.
- Share links: public / password-protected / TTL / one-time / max-downloads.
- RBAC (
USER/ADMIN), ban/unban, admin stats + audit log. - Structured logging, request-id,
/healthz,/readyz,/metrics. - Optional ClamAV scanning (
ENABLE_CLAMAV=true) withPENDING->CLEAN/INFECTEDstates. - Production infra:
web,api,postgres,minio,nginx, optionalclamav.
fluxsolutions/
apps/
api/ # Fastify + Prisma + S3 multipart + auth/security
web/ # Next.js App Router + Tailwind + SWR
packages/
shared/ # Shared constants/schemas/types
deploy/
nginx/ # Reverse proxy and files vhost config
scripts/
backup_postgres.sh
restore_postgres.sh
rotate_secrets.md
docker-compose.yml
.env.example
- Frontend: Next.js App Router (
apps/web) - API: Fastify + TypeScript (
apps/api) - DB: PostgreSQL + Prisma migrations
- Object storage: MinIO (S3-compatible)
- Reverse proxy: Nginx
- Observability: Pino logs + Prometheus metrics endpoint
- Browser asks API
/files/initiate-upload. - API creates multipart upload in MinIO, returns presigned part URLs on
files.fluxsolutions.tld. - Browser uploads parts directly to MinIO (API does not proxy large files).
- Browser calls
/files/complete-upload. - API finalizes multipart upload and stores metadata in PostgreSQL.
Recommended domain scheme:
app.fluxsolutions.tld-> CDN/Proxy -> Nginx -> Webapi.fluxsolutions.tld-> CDN/Proxy -> Nginx -> APIfiles.fluxsolutions.tld-> CDN/Proxy -> Nginx -> MinIO
- API signs S3 URLs using
S3_PUBLIC_ENDPOINT=https://files.fluxsolutions.tld. - Browser downloads/uploads directly through CDN/files domain.
- Works well with edge routing and direct object path.
- Set
ENABLE_CDN=false. - Set
S3_ORIGIN_PUBLIC_ENDPOINTto direct file origin (for examplehttps://files.fluxsolutions.ru). - Keep
S3_PUBLIC_ENDPOINTfor CDN mode only (can stay configured, but will be ignored when CDN is disabled). - In production,
S3_ORIGIN_PUBLIC_ENDPOINTis required ifENABLE_CDN=false.
In API /s/:token/download:
- Restricted share (password/TTL/one-time/max-downloads):
Cache-Control: private, no-store - Open share (no password, no limits, no TTL):
Cache-Control: public, max-age=31536000, immutable
When to disable cache:
- Any private or revocable access mode.
- When legal/privacy policy requires immediate revoke behavior.
Deletion/invalidation:
- Object keys are unique per upload; signed URLs are short-lived.
- For restricted links, cache is disabled, so explicit CDN purge is typically not required.
- For public immutable links, purge if you expose long-lived cached URL and need immediate deletion.
If query-signed URLs are not cache-efficient for your CDN policy, use:
- API download redirect endpoint with short-lived token,
- Edge rule to validate token/signature,
- Internal proxy (
X-Accel-Redirect-style pattern) or signed headers/cookies at edge.
This repo already exposes redirect entry point (GET /s/:token/download) that can be adapted for that flow.
Implemented:
argon2idpassword hashing.- JWT access + refresh cookies (
httpOnly, secure flags). - Refresh token rotation with DB persistence (
refresh_tokens). - CSRF token validation (
x-csrf-token+ cookie). - Input validation (
zod). - MIME allowlist policy + content sniff check after upload completion.
- File size limit (
MAX_FILE_SIZE_BYTES, default 1GB). - Rate limiting (
@fastify/rate-limit). - Helmet security headers.
- CORS allowlist.
- Secrets only from environment.
Threat model (short):
- Account takeover attempts -> strong hashing + token rotation + ban controls.
- CSRF on cookie auth -> enforced CSRF token for state-changing endpoints.
- Upload abuse / DoS -> request limits, file-size cap, direct object upload path.
- Malicious file delivery -> MIME policy, optional ClamAV scanning, scan-status gating.
- Leaked links -> optional password/TTL/one-time/max-downloads.
Auth:
POST /auth/registerPOST /auth/loginPOST /auth/refreshPOST /auth/logoutPOST /auth/forgot-passwordPOST /auth/reset-passwordGET /auth/csrf
User:
GET /mePOST /me/change-password
Files/Folders:
POST /files/initiate-uploadPOST /files/resume-uploadPOST /files/complete-uploadGET /filesPATCH /files/:idDELETE /files/:idGET /files/:id/downloadGET /foldersPOST /foldersDELETE /folders/:id
Shares:
POST /sharesGET /s/:tokenPOST /s/:token/verifyGET /s/:token/download
Admin:
GET /admin/usersPOST /admin/users/:id/banGET /admin/stats
Observability:
GET /healthzGET /readyzGET /metrics
Copy .env.example -> .env and set strong secrets.
Generate random secrets:
openssl rand -hex 32
openssl rand -base64 32 | tr -d '\n'Critical vars:
JWT_ACCESS_SECRET,JWT_REFRESH_SECRET,JWT_SHARE_SECRETPOSTGRES_PASSWORDMINIO_ROOT_PASSWORDMAX_FILE_SIZE_BYTES(default1073741824)ALLOWED_MIME_TYPESENABLE_CDN(true/false)NEXT_PUBLIC_API_BASE_URL(recommended:/api)CORS_ORIGIN(must include your web origin)COOKIE_DOMAIN(example:.fluxsolutions.ru)S3_PUBLIC_ENDPOINT(example:https://cdn.fluxsolutions.ru, used whenENABLE_CDN=true)S3_ORIGIN_PUBLIC_ENDPOINT(required in production whenENABLE_CDN=false)
- Install dependencies:
nvm use 22 || nvm install 22
npm install- Copy env file:
cp .env.example .env- Start infra only:
docker compose up -d fluxsolutions-postgres fluxsolutions-minio fluxsolutions-minio-init- Run migrations:
npm run prisma:migrate:deploy --workspace @fluxsolutions/api- Start apps:
npm run dev- Open:
- Web:
http://localhost:3000 - API:
http://localhost:4000
- Prepare server:
sudo apt update
sudo apt install -y docker.io docker-compose-plugin
sudo usermod -aG docker $USER- Clone and configure:
git clone <your-repo-url> fluxsolutions
cd fluxsolutions
cp .env.example .env
# edit .env- Build and start:
docker compose build
docker compose up -d- Verify:
docker compose ps
curl -fsS http://127.0.0.1:4000/healthz
curl -fsS http://127.0.0.1:4000/readyz- Enable ClamAV profile (optional):
docker compose --profile security up -d fluxsolutions-clamavCreate records:
fluxsolutions.ru->Ato VPS IPv4www.fluxsolutions.ru->CNAMEtofluxsolutions.ruapi.fluxsolutions.ru->Ato VPS IPv4cdn.fluxsolutions.ru-> CDN provider hostname (for example*.trbcdn.net)
CDN disabled mode:
- Point
files.fluxsolutions.rudirectly to VPS (Arecord). - Set
ENABLE_CDN=falseandS3_ORIGIN_PUBLIC_ENDPOINT=https://files.fluxsolutions.ru.
Option 1 (CDN edge TLS, origin HTTP):
- CDN terminates TLS at edge and forwards to origin over
HTTP:80. - Useful when
cdn.fluxsolutions.rupoints to CDN network, not directly to VPS.
Option 2 (Let's Encrypt via certbot):
- Use mounted paths
deploy/certbot/wwwanddeploy/certbot/conf. - Issue certs for app + api domains and enable HTTPS vhosts in Nginx.
- TLS vhost template:
deploy/nginx/examples/fluxsolutions-ssl.conf. - Copy to
deploy/nginx/conf.d/after certs are issued, then reload nginx.
Bootstrap certs example:
docker compose run --rm fluxsolutions-certbot certonly \
--webroot -w /var/www/certbot \
-d fluxsolutions.ru -d www.fluxsolutions.ru -d api.fluxsolutions.ru \
--email you@example.com --agree-tos --no-eff-email
cp deploy/nginx/examples/fluxsolutions-ssl.conf deploy/nginx/conf.d/fluxsolutions-ssl.conf
docker compose restart fluxsolutions-nginx
docker compose --profile tls up -d fluxsolutions-certbot- Proxy enabled for app + api domains.
- SSL/TLS:
Full (strict). - Cache rules:
app/api: bypass dynamic routes.files/cdn: allow cache only for responses withpublic, max-age=....
- Keep query string for presigned URLs (required).
- Enable range requests and large file support.
Create backup:
./scripts/backup_postgres.shRestore backup:
./scripts/restore_postgres.sh backups/fluxsolutions_postgres_YYYYMMDD_HHMMSS.sql.gzSecret rotation runbook: scripts/rotate_secrets.md
Rolling update (single VPS best effort):
git pull
docker compose build fluxsolutions-api fluxsolutions-web
docker compose up -d fluxsolutions-api fluxsolutions-web fluxsolutions-nginxNotes:
- Nginx keeps serving while web/api containers restart.
- Active uploads may need retry/resume (multipart resumable flow handles this).
Rollback:
git checkout <previous-stable-tag-or-commit>
docker compose build fluxsolutions-api fluxsolutions-web
docker compose up -d fluxsolutions-api fluxsolutions-web fluxsolutions-nginxIf schema changed, rollback requires DB migration plan and/or restore from backup.
- Strict TypeScript, ESLint, Prettier.
- Backend unit + e2e tests (
vitest,supertest).
Run checks:
npm run lint
npm run test
npm run buildcontainer fluxsolutions-minio is unhealthy:- Check logs:
docker compose logs fluxsolutions-minio. - Ensure
.envexists and hasMINIO_ROOT_USERandMINIO_ROOT_PASSWORD. - Recreate MinIO stack:
docker compose rm -sf fluxsolutions-minio fluxsolutions-minio-initdocker compose up -d fluxsolutions-minio fluxsolutions-minio-init
- Check logs:
Prisma ... Environment variable not found: DATABASE_URL:- Ensure
.envexists in repo root (cp .env.example .env). - Verify value:
grep '^DATABASE_URL=' .env. - Re-run:
npm run prisma:migrate:deploy --workspace @fluxsolutions/api.
- Ensure
- API dev crash on Node 18:
- Use Node 22 (
nvm use 22) because project targets Node 22 (see.nvmrc).
- Use Node 22 (
- Next.js
module is not definedfrom PostCSS:- Fixed by ESM postcss config; if cached old state, restart dev server.
- API app:
apps/api/src/app.ts - API routes:
apps/api/src/routes - Prisma schema:
apps/api/prisma/schema.prisma - Web app router:
apps/web/app - Compose:
docker-compose.yml - Nginx:
deploy/nginx/conf.d/fluxsolutions-app.conf,deploy/nginx/conf.d/fluxsolutions-files.conf - Backup scripts:
scripts/backup_postgres.sh,scripts/restore_postgres.sh