Open-source project
zenhosta/9drive avatar
zenhosta/9drive

9Drive: pooled Google accounts, a whole-Drive scope, and a sync button

9Drive is a storage gateway web app for connecting multiple Google Drive accounts into one virtual storage dashboard. Users can connect Google Drive accounts, track quota, upload files, organize files with virtual folders, preview files, and let the backend route uploads to the Drive account with enough free space.

2,219 stars427 forksTypeScriptApache-2.0

At a glance

What is it?
zenhosta/9drive is an Express and React gateway that presents several Google Drive accounts as one dashboard and routes each upload to whichever account has room. Its file is generous about features and quiet about three things worth reading first: the Drive scope it asks for, the default secrets in its compose file, and a Settings button that runs a shell script on the host.
Who is it for?
Read 9Drive as an upload router with a file browser attached, and judge it on that. It suits a person or a small team who has several Drive accounts, sometimes several storage providers, and wants one quota view and one place to drop files without caring which account absorbs the bytes.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 42 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 5, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two stores, and a button to reconcile them

The upload path and the catalogue are deliberately different stores. Bytes go to a Google Drive account or an S3 bucket, while the index that makes them browsable lives in MySQL under Prisma migrations. On the Drive side everything is written under one root folder named `9drive`, so the app's footprint inside an account is a single directory, and files are streamed straight through rather than landing on the server. The feature list then names the reconciliation explicitly: manual sync from the Google Drive `9drive` folder back into MySQL. One word carries the design. Manual. Nothing in the file describes a background job, a webhook or a poll that keeps the database in step, so any file added to that Drive folder by hand, by another client or by a sync tool will not appear in the dashboard until somebody presses sync. Virtual folders are a second layer on top of that, which means the tree a user browses is not the tree in Drive.

Three routing modes, one external endpoint

Choosing where a file lands is the feature the project is named for. Upload routing has three named policies: most-available, round-robin, and priority-order, so the same pool can be used as a capacity pool, as a striped pool, or as an ordered fallback chain. Files are not stored on the server, and for S3 the stream goes through the backend specifically so storage credentials are never exposed to the frontend, which is the correct shape for a browser-driven upload. The external surface is one route, `POST /api/v1/uploads`, authenticated with API keys rather than a session, and the key handling is the most carefully specified item in the list: keys are stored hashed, the secret is shown exactly once, each key records when it was last used, and any key can be revoked. Those four properties together are what makes an upload key safe to hand to a script, so it is worth checking they are actually implemented rather than aspirational before you build on them.

The scope requested is the whole Drive

The OAuth walkthrough asks for `https://www.googleapis.com/auth/drive`, which is unrestricted access to the account's files, plus `userinfo.email`. That is the difference between a scope that lets an app create its own files and one that lets it read and delete anything the signed-in user owns, and it is the scope a storage gateway needs in order to reorganise across an account's existing content. The consent screen is configured as an External app with an app name, a support email and a developer contact email, and the OAuth client ID and secret are not stored in an environment file at run time: they are consumed once by a seed script, `npm run seed:google-config`, and then held encrypted in the database as a global OAuth config. That config can also be set or changed from the Settings UI, which means whoever holds an admin session can replace the OAuth identity the whole installation signs users with.

The Settings page runs a shell script on the server

One line in the feature list deserves more attention than it gets. Automated system updates are offered via `update.sh`, triggered directly from the Settings UI, with the parenthesis noting a PM2 setup. That is a web-reachable action whose effect is to execute a script on the host, so two questions follow that the file does not answer. Where does `update.sh` come from, since the repository tree contains no such file; the root holds `.env.docker.example`, `AGENTS.md`, `LICENSE`, `README.md`, `backend/`, `docker-compose.yml`, `frontend/`, `setup.ps1` and `setup.sh`, and the script is not among them. And what stops a session from invoking it repeatedly, or with a modified script if it is fetched rather than local. The PM2 mention suggests the intended deployment is a bare Node process manager rather than the containers the compose file describes, which means the two deployment paths in this repository may not have the same exposure.

The compose file starts with published secrets

The deployment file is templated throughout, which is good practice, and the defaults it chooses are the ones to change first:

yaml
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-change-this-database-password}
JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:-change-this-jwt-secret-at-least-32-chars}
TOKEN_ENCRYPTION_KEY: ${TOKEN_ENCRYPTION_KEY:-change-this-encryption-key-32bytes!}

A stack that comes up with `docker compose up` therefore runs with a root database password of `root`, a known application password, and two known cryptographic secrets, and the app will happily sign tokens with them. The manual `.env` template repeats the same two token strings as its placeholder values, so a person who copies the template by hand inherits the same defaults as a person who copies the compose file. Neither is wrong as a starting point, and both are labelled as things to change, but the distinction matters: the compose file is the path that works with no edits, so it is the path where the defaults are most likely to survive.

The same variable carries two different defaults

Compare the manual environment template with the compose file and the numbers stop matching. Access tokens live for 900 seconds in the template and 3600 seconds in the compose default, so the documented session length is fifteen minutes and the deployed one is an hour, from the same variable name. The refresh window agrees at 30 days in both. The upload ceiling is set to 5368709120 bytes, which is five gibibytes, in both places, but only one of them is overridable: the compose file templates every other variable and hardcodes this one, so changing it requires editing the compose file rather than setting an environment variable. The database defaults differ too. The requirements section states the project's default database as host localhost, port 3306, database 9drive, user root, with an empty password, while the compose file creates a user named 9drive with a password. The setup script asks for a connection URL, so the documented empty-password default describes a manual install and not the container one.

The section numbering jumps from 2.3 to 4

The install documentation opens with the recommended path as section 1, covering the automated setup script for Windows in PowerShell and for Linux and macOS, then the manual path as section 2 with 2.1 for dependencies, 2.2 for creating the database and 2.3 for the backend environment file. The next heading is `4. Frontend Environment`, then `5. Run Prisma Migrations` and `6. Google Cloud Setup`. There is no section 3, and nothing in the visible text explains where it went, which makes the sequence ambiguous for anyone following it from a link that lands on section 4. The contents themselves are sound and unusually practical: the frontend file is two variables, captcha is disabled when either the site key or the backend secret is empty and enabled when both are set, and the Prisma section includes the Windows failure case where client generation is blocked by a running Node process, with `npx prisma generate` as the workaround once the dev servers are stopped.

Only the database is healthchecked, and the frontend URL is a build argument

The compose file defines three services on MySQL 8.4 and checks one of them. The database has a health check built on `mysqladmin ping`, with a five second interval, a five second timeout and twenty retries, and the backend waits for it by condition. The frontend's dependency on the backend is the short form, a list containing backend, which means it starts as soon as the container is up rather than when the API answers, and neither the backend nor the frontend has a health check at all. The frontend is also mapped as 5173 to 80, so the built app serves on port 80 inside its container, and its API address arrives as a build argument rather than a runtime variable:

yaml
VITE_API_URL: ${VITE_API_URL:-http://localhost:4000}

Because Vite inlines that value at build time, pointing the frontend at a different backend means rebuilding the image, not restarting it. The setup section matches that ordering, telling you to run `npm run prisma:migrate` and `npm run dev` in `backend` and `npm run dev` in `frontend` in separate terminals, with Node.js 20 or newer required on both.

Editorial conclusion

Read 9Drive as an upload router with a file browser attached, and judge it on that. It suits a person or a small team who has several Drive accounts, sometimes several storage providers, and wants one quota view and one place to drop files without caring which account absorbs the bytes. It is the wrong shape for an auditor: the catalogue of files lives in MySQL while the bytes live in Drive, and reconciliation is a button. Three things to check before you run it. Replace every default secret in the compose file, because the file starts with a root password of `root`, a database password of `change-this-database-password` and two published token secrets, so an untouched deployment is an exploitable one. Decide whether you accept the `auth/drive` scope, which is the whole Drive rather than the per-file scope, and remember the OAuth client is global and stored encrypted in the database, editable from the Settings UI. And look at the update button before you expose the app, because the Settings page is documented as running `update.sh` on the server, and that script is not in the repository tree.

Frequently asked questions

What does 9Drive do with several Google Drive accounts?

It shows them as one virtual storage dashboard. You connect multiple accounts, see a combined quota, upload into a dedicated root 9drive folder, organise files in virtual folders, preview them, and let the backend route each upload to the account with enough free space.

Which storage backends can 9Drive talk to?

Google Drive and S3-compatible storage in the same dashboard, with custom endpoints for providers like MinIO, Cloudflare R2, Wasabi, Backblaze B2 and AWS S3. S3 uploads stream through the backend so storage credentials are never exposed to the frontend.

How does 9Drive handle API keys for uploads?

Keys are stored hashed, the secret is displayed only once, last use is tracked per key, and any key can be revoked. The external endpoint is POST /api/v1/uploads and requests use bearer token authentication.

What does the 9Drive setup script actually do?

On Linux and macOS you run bash ./setup.sh, and on Windows powershell -ExecutionPolicy Bypass -File .\setup.ps1. It generates local environment files with keys, installs dependencies, generates the Prisma client, and prompts for the MySQL connection URL. Google client credentials can be skipped and set later.

What does 9Drive need installed before it will run?

Node.js 20 or newer, npm, a local MySQL with version 8 or newer for development, and a Google Cloud project with the Drive API enabled plus an OAuth client ID and secret. The Docker path supplies MySQL 8.4 itself.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. Project website
  4. README
  5. zenhosta/9drive on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/zenhosta-9drive.svg)](https://hysenlabs.com/projects/zenhosta-9drive)