9Drive is a storage gateway web app for connecting multiple Google Drive accounts into one virtual storage dashboard. Users can register with email/password or Google, automatically connect their first Google Drive account during Google sign-in, track quota, upload files into a dedicated 9drive Drive folder, organize files with virtual folders, preview files, sync MySQL from Google Drive, and let the backend route uploads to the Drive account with enough free space.
- React + Vite frontend.
- Express + TypeScript backend.
- MySQL database with Prisma migrations.
- Bearer token authentication.
- Email/password auth plus Google sign-in/register with automatic first Drive connection.
- Global Google OAuth config stored encrypted in DB.
- Optional reCAPTCHA on email/password registration.
- Direct upload stream to Google Drive. Files are not stored on the server.
- Google Drive uploads are stored under a root
9drivefolder. - Manual sync from the Google Drive
9drivefolder back into MySQL. - Multi-account storage quota summary.
- Quota tracker page.
- Virtual folders.
- File preview, download, rename, move, and delete actions.
- Bottom-right upload progress panel.
Live preview: https://9drive.zenhosta.com
backend/ Express API, Prisma schema, Google Drive integration
frontend/ Vite React app- Node.js 20+
- npm
- MySQL running locally
- Google Cloud project
- Google OAuth Client ID and Client Secret
Default database used by this project:
host: localhost
port: 3306
database: 9drive
user: root
password: emptygit clone git@github.com:zenhosta/9drive.git
cd 9driveInstall backend dependencies:
cd backend
npm installInstall frontend dependencies:
cd ../frontend
npm installCreate database:
CREATE DATABASE 9drive;If using MySQL CLI:
mysql -u root -e "CREATE DATABASE IF NOT EXISTS 9drive;"Create backend/.env:
DATABASE_URL="mysql://root@localhost:3306/9drive"
APP_PORT=4000
FRONTEND_URL="http://localhost:5173"
JWT_ACCESS_SECRET="change-this-jwt-secret-at-least-32-chars"
TOKEN_ENCRYPTION_KEY="change-this-encryption-key-32bytes!"
ACCESS_TOKEN_TTL_SECONDS=900
REFRESH_TOKEN_TTL_DAYS=30
MAX_UPLOAD_BYTES=5368709120
RECAPTCHA_SECRET_KEY=""
# Used only by `npm run seed:google-config`.
# These values are encrypted and stored in DB as global Google OAuth config.
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
GOOGLE_REDIRECT_URI="http://localhost:4000/connected-accounts/google/callback"Important:
JWT_ACCESS_SECRETshould be long and random.TOKEN_ENCRYPTION_KEYshould be long and random.- Do not commit
backend/.env. - Google OAuth credentials are used by the seed script, then stored encrypted in the database.
Create or confirm frontend/.env:
VITE_API_URL=http://localhost:4000
VITE_RECAPTCHA_SITE_KEY=Captcha is disabled when VITE_RECAPTCHA_SITE_KEY or backend RECAPTCHA_SECRET_KEY is empty. Set both values to enable captcha on registration.
cd backend
npm run prisma:migrateIf Prisma client generation is blocked on Windows by a running Node process, stop running backend/frontend dev servers and run:
npx prisma generateGoogle setup is done in Google Cloud Console, not Google Search Console. Google Search Console is for website indexing/search ownership. OAuth and Drive API are managed in Google Cloud Console.
Open Google Cloud Console:
https://console.cloud.google.com/- Open Google Cloud Console.
- Click project selector in top bar.
- Create a new project or select an existing project.
- Remember the project name because OAuth client and Drive API must be in the same project.
- Go to:
APIs & Services -> Library- Search:
Google Drive API- Open
Google Drive API. - Click
Enable. - Wait a few minutes if Google says the API was enabled recently.
Direct URL pattern:
https://console.developers.google.com/apis/api/drive.googleapis.com/overview?project=YOUR_PROJECT_IDIf Google Drive API is disabled, you will see an error like:
Google Drive API has not been used in project ... before or it is disabled.- Go to:
APIs & Services -> OAuth consent screen- Choose app type:
External- Fill required fields:
App name
User support email
Developer contact email- Add scopes:
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profileFull Drive access is required so Google sign-in can connect the first Drive account automatically and sync files manually added to the 9drive folder.
- If publishing status is
Testing, add test users.
Add every Google account that will test the app:
OAuth consent screen -> Test users -> Add usersIf you do not add test users, Google may show:
Access blocked: app has not completed the Google verification process
Error 403: access_denied- Go to:
APIs & Services -> Credentials- Click:
Create Credentials -> OAuth client ID- Application type:
Web application- Add authorized JavaScript origin:
http://localhost:5173- Add authorized redirect URI:
http://localhost:4000/connected-accounts/google/callback- Click Create.
- Copy:
Client ID
Client SecretPut values into backend/.env:
GOOGLE_CLIENT_ID="your-client-id"
GOOGLE_CLIENT_SECRET="your-client-secret"
GOOGLE_REDIRECT_URI="http://localhost:4000/connected-accounts/google/callback"Then run:
cd backend
npm run seed:google-configThis stores the Google OAuth config as a global encrypted provider config in MySQL. Google sign-in uses the same config and automatically connects the first Drive account. Logged-in users can still click Connect Drive in Settings to add more Drive accounts.
Start backend:
cd backend
npm run devBackend runs at:
http://localhost:4000Start frontend:
cd frontend
npm run devFrontend runs at:
http://localhost:5173This repository includes Docker files for running MySQL, backend, and frontend together.
Files:
docker-compose.yml
.env.docker.example
backend/Dockerfile
frontend/Dockerfile
frontend/nginx.confCopy the example env file:
cp .env.docker.example .envOn Windows PowerShell:
Copy-Item .env.docker.example .envEdit .env:
MYSQL_ROOT_PASSWORD=root
MYSQL_DATABASE=9drive
FRONTEND_URL=http://localhost:5173
VITE_API_URL=http://localhost:4000
VITE_RECAPTCHA_SITE_KEY=
JWT_ACCESS_SECRET=replace-with-long-random-secret
TOKEN_ENCRYPTION_KEY=replace-with-long-random-secret
RECAPTCHA_SECRET_KEY=
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URI=http://localhost:4000/connected-accounts/google/callbackCaptcha is disabled when either VITE_RECAPTCHA_SITE_KEY or RECAPTCHA_SECRET_KEY is empty.
docker compose up -d --buildServices:
frontend: http://localhost:5173
backend: http://localhost:4000
mysql: localhost:3306The backend container runs Prisma migrations automatically on startup:
npx prisma migrate deployAfter containers are running, seed the global Google OAuth config:
docker compose exec backend npm run seed:google-configThis stores GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REDIRECT_URI from Docker env into MySQL as encrypted global config.
docker compose logs -f backend
docker compose logs -f frontend
docker compose logs -f mysqldocker compose downRemove database volume too:
docker compose down -v- Replace localhost URLs with production domain.
- Update Google OAuth authorized JavaScript origin.
- Update Google OAuth redirect URI.
- Use strong
JWT_ACCESS_SECRETandTOKEN_ENCRYPTION_KEY. - Do not expose MySQL port publicly in production.
- Put frontend/backend behind HTTPS reverse proxy.
- Rebuild frontend when
VITE_API_URLchanges because Vite embeds env at build time. - Rebuild frontend when
VITE_RECAPTCHA_SITE_KEYchanges because Vite embeds env at build time.
- Open frontend:
http://localhost:5173- Register a user with email/password and captcha, or click
Continue with Google and connect Drive. - If using Google sign-in, approve Drive access once and confirm
/settingsalready shows the connected account. - If using email/password, open
Settings, clickConnect Drive, approve access, and confirm the account appears. - Open
Quota Tracker. - Confirm quota appears.
- Open
All Files. - Create nested virtual folders.
- Upload a file and confirm it appears under Google Drive root folder
9drive. - Add or remove a file manually inside Google Drive folder
9drive, then clickSync Drivein All Files. - Watch bottom-right upload progress.
- Right-click file row for actions:
View
Download
Rename
Move to Folder
DeleteAuth:
POST /auth/register
POST /auth/login
GET /auth/google/url
GET /auth/google/callback
POST /auth/google/exchange
POST /auth/refresh
POST /auth/logout
GET /auth/meGoogle accounts:
GET /connected-accounts/google/connect-url
GET /connected-accounts/google/callback
GET /connected-accounts
POST /connected-accounts/:id/sync-quota
DELETE /connected-accounts/:idStorage:
GET /storage/summaryFolders:
GET /folders
GET /folders/recent?limit=4
POST /folders
DELETE /folders/:idFiles:
GET /files
GET /files?folderId=<id>
GET /files?q=<search>
GET /files/shared-links
GET /files/:id
PATCH /files/:id
PATCH /files/batch
DELETE /files/batch
POST /files/sync-google
POST /files/:id/share
DELETE /files/:id/share
POST /files/:id/preview-token
GET /files/:id/view-url
GET /files/:id/download
DELETE /files/:id
GET /files/preview/:tokenUploads:
POST /uploadsUpload is multipart/form-data. Metadata fields should be appended before the file:
sizeBytes
fileName
mimeType
folderId optional
file- Backend never stores uploaded files on disk.
- Uploads are streamed through the backend to Google Drive folder
9drive. - Google tokens are encrypted in MySQL.
- Refresh tokens for app sessions are hashed in MySQL.
- Google auth handoff tokens, public share tokens, and preview tokens are hashed before lookup/use.
backend/.envis ignored by git.- Do not expose
TOKEN_ENCRYPTION_KEY,JWT_ACCESS_SECRET,RECAPTCHA_SECRET_KEY, OAuth client secrets, or raw share/preview/handoff tokens.
- Replace localhost redirect URIs with production URLs.
- Add production domain to Google OAuth authorized origins.
- Set OAuth consent screen to production when ready.
- Google may require verification for public apps.
- Use strong secrets.
- Put the backend behind HTTPS.
- Consider secure cookies or stronger token storage for production.
Backend:
cd backend
npm run buildFrontend:
cd frontend
npm run build

