A blazing-fast, self-hosted file CDN built on Cloudflare R2 + MongoDB + Express.
Upload images, videos, audio and documents through a friendly web UI or a clean public API β get a permanent CDN link back in milliseconds.
Live Demo β’ API Docs β’ Report Bug β’ Contact
- What is Gifted CDN?
- Features
- Tech Stack
- How It Works
- Demo
- Prerequisites
- Step-by-Step Setup (Total Beginner Friendly)
- Running the App
- Deploying to Production (PM2)
- API Reference
- Web Interface
- Project Structure
- Environment Variables Reference
- Rate Limits & Security
- Troubleshooting / FAQ
- Roadmap
- Contributing
- License
- Author & Contact
Gifted CDN is a lightweight, open-source file management service that lets you (or your users) upload files and instantly get back a permanent, publicly-accessible CDN URL.
Behind the scenes it stores files on Cloudflare R2 (S3-compatible object storage with zero egress fees) and tracks file metadata in MongoDB. It ships with:
- A polished web upload UI (drag-and-drop, progress bar, copy-link, animated)
- A clean JSON REST API for programmatic uploads / deletes
- A built-in contact form that delivers messages straight to your Telegram
- A full HTML API documentation page at
/docs
It's perfect for:
- π· Hosting media for your blog, app, or bot
- π¬ Sharing videos/audio without YouTube limits
- π¦ Acting as a dropbox-style file share for clients
- π€ Powering uploads from chatbots, scrapers, or scripts
| Category | Capabilities |
|---|---|
| Storage | Cloudflare R2 (S3 SDK), public-read URLs via custom domain, automatic folder routing (image/, video/, audio/, file/) |
| Uploads | Drag-and-drop UI, REST API, multer memory-buffer pipeline, 100 MB max per file |
| File Types | Images, videos, audio, documents β 100+ MIME types whitelisted out of the box |
| Security | Cloudflare Turnstile CAPTCHA on the UI, Helmet headers, CORS, rate-limiting, optional deleteKey for safe deletion |
| Database | MongoDB stores metadata, original name, R2 path, mimetype, size, timestamps |
| Logging | Winston JSON logs to logs/combined.log and logs/error.log |
| Deployment | First-class PM2 support (start, stop, reload, save, startup, monit) |
| Docs UI | Built-in /docs page with copy-paste examples in cURL, JavaScript and Python |
| Contact | Built-in /contact form delivers submissions (and attachments) to a Telegram chat |
| Mobile | Fully responsive Tailwind UI |
| Layer | Technology |
|---|---|
| Runtime | Node.js 18+ |
| Framework | Express.js |
| Storage | Cloudflare R2 (via @aws-sdk/client-s3) |
| Database | MongoDB (via mongoose) |
| Uploads | multer (memory storage) |
| CAPTCHA | Cloudflare Turnstile |
| Notifications | Telegram Bot API (for contact form) |
| Security | Helmet, CORS, express-rate-limit |
| Logging | Winston |
| Process Manager | PM2 |
| Frontend | Vanilla HTML + Tailwind CSS + particles.js |
βββββββββββββββββββββββββββββββ
β Browser / β
β External Client β
ββββββββββββββββ¬βββββββββββββββ
β HTTPS upload
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Express API (Node.js) β
β β’ Turnstile CAPTCHA check (UI only) β
β β’ MIME-type whitelist + 100 MB cap β
β β’ Generates short random prefix: e.g. "Xy3" β
β β’ Routes file into folder by type (image/videoβ¦) β
ββββββββββββββ¬ββββββββββββββββββββββββββββ¬βββββββββββββ
β β
writes file β β saves metadata
βΌ βΌ
ββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββ
β Cloudflare R2 Bucket β β MongoDB collection β
β (public-read object) β β name, path, url, β
β β β mimetype, deleteKeyβ¦ β
ββββββββββββββ¬ββββββββββββββ ββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β Custom domain CDN URL β
β https://files.your.com β
β /image/Xy3-photo.jpg β
ββββββββββββββββββββββββββββ
| Page | URL |
|---|---|
| Upload UI | https://cdn.giftedtech.co.ke |
| API Docs | https://cdn.giftedtech.co.ke/docs |
| Contact | https://cdn.giftedtech.co.ke/contact |
| Delete UI | https://cdn.giftedtech.co.ke/delete |
Before you start, make sure you have:
- β Node.js 18 or newer β download here
- β npm (comes with Node.js) or yarn
- β Git β download here
- β A free Cloudflare account β signup
- β A free MongoDB Atlas account (or a self-hosted MongoDB) β signup
- β A Telegram account (only needed for the contact form)
- β Basic familiarity with the terminal/command line
π‘ New to all this? Just follow each step in order. Don't skip β every variable matters.
Open your terminal and run:
git clone https://github.com/mauricegift/gifted-cdn.git
cd gifted-cdnnpm installThis installs every package listed in package.json (Express, MongoDB driver, AWS S3 SDK, etc.).
Cloudflare R2 is where your actual files will live. It's S3-compatible and free for up to 10 GB with no egress fees.
- Sign in to your Cloudflare Dashboard β in the left sidebar click R2 Object Storage.
- Click Create bucket.
- Bucket name: anything you like, e.g.
files - Location: Automatic (or pick the region closest to your users)
- Click Create bucket.
- Bucket name: anything you like, e.g.
- Open the bucket β go to the Settings tab.
- Scroll to Public access β click Connect Domain and choose either:
- A subdomain you own (e.g.
files.yourdomain.com) β recommended; OR - The free
r2.devURL Cloudflare gives you.
- A subdomain you own (e.g.
- Note the Public Access URL (we'll use this as
CF_BUCKET_DOMAIN). - Now go back to R2 Overview β click Manage R2 API Tokens (top-right) β Create API token.
- Permissions: Object Read & Write
- Specify bucket: select the bucket you just created
- Click Create API Token
- Copy and save:
- Access Key ID β
CF_ACCESS_KEY_ID - Secret Access Key β
CF_SECRET_ACCESS_KEY - Endpoint URL (looks like
https://<account-id>.r2.cloudflarestorage.com) βCF_BUCKET_API_ENDPOINT
- Access Key ID β
β οΈ The Secret Access Key is shown only once. Save it somewhere safe immediately.
MongoDB stores file metadata (name, R2 path, delete key, etc.).
- Sign in to MongoDB Atlas.
- Click Build a Database β choose the free M0 tier β pick a cloud provider/region close to you β Create.
- Create a database user: pick a username + a strong password. Save these.
- Network Access β Add IP Address β click Allow Access from Anywhere (
0.0.0.0/0) for development. - Go back to Database β click Connect on your cluster β choose Drivers β copy the connection string. It looks like:
mongodb+srv://<user>:<password>@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority - Replace
<user>and<password>with the credentials you just made. - Add a database name (e.g.
gifted_cdn) before the?:mongodb+srv://yourUser:yourPass@cluster0.xxxxx.mongodb.net/gifted_cdn?retryWrites=true&w=majority - This full string is your
MONGO_URI.
Turnstile protects the public upload page from bots. It's free.
- In the Cloudflare Dashboard β left sidebar β Turnstile β Add Site.
- Site name:
gifted-cdn - Domains: add your domain (e.g.
cdn.yourdomain.com) ANDlocalhostfor local testing. - Widget mode: Managed (recommended).
- Click Create.
- Copy:
- Site Key β paste it into
public/index.htmlwhere it saysdata-sitekey="..."(search forcf-turnstile). - Secret Key β save as
CF_TURNSTILE_SECRET_KEYin your.env.
- Site Key β paste it into
π‘ Only needed if you want contact-form submissions delivered to Telegram. Skip if you don't care.
- Open Telegram β search @BotFather β start a chat β send
/newbot. - Follow the prompts (name + username). BotFather replies with a token β save it as
BOT_TOKEN. - Get your Chat ID β the easy way:
- Open Telegram and search for @getmyid_bot (the Get My ID Bot).
- Hit Start β it instantly replies with your numeric Telegram user ID.
- Copy that number β it's your
CHAT_ID. - πΊ Full walkthrough (with screenshots): see mauricegift/telegram-bot β bot-token-and-uid.md.
- For a group chat, add your bot to the group, send
/start, then visithttps://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdatesβ the group ID will be a negative number (e.g.-100123456789).
In the project root create a file named .env and paste:
# βββ Server βββββββββββββββββββββββββββββββββββββββββββββββββ
PORT=5000
# βββ Cloudflare R2 (Storage) ββββββββββββββββββββββββββββββββ
CF_REGION=auto
CF_BUCKET_NAME=files
CF_BUCKET_DOMAIN=files.yourdomain.com
CF_BUCKET_API_ENDPOINT=https://<your-account-id>.r2.cloudflarestorage.com
CF_ACCESS_KEY_ID=your_r2_access_key_id
CF_SECRET_ACCESS_KEY=your_r2_secret_access_key
# βββ Cloudflare Turnstile (CAPTCHA) βββββββββββββββββββββββββ
CF_TURNSTILE_SECRET_KEY=your_turnstile_secret_key
CF_TURNSTILE_API_URL=https://challenges.cloudflare.com
# βββ MongoDB ββββββββββββββββββββββββββββββββββββββββββββββββ
MONGO_URI=mongodb+srv://user:pass@cluster0.xxxxx.mongodb.net/gifted_cdn?retryWrites=true&w=majority
# βββ Telegram Contact Form ββββββββββββββββββββββββββββββββββ
BOT_TOKEN=your_telegram_bot_token
CHAT_ID=your_telegram_chat_id
TELEGRAM_API_URL=https://api.telegram.orgπ NEVER commit
.envto git. Make sure it's listed in.gitignore.
npm run devnpm startVisit http://localhost:5000 in your browser. You should see the Gifted CDN upload page. π
PM2 keeps your app running 24/7 and restarts it automatically if it crashes.
npm run pm2:install
# or directly: npm install pm2 -g| Action | Command |
|---|---|
| Start in production mode | npm run pm2:start:prod |
| Start (default) | npm run pm2:start |
| Stop | npm run pm2:stop |
| Restart | npm run pm2:restart |
| Reload (zero-downtime) | npm run pm2:reload |
| Delete process | npm run pm2:delete |
| View live logs | npm run pm2:logs |
| Status dashboard | npm run pm2:status |
| Live monitor | npm run pm2:monit |
| Save current process list | npm run pm2:save |
| Auto-start on boot | npm run pm2:startup |
npm run pm2:start:prod
npm run pm2:save
npm run pm2:startup # follow the printed command to enable boot startupAdd a reverse proxy + HTTPS:
server {
listen 80;
server_name cdn.yourdomain.com;
client_max_body_size 110M; # match the 100 MB upload cap
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Then issue a free SSL cert with Certbot:
sudo certbot --nginx -d cdn.yourdomain.comBase URL (your deployment):
https://cdn.yourdomain.comLive demo base URL:https://cdn.giftedtech.co.ke
Public endpoint β no CAPTCHA. Use it from scripts, bots, mobile apps.
| Field | Type | Required | Description |
|---|---|---|---|
file |
multipart/form-data | β | The file to upload (max 100 MB) |
deleteKey |
string | β | Optional. If set, you'll later need this same key to delete the file |
Rate limit: 10 uploads per IP per minute.
curl -X POST https://cdn.giftedtech.co.ke/api/upload.php \
-F "file=@/path/to/photo.jpg" \
-F "deleteKey=my-secret-key-123"const form = new FormData();
form.append('file', fileInput.files[0]);
form.append('deleteKey', 'my-secret-key-123'); // optional
const res = await fetch('https://cdn.giftedtech.co.ke/api/upload.php', {
method: 'POST',
body: form
});
const data = await res.json();
console.log(data.url); // β your CDN linkimport requests
with open('photo.jpg', 'rb') as f:
r = requests.post(
'https://cdn.giftedtech.co.ke/api/upload.php',
files={'file': f},
data={'deleteKey': 'my-secret-key-123'}, # optional
)
print(r.json()['url']){
"size": "546.03 kB",
"mimetype": "image/jpeg",
"storageClass": "Standard",
"expiry": "No Expiry Unless Deleted",
"name": "Xy3-photo.jpg",
"path": "image/Xy3-photo.jpg",
"modified": "May 17, 2025 1:49 PM",
"url": "https://files.giftedtech.co.ke/image/Xy3-photo.jpg",
"_id": "664a0b2f9c6e4e4f0c1a2b3c",
"createdAt": "2025-05-17T10:49:00.000Z",
"deleteKey": "my-secret-key-123"
}Removes the file from R2 and the database. Requires the same deleteKey used at upload.
| Field | Type | Required | Description |
|---|---|---|---|
fileName |
string | β | Exact name returned at upload (e.g. Xy3-photo.jpg) |
deleteKey |
string | β | The delete key originally used |
curl -X DELETE https://cdn.giftedtech.co.ke/api/delete.php \
-H "Content-Type: application/json" \
-d '{"fileName":"Xy3-photo.jpg","deleteKey":"my-secret-key-123"}'const res = await fetch('https://cdn.giftedtech.co.ke/api/delete.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileName: 'Xy3-photo.jpg',
deleteKey: 'my-secret-key-123'
})
});
console.log(await res.json());{
"name": "Xy3-photo.jpg",
"path": "image/Xy3-photo.jpg",
"deleted": true,
"deletedAt": "May 17, 2025 2:10 PM",
"_id": "664a0b2f9c6e4e4f0c1a2b3c",
"deletedFromDb": true,
"deletedFromServer": true
}| Status | Reason |
|---|---|
400 |
fileName missing |
403 |
No delete key was set on upload, or key doesn't match |
404 |
File not found in DB |
Returns the metadata for an existing file (path = image/Xy3-photo.jpg, etc.).
curl https://cdn.giftedtech.co.ke/file/image%2FXy3-photo.jpgUsed internally by the /contact page to forward messages (and optional attachments) to your Telegram chat.
| Field | Required | Description |
|---|---|---|
name |
β | Sender's name |
email |
β | Sender's email |
phone |
β | Phone |
message |
β | Message body |
file |
β | Optional file attachment |
Same as /api/upload.php but requires a valid Cloudflare Turnstile token in the Turnstile-Token header. Used by the built-in upload UI.
| Route | What it does |
|---|---|
/ |
Drag-and-drop upload UI (CAPTCHA-protected) |
/docs |
Pretty HTML API documentation |
/delete |
Self-service file deletion via delete key |
/contact |
Contact form (forwards to Telegram) |
All pages are responsive (mobile + desktop) and styled with Tailwind.
gifted-cdn/
βββ api/
β βββ client/
β β βββ index.js # Cloudflare R2 (S3 SDK) wrapper β upload/get/delete
β βββ db/
β β βββ index.js # MongoDB connection (mongoose)
β βββ models/
β β βββ index.js # File schema (name, path, url, mimetype, deleteKeyβ¦)
β βββ index.js # Express app β routes, multer, rate-limit, Turnstile
β
βββ public/
β βββ index.html # Upload UI (home page)
β βββ docs/index.html # API documentation page
β βββ delete/index.html # File deletion UI
β βββ contact/index.html # Contact form
β
βββ logs/ # Winston logs (auto-created)
β βββ combined.log
β βββ error.log
β
βββ config.js # Reads env vars + sane defaults (incl. MIME whitelist)
βββ package.json # Dependencies + npm scripts (incl. PM2)
βββ .env # Secrets (you create this β never commit)
βββ README.md # β this file
| Variable | Required | Default | Description |
|---|---|---|---|
PORT |
β | 5000 |
Port the Express server listens on |
CF_REGION |
β | auto |
R2 region β leave as auto |
CF_BUCKET_NAME |
β | files |
Your Cloudflare R2 bucket name |
CF_BUCKET_DOMAIN |
β | files.giftedtech.co.ke |
Public domain bound to your R2 bucket (no https://) |
CF_BUCKET_API_ENDPOINT |
β | β | R2 S3 endpoint (https://<acct>.r2.cloudflarestorage.com) |
CF_ACCESS_KEY_ID |
β | β | R2 API access key id |
CF_SECRET_ACCESS_KEY |
β | β | R2 API secret access key |
CF_TURNSTILE_SECRET_KEY |
β * | β | Turnstile secret (*needed only by /giftedUpload.php) |
CF_TURNSTILE_API_URL |
β | https://challenges.cloudflare.com |
Turnstile verification endpoint |
MONGO_URI |
β | β | Full MongoDB connection string |
BOT_TOKEN |
β | β | Telegram bot token (contact form) |
CHAT_ID |
β | β | Telegram chat id (contact form recipient) |
TELEGRAM_API_URL |
β | https://api.telegram.org |
Telegram API base URL |
IMAGE_MIMETYPES |
β | built-in list | Override the allowed image MIME types |
VIDEO_MIMETYPES |
β | built-in list | Override the allowed video MIME types |
AUDIO_MIMETYPES |
β | built-in list | Override the allowed audio MIME types |
DOC_MIMETYPES |
β | built-in list | Override the allowed document MIME types |
- Upload rate limit: 10 uploads / minute / IP (configurable in
api/index.js). - File size cap: 100 MB (set in
multerlimits). - MIME whitelist: uploads outside the configured image/video/audio/doc lists are rejected with
400. - CAPTCHA: the public web upload uses Cloudflare Turnstile so bots can't spam your bucket.
- Delete protection: files saved without a
deleteKeycannot be deleted via the public API β you must delete them directly from your R2 dashboard. - CORS: open by default (
*). Lock this down inapi/index.jsif you only want certain origins to call your API. - Helmet & secure headers: ready to enable β call
app.use(helmet())inapi/index.js. - Logs: all errors and uploads logged to
logs/via Winston.
Database Connected β / "DB connection error"
- Double-check
MONGO_URIβ it must include the username, password (URL-encoded if it contains@,:,/), and a database name. - In MongoDB Atlas β Network Access, make sure your server's IP (or
0.0.0.0/0for testing) is whitelisted.
Files upload but the URL returns 403 / Access Denied
- In R2 β bucket β Settings β Public access, confirm a public domain is connected.
- The code uploads with
ACL: 'public-read'β that ACL is honoured only when the bucket has public access enabled. - If you're using the
r2.devURL, it must be enabled under Public Access.
"CAPTCHA Response is Required"
- The web UI must include a valid Turnstile site key in
public/index.html(data-sitekey). - If you only use the API (
/api/upload.php), you do not need Turnstile β that endpoint skips it.
Telegram contact form does nothing
- Verify the bot token by visiting
https://api.telegram.org/bot<TOKEN>/getMeβ it should return your bot info. - Verify the chat ID by sending the bot a message first, then calling
/getUpdates. - For groups, the chat ID is negative (e.g.
-100123β¦).
"File type not allowed"
- The MIME type isn't in any of the four whitelists. Either add it to the relevant
*_MIMETYPESenv variable in.env, or convert the file to a supported format.
I lost my deleteKey β how do I delete a file?
- You can't delete it via the API. Go to your R2 dashboard β bucket β find the file β delete manually. Then connect to MongoDB and delete the matching document from the
filescollection.
- User authentication (per-user file dashboards)
- Image transformations (resize, format convert) on the fly
- Configurable per-user storage quotas
- WebSocket upload progress for huge files
- Docker / Docker Compose support
- Optional virus scan via ClamAV before upload commits
Contributions are welcome and appreciated! π
- Fork the repo
- Create your feature branch:
git checkout -b feature/awesome-thing - Commit your changes:
git commit -m 'Add awesome thing' - Push to the branch:
git push origin feature/awesome-thing - Open a Pull Request
Please run npm run lint and npm run format before submitting.
This project is licensed under the MIT License β see the LICENSE file for details.
MIT Β© 2024 - Present Maurice Gift / GiftedTech
Maurice Gift β Founder, GiftedTech
- π Website: https://me.giftedtech.co.ke
- π Live CDN: https://cdn.giftedtech.co.ke
- πΌ GitHub: @mauricegift
- π§ Email: founder@giftedtech.co.ke
- π¬ Need help? Use the contact form
β If this project helped you, please give it a star! β
Made with β€οΈ in Kenya π°πͺ