Zero-config, project-scoped cloud asset storage β a secure gateway on top of Cloudinary.
Part of the TheDreamBitLabs open-source ecosystem Β· Built by Dhairya Darji
Why D2 Storage? Β· Quick Start Β· Cloudinary Setup Β· API Reference Β· AI Integration Β· Deployment
D2 Storage is a self-hosted storage microservice that sits in front of Cloudinary. You deploy it once on your own server, create isolated project workspaces with their own API keys and CORS rules, and upload files with a single fetch() call β from any app, any framework, any language.
No SDK needed. No Cloudinary account exposed to the client. Just a clean, secure REST API.
"I already have Cloudinary β why would I use this?"
Great question. Here's what D2 Storage adds that raw Cloudinary cannot:
| Problem with raw Cloudinary | What D2 Storage solves |
|---|---|
| π΄ API Key/Secret must stay server-side β you can't use it from a browser safely | β D2 issues per-project bearer tokens β safe to embed in any client app |
| π΄ One global bucket β all projects share the same namespace | β
Every project gets its own isolated root folder under dreambit/<your-root>/ |
| π΄ No origin restriction β any server can upload to your account if they get the key | β Project-level CORS whitelist β only approved origins can upload |
| π΄ Integrating Cloudinary in every new project means re-reading docs every time | β
Once deployed, every new project is just POST /api/upload with a bearer token β done in 2 minutes |
| π΄ Explaining Cloudinary SDK setup to an AI assistant is slow and error-prone | β Built-in AI Integration Copilot β copy a pre-built system prompt and get working SDK code instantly |
| π΄ File naming conflicts in shared buckets | β Timestamp-appended filenames prevent collisions automatically |
π΄ .zip, .pdf, .csv are uploaded as the wrong resource type |
β
Automatic raw type routing so downloads work correctly in-browser |
The philosophy: "A 500-line storage service that removes 30 minutes of setup from every future project."
- ποΈ Project Workspaces β spin up isolated storage containers with unique API keys
- π Hashed API Keys β cryptographically hashed bearer tokens, never stored in plaintext
- π Per-Project CORS β restrict uploads to your approved origins only
- π Auto Folders β upload to a folder that doesn't exist yet and it's provisioned automatically
- π€ AI-Ready Copilot β generate a master integration prompt per project, paste into any LLM
- π
Admin Dashboard β beautiful UI at
/dashboardwith 24-hour session management - π In-App API Docs β full interactive reference at
/docs, no external site needed - π Format Integrity β raw/video/image types routed correctly for proper download behavior
- Node.js β₯ 18
- A free Cloudinary account (10 GB free)
- A server with a persistent filesystem β Railway, Render, VPS, or Docker (see Deployment)
git clone https://github.com/TheDreamBitLabs/d2-storage.git
cd d2-storagenpm installcp .env.example .env.localOpen .env.local and fill in your values:
# Cloudinary (required) β from cloudinary.com β Settings β API Keys
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
# Master admin password (required) β protects the /dashboard panel
# This is NOT a Cloudinary key β pick any strong passphrase you want
API_KEYS=your_master_password
# Your deployment URL (recommended)
# The AI Integration Prompt and /docs page use this as the base URL.
# Change this when you go to production!
NEXT_PUBLIC_APP_URL=http://localhost:3001npm run devApp starts on http://localhost:3001 Β· Dashboard at /dashboard Β· Docs at /docs
Cloudinary's free tier gives you 10 GB storage + 25 GB bandwidth/month β plenty for hackathons and indie projects.
Go to cloudinary.com/users/register/free β no credit card required.
- Log in β Settings β API Keys
- Copy three values into your
.env.local:
| Env Variable | Where to find it |
|---|---|
CLOUDINARY_CLOUD_NAME |
Settings β Account β Cloud name |
CLOUDINARY_API_KEY |
Settings β API Keys β API Key |
CLOUDINARY_API_SECRET |
Settings β API Keys β API Secret (click reveal) |
When Cloudinary asks you to select a role for a new API key, choose Master or Full Access. D2 Storage needs permission to upload, delete, and list assets across folders.
D2 Storage manages Cloudinary folders automatically. When you create a project workspace, folders are provisioned on first upload:
cloudinary-bucket/
dreambit/
your-project-root/
avatars/ β auto-created on first upload
docs/
uploads/
...
You never need to touch the Cloudinary dashboard for folder management.
| Resource | Free tier |
|---|---|
| Storage | 10 GB |
| Bandwidth / month | 25 GB |
| Transformations / month | 25,000 credits |
| Variable | Required | Description |
|---|---|---|
CLOUDINARY_CLOUD_NAME |
β | Your Cloudinary cloud name |
CLOUDINARY_API_KEY |
β | Your Cloudinary API key |
CLOUDINARY_API_SECRET |
β | Your Cloudinary API secret |
API_KEYS |
β | Master admin password(s). Comma-separate for multiple: pass1,pass2 |
NEXT_PUBLIC_APP_URL |
Recommended | Your public deployment URL β used in AI prompt + docs. Defaults to window.location.origin on client. Update this when you host on a custom domain. |
CONVEX_DEPLOYMENT |
β Optional | Convex deployment name (e.g. dev:fancy-llama-123). Only needed if you run or build with Convex backend (reserved for future multi-tenant hosted version). Not needed for basic self-hosting. |
NEXT_PUBLIC_CONVEX_URL |
β Optional | Convex deployment HTTP URL. Only needed if Convex backend is active. |
All requests need a project-scoped bearer token in the header:
Authorization: Bearer sk_your_project_api_keyPOST /api/upload?projectId=<PROJECT_ID>
Content-Type: multipart/form-data| Field | Required | Description |
|---|---|---|
file |
β | The file to upload. Max 10 MB via server. |
folder |
optional | Subfolder path. Auto-provisioned if it doesn't exist. |
curl -X POST "https://your-app.railway.app/api/upload?projectId=proj_abc123" \
-H "Authorization: Bearer sk_proj_live_8f3d9a..." \
-F "file=@logo.png" \
-F "folder=brand-assets"Response:
{
"success": true,
"file": {
"id": "dreambit/my-app/brand-assets/logo_1717711234567",
"name": "logo_1717711234567.png",
"url": "https://res.cloudinary.com/.../logo_1717711234567.png",
"size": 45120,
"type": "image/png",
"created_at": "2026-06-06T18:00:00Z"
}
}GET /api/files?projectId=<PROJECT_ID>&folder=<FOLDER>&cursor=<CURSOR>&limit=<LIMIT>DELETE /api/files/<PROVIDER_ID>?type=<image|video|raw>GET /api/folders?projectId=<PROJECT_ID>POST /api/folders?projectId=<PROJECT_ID>
Content-Type: application/json
{ "name": "avatars" }For the full interactive reference, go to
/docsin your running app.
D2 Storage has an AI-Ready Copilot built into every project dashboard. This is one of its most powerful features.
- Open your project at
/dashboard/<project-id> - Click "AI Integration Prompt"
- A complete system prompt is generated with:
- Your Project ID and API key pre-filled
- Your deployment base URL (from
NEXT_PUBLIC_APP_URL) - All endpoint specs in the correct format
- Paste it into Claude, ChatGPT, Gemini, Cursor, or any AI tool
- Ask "Write me a JavaScript helper to upload files" β get working code in seconds
Note on the base URL: The AI prompt reads from
NEXT_PUBLIC_APP_URL. If it showslocalhost:3001after deployment, update that env variable to your real domain and redeploy.
d2-storage/
βββ src/
β βββ app/
β β βββ api/
β β β βββ upload/ # POST /api/upload
β β β βββ files/ # GET + DELETE /api/files
β β β βββ folders/ # GET + POST /api/folders
β β β βββ projects/ # GET + POST /api/projects
β β βββ dashboard/
β β β βββ page.tsx # Project list + session auth
β β β βββ [id]/ # Per-project workspace
β β βββ docs/ # In-app API documentation
β β βββ globals.css # All design tokens + theme
β β βββ page.tsx # Public landing page
β βββ components/ui/ # Custom UI components
β βββ lib/
β β βββ auth.ts # Master key validation
β β βββ cloudinary.ts # Cloudinary SDK wrapper
β β βββ cors.ts # Project-scoped CORS enforcement
β β βββ crypto.ts # API key hashing utilities
β β βββ db.ts # JSON flat-file database layer
β β βββ utils.ts # Shared utilities
β βββ sdk/ # Embeddable client SDK (WIP)
βββ data/ # Runtime data β gitignored
β βββ projects.json # Project registry
β βββ api_keys.json # Hashed API key store
βββ convex/ # Convex schema (reserved for future hosted version)
β βββ schema.ts
βββ docs/ # Internal design docs
βββ .env.example # Environment variable template
βββ package.json
D2 Storage uses a local JSON flat-file database (data/projects.json and data/api_keys.json) β no PostgreSQL, no Redis, no external database required. This is intentional: it keeps setup to zero.
β οΈ Thedata/folder must persist between restarts. This means:
- β Works great on Railway, Render, VPS, Docker (persistent filesystem)
- β Will lose data on Vercel, Netlify, AWS Lambda (ephemeral serverless filesystem)
If you want to deploy to Vercel/serverless, swap
src/lib/db.tsfor any key-value store (Vercel KV, Upstash Redis, PlanetScale, etc.). The interface is straightforward to replace.
Note on Convex: The
convex/directory andconvexnpm package are present but not yet active in this version. The schema is defined and ready β Convex is planned for a future multi-tenant hosted version of D2 Storage. It is not required for self-hosting.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 App Router |
| Language | TypeScript 5 |
| UI | React 19 |
| Styling | Tailwind CSS 4 + Shadcn UI + Base UI |
| Storage Driver | Cloudinary Node SDK v2 |
| Database | Local JSON flat-file β zero external dependencies |
| Auth | Native Node.js crypto module β hashed bearer tokens |
| Icons | Lucide React |
Important: D2 Storage needs a persistent filesystem to store
data/projects.jsonanddata/api_keys.json. Choose a platform that supports this.
- Push your fork to GitHub
- Go to railway.app β New Project β Deploy from GitHub
- Select your repo
- Add environment variables in Railway dashboard:
CLOUDINARY_CLOUD_NAME,CLOUDINARY_API_KEY,CLOUDINARY_API_SECRETAPI_KEYSNEXT_PUBLIC_APP_URLβ set to your Railway-generated URL (e.g.https://d2-storage.up.railway.app)
- Deploy β Railway handles the persistent volume automatically β
- New Web Service β connect your repo
- Build command:
npm install && npm run build - Start command:
npm start - Add the same environment variables
- Set
NEXT_PUBLIC_APP_URLto your Render URL
npm run build
npm start # runs on port 3001Or with Docker:
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci && npm run build
EXPOSE 3001
CMD ["npm", "start"]Mount a persistent volume at /app/data to survive container restarts.
Vercel's serverless functions have an ephemeral filesystem β your data/ folder will be wiped on each cold start. If you still want to use Vercel, replace src/lib/db.ts with Vercel KV or Upstash Redis. The db.ts interface is minimal and straightforward to swap out.
- Master password (
API_KEYS) is validated server-side only, never returned to the client - Project API keys are stored as one-way hashes β the plaintext is shown exactly once, at creation time
- CORS enforcement runs on every API request before any Cloudinary call
data/is gitignored β your project registry and key hashes never get committed- Dashboard sessions expire after 24 hours automatically via localStorage TTL
- Signed upload URLs β client-to-Cloudinary direct uploads for files > 10 MB
- Image transformations β proxy Cloudinary transform params (
?w=300&h=300) - Usage analytics β storage used, monthly bandwidth, uploads today
- Multi-key support β multiple admin keys with role separation
- SDK package β publish
@dreambitlabs/storageto npm - Convex backend β multi-tenant hosted version with user accounts
D2 Storage is an open-source project, and we welcome contributions, suggestions, and ideas of all kinds! Whether you want to fix a bug, suggest a new feature, or improve the documentation, we'd love to collaborate with you.
- Suggest an Idea or Feature: Have a recommendation to make D2 Storage better? Please open an Issue with the tag
enhancementdescribing your idea, use cases, and how it could work. - Report a Bug: If you notice unexpected behavior, file an Issue with a clear description, steps to reproduce, and screenshots if possible.
- Ask Questions or Discuss: Want to talk about architecture, deployment, or share what you've built? Open a thread in GitHub Discussions (or file a general issue if discussions aren't active).
- Write Code (Pull Requests):
- Fork the repository and create your branch from
main:git checkout -b feat/my-amazing-feature - Implement your changes. Make sure to keep the PR focused and minimal (this project is designed to be lightweight).
- Verify your changes compile correctly by running:
npm run build - Commit your changes with descriptive messages:
git commit -m "feat: add support for signed URLs" - Push to your branch and open a Pull Request!
- Fork the repository and create your branch from
We want to keep D2 Storage extremely fast, lightweight, and zero-config. Before writing complex integrations or adding external databases, please open an issue to discuss your proposal so we can align on the implementation approach!
MIT β use it, fork it, ship it. Attribution appreciated but not required.
Built with β by Dhairya Darji Β Β·Β TheDreamBitLabs open-source ecosystem