Warehouse is a distributed object storage system (an alternative to S3) that is fully self hostable
Warehouse is a distributed object storage system (an alternative to S3) that is fully self hostable
🔥 AVD marked your project as well cooked! As a prize for your nicely cooked project, look out for a bonus prize in the mail :)
I built Warehouse, a distributed object storage system from scratch!
It has three main components:
There is also a Go SDK, although the documentation is limited to bucket and object management
My motivations to build it were that I was unhappy with the current feature set of S3. Unfortunately, Warehouse doesn’t currently have more features than S3, but that’s because I underestimated how long it would take to build.
I built it to be horizontally scalable, by making it possible to extend the storage pool with multiple “volume servers”.
Its also optimised for small files, using large “volume” files which contain many small files, which reduces disk operations and actually safes storage by reducing the amount of metadata needed.
It has support for very large files, using a chunking system, where files are split into 80 MiB chunks, and stored across multiple volumes for faster uploads/downloads. The chunking and reassembly of files is managed on the client side, to reduce load on the server.
My favourite features are:
I made the Warehouse setup process even easier, by creating another project Warehouse Manager that lets you setup warehouse with two commands!
To do this, I added 2 flags to the master server, --init, which stops the server after generating tokens, and --json, which sets the structured log output to JSON for easy parsing.
I also updated README.md and added docs for Warehouse Manager, to get ready for shipping tomorrow!
Documentation
Commit a4b31017dc - README
Commit 6eaf6fa943 - Docs
Log in to leave a comment
I added a button to create API keys, and I made it so updating a key will automatically update it in the list. These two features were quite quick to implement, my previous code had done most of the work already.
Now Warehouse is almost ready to ship! I just need to do some polishing first
v0.37.2 Binaries
Commit 541755f49a - Create API keys
Commit 07a9e856df - Automatic updates
Log in to leave a comment
You can now update API key permissions through the API and web UI!
It took a while because I had to create components to manage bitfields, which is a bit complicated. In the demo video, you can see the computed JSON which has the calculate bitfields
I also made it so you can switch API keys from the web UI
Next I’ll automatically update the list of keys with new permissions
v0.37.0 Binaries
Commit 94f335d07b - Edit API keys
Commit 127a3dfe03 - Change API key
Log in to leave a comment
You can now delete API keys in the web dashboard. You might notice that you can still access the web dashboard after deleting the API key. This is because although the refresh key has been deleted, the JWT has not expired yet. When the client attempts to refresh the JWT, it will fail.
To immediately invalidate all JWTs, you can restart the master server, which will regenerate the private/public keys.
I should probably set a shorter expiry, though.
Log in to leave a comment
A previous issue was that Warehouse used one API key for everything, and the web UI gave public access. To improve security, I implemented JWTs with Refresh Tokens.
A JWT is simply a signed string, containing the payload (in this case permissions) and a signature, to verify the JWT was created by a trusted source (the master server). JWTs are stateless, meaning they do not require a database call to verify them, so they are faster then session tokens.
However, JWTs can not be revoked. To avoid security issues, a JWT must expire in a relatively short time, and be refreshed before they expire. I updated the SDK to automatically handle JWT refreshing.
I store permissions in the JWT using flagsets. The permissions available are:
You can also give each token permissions to read/write specific buckets, for example I can give the token permission to only read a bucket and its objects, or only write to buckets.
This permission system allows you to minimise the risk of leaking tokens by only allowing certain tokens certain actions. Unfortunately it also slightly complicates the setup process, as seen in the attached video and docs
Next I’ll add manual token creations/deletion.
v0.34.0 Binaries
Commit c5c1092c14
Commit 58c820df3b (Docs update)
I:
Log in to leave a comment
I made docker images! Each image is distroless, containing only the binary and the master contains a migration tool. This means that they are really lightweight. Using docker means I don’t have to worry about testing for Windows/MacOS
To do this I had to change a few things:
Next, I need to update docs to prioritise docker instead of binaries, and show an example docker compose
Log in to leave a comment
I made it so uploading an object will update the object list. This was super easy to implement thanks to the HTMX Upsert Extension
I also updated the sort order to be based off of recency
Log in to leave a comment
You can now upload files directly from the web UI. This was actually really fun to implement. Maybe JavaScript isn’t that bad…
Currently the object list doesn’t get updated, that’s up next.
I also made it so you can click to dismiss toasts, and cleaned up some code
v0.30.0 Binaries
Commit 3833fb5fe3 - Object Uploads
Commit 447f4e5b1a - Click to dismiss
Log in to leave a comment
You can now download objects from the web UI.
In chromium browsers, it uses showSaveFilePicker for a streaming download which keeps memory usage low. In firefox, its unimplemented so a ponyfill is used. It seems this ponyfill might load the entire file into memory before writing to disk, not good.
Next up I’ll add uploads
Log in to leave a comment
Log in to leave a comment
Was basically due to bad tracking of data offset and size.
I explained my debugging step by step here
Unfortunately this did not fix the bug with invalid keys. I’m struggling to replicate it though…
Log in to leave a comment
I added a button to delete objects in the web UI. I encountered an issue using HTMX where id selectors with special characters wouldn’t be targeted properly. To fix this, I had to use a CSS escaping library. Apparently, noone has ever needed to do this in go before, so I had to adapt a JavaScript one to Go.
I also found a couple of issues:
Maybe I’ll do a rewrite in rust. Writing safe code is not my strong suite…
v0.26.0 Binaries
Commit 1388f39b83 - Delete object button
Commit eb28b88d8d - CSS Escaping package
The new usage after a compaction is now displayed.
HTMX Multi-Target Updates were being annoying… For some reason OOB swaps never work for me, but hx-partials do.
Log in to leave a comment
I added a button to compact volumes (remove deleted data from the volume file).
To do this, I had to write a toast system, which was annoying because I had to not use the native dialog element, because it renders on a separate “top-layer” which means the toasts could not be rendered above it.
Next up I will make it update the displayed usage after compacting
Log in to leave a comment
I implemented chunking! It wasn’t as hard as I expected.
The way it works is that objects are split into 80 MiB chunks, and clients handle splitting uploads, sending a request for each chunk, then the volume confirms each chunk is uploaded, with the master updating the object locations when all chunks have been uploaded. The client must also reassemble chunks. Chunking also spreads files more evenly across volumes
I updated the SDK to handle this automatically, and concurrently so multiple chunks can be downloaded/uploaded at the same time.
Some features were removed:
This update massively decreases memory usage, but does increase CPU usage depending on how many chunks get downloaded/uploaded in parallel. Large files get uploaded faster, and small files probably get uploaded a tiny bit more slowly. I need to do benchmarks to know for sure, and find out what number of concurrent chunk uploaders/downloaders is optimal.
There’s probably still some optimisation to do to avoid copying and stuff. And I need to update the docs
Log in to leave a comment
Now the master server keeps track of volumes free space, so it can check if there is enough free space before telling a client to put to it, otherwise checking the next volume.
This update was actually hell. Everything broke :)
What I fixed:
Unfortunately memory usage is increased as a result of loading the whole data into a buffer and Go GC taking ages to clean it up. Luckily I have 32GB of RAM, my old PC would have crashed…
I don’t think there’s a way to fix the memory usage apart from chunking the files into far smaller pieces - that is not gonna be fun to implement…
Also updated Docs with a custom footer
Log in to leave a comment
I found a memory leak where every object write never got released from memory 😬
Fixed this by not using byte arrays and instead an io.ReadCloser, and writing directly to the file.
Can’t believe I wasn’t doing this before…
I also updated the docs to include object RPCs.
v0.21.0 Binaries
Commit 9246b45848 - Memory Leak
Commit b9c85a3416 - Docs
Log in to leave a comment
I wrote docs using Hugo. It includes how to self host, and how to use the new Go SDK (not completed though). I also fixed an issue where bucket deletion wouldn’t work.
v0.20.1 Binaries
Commit 63c5d29d75 - Docs
Commit cce395e1a5 - Bucket deletion fix
Log in to leave a comment
I wrote a SDK for go (a wrapper around the generate gRPC client) with nice features like:
I also fixed a bug where you couldn’t start the master server without migrating the schema
Next up I’ll probably write some proper docs with hugo, or add chunking support (or not, its a lot of effort), or add a compact button to the volumes list
Log in to leave a comment
I did this by:
--schema) to the server binary that allows for automatically applying database schema, and include the schema in releasesReleases are available at Codeberg if you want to try it yourself. These include the README with install instructions.
Log in to leave a comment
I added an action to list volumes of a server, which shows usage and object count of each volume.
I need to display the wasted space count, and add an option to compact the volume.
I also fixed a bug where terminating the volume server before it had synced the needle locations to disk would result in the data being inaccessible. I fixed this by handling interrupts and syncing before shutdowns.
You might notice that volume 1 has 29 bytes of usage, but 0 objects. This is because the object has been flagged as deleted, but is still in physical storage. The reason for this is because volumes are an append only file, meaning deleted and duplicate files are kept until compaction.
Log in to leave a comment
I added a details action that shows a preview of the object, and displayed the last updated date.
Right now previews only show for text/* content types, but I will add support for more, like a specific renderer for CSV, JSON, images and videos, with a toggle between raw and formatted.
Log in to leave a comment
You can now view a buckets objects in the web UI, including total size and count.
The object viewer is flat for now, meaning there are no virtual folders and everything appears at the top level. I will implement this later.
Next up is object actions.
Log in to leave a comment
I made the usage of volume servers available (through /usage) and displayed it in the Web UI, with a since meter.
This update took longer than expected, because styling meters is hell and I didn’t even end up using the built in ones. I was also having CORS issues :)
Log in to leave a comment
This fixes the issue where server where still marked as offline even after disconnecting. This was fixed by switching from a unary (one time) request to bidirectional stream, where disconnects can be handled.
I also:
Log in to leave a comment
Web UI now displays the accurate bucket/object count. I should probably lazy load this though
v0.10.0 Binaries
Commit 5396408a38 (Object count)
Commit 2526a33759 (Bucket count)
Log in to leave a comment
I implemented the basics of the Admin UI, using Golang, Templ, and TailwindCSS.
The numbers you see are made up, there is currently no integration with the master API. The colour also adapts to the status, if all servers are offline the colour is red and amber if some are offline.
Log in to leave a comment
This is a big feature that allows for scaling horizontally. Each volume server connects to the master to initialise and then starts a REST API which provides direct access to needle management.
One difference from normal S3 is that every request is now pre-signed, and you have to communicate directly with each volume server.
How horizontal scaling works

How a volume server connects to the master:

How a put (overwrite) works:

I would’ve fixed all these problems, but this devlog was getting long enough :)
Log in to leave a comment
One problem with using a single, append only, volume file is that deleted files and duplicates are not removed. Over time, this can waste a lot of storage. To fix this, I wrote a compaction tool, which reads the volume file, scanning each needle. If a needle is flagged as deleted, it is ignored and any previous needle with the same id is removed. I also only keep a copy of the latest needle, to keep the latest version and remove duplicates. Then, for each needle, the data is copied to a new volume file, then the old file is replaced with the new clean data.
I added an admin RPC to manually trigger compaction, and a utility to retrieve what proportion of the volume file is wasted.
Log in to leave a comment
One problem with my storage server is that each object was stored as a separate file on disk.This means each file retrieval is actually multiple disk operations, which can slow down retrieval
Heavily based on Facebook’s Haystack Paper, I wrote a storage system that uses one large file for many smaller objects, lowering the number of disk operations to read one object.
There are some (fixable) problems with this approach:
This work will allow me write volume servers, which manage multiple volumes, to allow for horizontal scaling and redundancy.
Here’s all the changes I made:
And the features I added:
Log in to leave a comment
Log in to leave a comment
I added an endpoint to get a buckets information. I also wrote a function to stringify data and handle errors, to remove duplicated code.
I’m planning on rewriting the API with gRPC, because honestly I cannot be bothered with manually stringifying and parsing data. Protobuf is also way more efficient than JSON. It also allows me to generate clients for many languages automatically
Log in to leave a comment
I moved the go files to cmd/server, and moved utility functions into separate files.
I also added environment variable configuration for setting the server port and database location.
For bucket name validation, I used regex to only allow characters a-z, 0-9, ‘.’ and ‘_’, with a max length of 32
Log in to leave a comment
In this devlog, I set up dependencies for Warehouse, and write a REST API with a single POST /buckets endpoint, which creates an entry in the SQLite Database
I used S3/SeaweedFS for my project Watchtower. But I discovered a few problems:
Log in to leave a comment