# Foxel, a self-hosted file manager where the storage backends are adapters

> Foxel puts one interface over local disk, S3, WebDAV, SFTP, four cloud drives, Telegram and other Foxel instances, and layers on role-based access control, path rules with wildcards and regex, share links, a manifest plugin system, background task processing and embedding-based semantic search. The dependency list is the interesting part: an agent framework, a Telegram client and two vector databases in a file manager.

**DrizzleTime/Foxel** — A highly extensible private cloud storage solution for individuals and teams, featuring AI-powered semantic search.

- Repository: https://github.com/DrizzleTime/Foxel
- Website: https://foxel.cc
- Stars: 1,086 · Forks: 99
- Language: Python
- License: MIT
- Published: 2026-09-30 · Updated: 2026-09-30 · Language: en
- Canonical page: https://hysenlabs.com/projects/drizzletime-foxel

## Adapters, and the reason cloud drives sit alongside SFTP

The core feature is a pluggable storage adapter pattern, and the adapter list is where the project's real character shows.

Three categories, and the categories are ordered by how the protocol behaves rather than by how common it is. Standard protocols: local filesystem, S3-compatible object storage, WebDAV, SFTP and FTP. Cloud drives: Google Drive, OneDrive, Dropbox and Quark. And a special group that is harder to place: Telegram, AList, and Foxel-to-Foxel.

A unified file management layer over that set is a real problem rather than a feature list, because each backend has a different model underneath. Local disk has a real path hierarchy and POSIX permissions. S3 has keys and a prefix illusion of a hierarchy, with no rename and no partial overwrite. WebDAV has a hierarchy and a locking model that behaves differently per server. A cloud drive has a document model with revisions that sits underneath a file interface. A Telegram chat is a message history with an attachment per message, which is not a filesystem at all and only resembles one if you choose to make it.

So the adapter contract is where this project either has done good work or has papered over the differences, and the honest answer is that the README does not tell you. It says the adapter pattern makes it easy to integrate various storage types, and it lists the permissions as configurable per adapter, which implies the abstraction is real rather than a thin wrapper. What it does not describe is how the differences are surfaced: whether a move on S3 is a copy plus a delete, whether a rename fails, and how a permission on a path is expressed when the backend has no paths.

The last group explains a lot about the project's likely origin. AList is itself a multi-backend file listing tool, so listing it as a backend is the same problem one level up. Telegram as a backend is unusual enough to say something: a cloud drive you access with an API token, a large free quota, and a bot client rather than a login. The `telethon` dependency in the manifest is the mechanism, and a Telegram-based backend for a personal file store is a coherent choice for someone who wants their archive somewhere that is not tied to a subscription.

Foxel-to-Foxel is federation. One Foxel presenting another as a backend means a small deployment can front a larger one, or two deployments for two teams can present as one interface, and that is the feature that makes the adapter list a design rather than a checklist.

## Path rules with wildcards and regex, which is where the real access model is

Most of the access control in a file manager is role assignment: this person is an admin, that person is a viewer. Foxel's more interesting feature is path-based rules, and it is worth spending time on because it is the difference between a permission model that scales and one that does not.

You can define read, write, delete and share permissions per path, with support for wildcards, regex patterns, and priority-based rule ordering. Three capabilities in one sentence, each solving a distinct problem.

Wildcards handle the ordinary case: everything under a shared folder for the team, everything under a personal folder for its owner. Regex handles the cases wildcards cannot, which are the ones that actually appear in practice. A rule matching a path component that looks like a date, a version prefix, or a generated identifier is not expressible with a glob that anyone can reason about. Priority ordering handles the case where rules overlap, which is the one that makes the whole thing usable: without a defined precedence, two rules that both match produce undefined behaviour, and a permissions system with undefined behaviour is a permissions system nobody trusts.

The built-in roles are three, and the way User is described is the tell. Admin has full access, Viewer is read-only, and User has configurable access. Making the middle role configurable rather than fixed is the design choice, because it means the same role can be granted different path rules for different people. A user with read on a finance folder and write on their own documents is one role, not two, which keeps the role list short enough that administrators can hold it in their heads.

Custom roles with fine-grained system and adapter permissions are the extension point, and the phrase adapter permissions is the part that matters. It means access is not only to paths but to backends, so a person can be permitted to read a particular S3 bucket and not touch the Telegram account at all. On a system with a dozen backends of different sensitivity, that separation is not a refinement.

Audit logging completes the model, and the fields named are the ones that make a log useful: user, IP, method, status and duration. Recording the method means you can reconstruct what was attempted, and recording the duration means you can find the request that took three minutes against a slow SFTP host.

## Semantic search, and the two vector databases sitting in the dependency list

The headline feature is AI-powered semantic search, described as going beyond filename matching to search by natural language description to find content within images, documents and other unstructured data, powered by configurable embedding providers and vector databases.

The design choice that makes this credible is configurability on both sides. The embedding provider is configurable rather than hard-coded, which means you choose the model that produces vectors, and therefore you choose what leaves your machine. That matters a great deal for a system whose purpose is holding your photographs. An open-source embedding model run locally and a hosted API are the same interface with very different privacy consequences, and a project that lets you choose is making a different decision from one that picks for you.

The vector database is also configurable, with Milvus and Qdrant both named, and the manifest shows both client libraries present. Milvus appears with the Lite extra, which is the embedded variant that runs in-process or locally without a server, so the small deployment and the large one use the same code. Qdrant is a separate service. The README does not say how the choice is made, and that is a question worth asking, because an embedded vector store and a client-server one have different operational profiles and different scaling ceilings.

There is a real cost question here that the feature list does not address. Embedding every image and document in your library is a background job proportional to the size of the library, and the manifest includes a croniter, which is the scheduling library, so scheduled indexing is clearly part of the design. But indexing is a one-time cost with a permanent CPU and storage footprint, and for a user with two hundred thousand photographs it is a decision, not a default. The project does not say whether indexing is on by default, whether it is opt-in per adapter, or how much it costs.

What you get in exchange is search that works on content you cannot remember the filename of. That is a genuine gap for anyone with an image library organised by date, and for scanned documents it is the difference between a search box that helps and one that does not.

The manifest also carries an MCP dependency and a separate HTTP client library alongside the mainstream one, which suggests the semantic search and agent features reach out to more than one kind of service.

## The agent with VFS tools, and a plugin system that loads code at runtime

Two features here change the risk profile of the whole application, and they are worth reading together rather than separately.

The integrated AI agent has built-in tools for virtual filesystem operations, web fetching and file processing. A tool set with filesystem access plus web fetching is a combination that needs thought, because the combination is what turns a prompt injection in a fetched web page into writes in your file storage. The README describes the capability and does not describe the boundaries, so an evaluator should assume the default: an agent that can write files and can read arbitrary web content is a path from any untrusted document to your storage, and the mitigations, if any, are not documented here.

The plugin system is the other one. It is described as a manifest-based architecture that can load React frontend components and custom backend routes at runtime without modifying the core codebase. The phrase without modifying the core codebase is the appeal and the risk in the same breath. A plugin that adds backend routes is code running inside your application process, with the application's database credentials and the user's permissions available to it.

Neither feature is unusual in kind. What is unusual is having both in a file manager, and what makes it worth a careful read is the combination with the path-based access control from the previous section. An agent that operates through the same permission model as a human user is constrained by it, and one that does not is not. Whether the agent's tool calls are subject to the same path rules is not stated, and that single question determines whether the RBAC system is a security boundary or a UI convenience.

The three-layer task processing centre is the least dramatic feature and possibly the most useful. File indexing, data backups and scheduled jobs run asynchronously without affecting the main application, which is the correct architecture for work that takes minutes, and the manifest's croniter confirms scheduled jobs are a first-class concept rather than a cron job bolted on later.

The protocol mappings deserve a note because they cut the other way. An S3-compatible endpoint, a WebDAV mount, and temporary signed URLs for direct access mean Foxel's storage is reachable by tools that know nothing about Foxel, which is excellent for scripting and means a permission model expressed in Foxel's terms is not the only perimeter. The temporary link feature is the sharpest example: a signed URL is a bearer token, so its expiry configuration is a security setting.

## The compose file ships a signing key, and the pull policy ships an update policy

The quick start is four steps, and two of them contain a decision every self-hoster should look at before typing them.

Creating the data directories:

```bash
mkdir -p data/db data/mount
chmod 777 data/db data/mount
```

The mode 777 is what the README tells you to use, and it is what the Dockerfile also applies at build time. It is a defensible choice in a container that runs as an unpredictable user and needs to write to a bind mount, and it is also a data directory holding your entire file index and metadata that any local account can read and write. On a single-user machine that is a non-issue. On a shared host it is not, and the fix is a group with a narrower mode and a matching user id inside the container.

The more serious item is in the compose file rather than the instructions. Both the main signing key and the key used for temporary links are set in the environment block to the same hard-coded value, in plaintext, in a repository everyone can read. The README does address this, because it says it is strongly recommended to modify the environment variables in the compose file to ensure security, and to replace both keys with randomly generated strong ones. So the author knows, and the mitigation is documented, and the default is still a published key.

A published signing key is only a problem if it is used unchanged, and the consequence of that is a signed token anyone can mint. Depending on what the key signs, that ranges from a temporary link that bypasses the share expiry to a session token. The right procedure is to generate both keys before the first start, put them in a .env file rather than in the compose file, and treat any instance that ran with the default as needing a rotation.

The other item is the pull policy. The compose file sets the image to pull always, which means the container is replaced with whatever is currently published on the next restart. Combined with a project that publishes tagged releases rather than stable channels, that is an uncontrolled upgrade path. A self-hosted service holding your file index should be pinned to a version, and the compose file's only alternative to the always tag is a commented mirror for users in China, so the operational choice is yours to make and easy to get wrong.

The Chinese mirror line is also worth noting as a fact about who this is for: the time zone is set to Shanghai by default and a university mirror of the registry is offered as a commented alternative, which tells you the primary audience is in China and the secondary audience is not.

## Python 3.14, uv and Bun, and a build that fetches a migration tool at image build time

The toolchain choices are recent, and one of them has a consequence worth understanding.

The manifest requires Python 3.14 or newer, and the container is built on a Python 3.14 slim base. The frontend is React 19 with TypeScript and Vite, and the Dockerfile builds it in a separate stage from a Bun image, so the front end and the back end have different toolchains in the same repository, which is normal and which the multi-stage build handles cleanly.

Python 3.14 as a floor is aggressive. It means the application cannot run on a distribution's default Python, which for several years will be 3.11 or 3.12, so a self-hoster either uses uv to manage the interpreter or runs the container and nothing else. Given that the recommended path is Docker Compose, that is largely fine, and the container approach is the right one for a system with a database and a vector store in it.

The dependency list is unusual for a file manager and is worth reading as a statement of what the features cost. There is aioboto3 for S3, paramiko for SFTP, pysocks for proxy support, telethon for the Telegram backend, pillow for image work, croniter for scheduling. Two HTTP client libraries with different version ranges, plus an MCP package, plus a Telegram client, plus two vector database clients. Then pydantic with the email extra, which is the sign that the authentication system sends mail, tortoise-orm for the database, and PyJWT for tokens.

The database is SQLite, with Tortoise ORM, and there is no Redis anywhere in the list. For a system with background jobs, a task centre, a plugin system and an agent, that is a deliberate constraint. It means one less service to run, which for a self-hosted tool is a real advantage, and it means the write concurrency ceiling is a single file. The compose file confirms it: one service, one container, no database service, no vector database service, which means the semantic search is running on the embedded variant of whichever vector store you configure.

The build has one line that deserves a flag. The Dockerfile downloads a tarball from a different repository, a migration project, extracts it into the image and keeps it. That means the image build depends on the current contents of that repository's main branch, not on a pinned version, so two builds of the same commit can produce different images. It is also a supply chain dependency that the repository's own licence does not cover, and it is not mentioned in the README. Anyone building the image for a production deployment should replace that fetch with a pinned artefact.

## Conclusion

Adopt Foxel if you have files scattered across local disk, an S3 bucket, a NAS over WebDAV or SFTP and a cloud drive, and want one permission model over all of them rather than four, and if you can run a current Python. Do not deploy the shipped compose file as it stands, because it carries a hard-coded signing key in plaintext and an image pull policy that will replace your running version on the next restart. Verify first by generating your own two signing keys before the first start, reading the plugin manifest format before installing anything from a third party, and testing the semantic search against a folder of images to see whether the embedding provider you configure is worth the indexing cost.

## FAQ

### What storage backends does Foxel support?

An adapter pattern covers three categories. Standard protocols: local, S3-compatible, WebDAV, SFTP and FTP. Cloud drives: Google Drive, OneDrive, Dropbox and Quark. Special: Telegram, AList, and Foxel-to-Foxel. Browsing, upload, download, move, copy and delete all go through one interface.

### How does Foxel's access control work?

Role-based access control with three built-in roles, where Admin has full access, Viewer is read-only and User is configurable, plus custom roles. On top of that, path-based rules define read, write, delete and share per path, with wildcards, regex patterns and priority-based ordering, and audit logging records user, IP, method, status and duration for every action.

### What is Foxel's semantic search and what does it cost?

It searches by natural language description across images, documents and other unstructured data, using configurable embedding providers and vector databases, with Milvus and Qdrant both supported. The README does not state whether indexing is on by default, how it is scheduled beyond the presence of a cron library, or what the CPU and storage cost is for a large library.

### What is the Foxel plugin system?

A manifest-based architecture that loads React frontend components and custom backend routes at runtime without modifying the core codebase. A plugin that adds backend routes is code running inside the application process, and the README does not describe any permission boundary or sandboxing for plugins.

### How do I install Foxel with Docker Compose?

Create data/db and data/mount directories, download the compose file, then run docker-compose up -d and open it in a browser, following the setup guide to initialise the administrator account. Before starting, the README strongly recommends replacing the SECRET_KEY and TEMP_LINK_SECRET_KEY environment variables, both of which ship with the same hard-coded value in the compose file.

### What is the tech stack behind Foxel?

Python 3.14 or newer with FastAPI, Tortoise ORM and SQLite on the backend; React 19, TypeScript, Vite and Ant Design on the frontend; JWT under OAuth2 with bcrypt for auth; Milvus Lite or Qdrant for vectors; and Docker with Gunicorn and Uvicorn for deployment. uv manages Python and Bun manages the front end.

## Sources

- [DrizzleTime/Foxel on GitHub](https://github.com/DrizzleTime/Foxel)
- [License: MIT](https://github.com/DrizzleTime/Foxel/blob/main/LICENSE)
- [Project website](https://foxel.cc)
- [README](https://github.com/DrizzleTime/Foxel/blob/main/README.md)
- [Releases](https://github.com/DrizzleTime/Foxel/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/drizzletime-foxel
