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:
npm run db:migrate:deployThis applies pending migrations such as S3 storage support before the API starts, so deployments from an older database can update safely without dropping data.
It also seeds the global Google OAuth config automatically when GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are set to real values in .env. If those values are blank or still placeholders, the backend still starts and logs a warning. Google connect/sign-in will be unavailable until you set real Google OAuth credentials and restart the stack:
docker compose up -d --buildAutomatic Docker startup seeding is usually enough. If you update Google OAuth values while containers are already running, seed the global Google OAuth config manually:
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.
Run production migrations before starting the backend:
cd backend
npm run db:migrate:deploy
npm run startOr use the combined command:
cd backend
npm run start:deploynpm run db:migrate:deploy uses Prisma production migrations and does not reset the database. If Prisma reports migration drift, stop the deploy and repair migration history first; do not run prisma migrate reset on production.
- 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

