stashy
Health Pass
- License — License: Apache-2.0
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 15 GitHub stars
Code Pass
- Code scan — Scanned 9 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
Self-hosted file storage for AI agents
Stashy
Self-hosted file storage built for AI agents. Connect your agent over
MCP or give it an API key — it uploads, manages, and shares files,
and you stay in control.
- Agent-native — built-in MCP server, plus REST, gRPC, gRPC-Web, and Connect on one endpoint
- Any storage backend — local disk, Amazon S3, Cloudflare R2, MinIO, or Google Cloud Storage
- Private, team, or public — choose who sees each file, with clean shareable URLs
Install
brew install stashysh/tap/stashy
Or download a binary from Releases.
Quick start
export SESSION_SECRET="your-secret-here"
export GOOGLE_CLIENT_ID="your-client-id"
export GOOGLE_CLIENT_SECRET="your-client-secret"
just run
# or: go run ./cmd/stashy serve --migrate
Visit http://localhost:8080 to sign in and generate API keys.
Uses SQLite by default, with PostgreSQL available when you need a separate database.
Build
just build # build binary
just generate # regenerate proto code
just tidy # go mod tidy
just clean # remove build artifacts
Docker
cp .env.example .env # fill in your secrets
docker compose up
Environment variables
| Variable | Description | Default |
|---|---|---|
PORT |
Server listen port | 8080 |
HOSTNAME |
Public base URL | http://localhost:$PORT |
DATABASE_URL |
Database connection string (see below) | file:stashy.db |
STORAGE_BACKEND |
Storage backend: memory, local, gcs, or s3 |
memory |
LOCAL_STORAGE_DIR |
Directory for local file storage | ./storage |
GCS_BUCKET |
GCS bucket name (required when STORAGE_BACKEND=gcs) |
— |
S3_BUCKET |
S3 bucket name (required when STORAGE_BACKEND=s3) |
— |
SESSION_SECRET |
HMAC key for signing session cookies | required |
GOOGLE_CLIENT_ID |
Google OAuth 2.0 client ID | required |
GOOGLE_CLIENT_SECRET |
Google OAuth 2.0 client secret | required |
ALLOWED_DOMAINS |
Comma-separated list of allowed email domains | — (all allowed) |
Database
Driver is auto-detected from the DSN:
| DSN | Database |
|---|---|
file:stashy.db |
SQLite (default) |
postgres://user:pass@host/db |
PostgreSQL |
Migrations are managed by goose. Run them explicitly:
stashy migrate
# or: stashy serve --migrate
CLI
stashy serve [--migrate] # start the server (default), optionally run migrations first
stashy migrate # run database migrations and exit
stashy version # print version
stashy help # print usage
Authentication
Web UI (Google OAuth)
- Visit
/— sign in with Google - After login, the dashboard lets you generate and manage API keys
API (Bearer token)
All /v1/* API endpoints require a Bearer token:
curl -H "Authorization: Bearer <api-key>" \
-X POST http://localhost:8080/v1/files \
-H "Content-Type: image/png" \
--data-binary @photo.png
Each file's visibility controls who can open its link (/{id}): private (only you), internal (any signed-in user; the default), or public (anyone).
Set ALLOWED_DOMAINS to restrict login to specific email domains.
Protocols
A single endpoint serves all protocols via vanguard-go transcoding:
| Protocol | Transport |
|---|---|
| gRPC | HTTP/2 (h2c) |
| gRPC-Web | HTTP/1.1 or HTTP/2 |
| Connect | HTTP/1.1 or HTTP/2 |
| REST | HTTP/1.1 or HTTP/2 |
API
The API only works with your own files: every endpoint below acts on files created with your API keys.
Create a file
curl -H "Authorization: Bearer <api-key>" \
-X POST "http://localhost:8080/v1/files?slug=my-photo&name=photo.png" \
-H "Content-Type: image/png" \
--data-binary @photo.png
All query parameters are optional:
slug: the human-readable end of the file's URL (/{id}/my-photo).name: the original filename, shown in the file list and used as the
filename when the file is downloaded.visibility: who can open the file's link:private,internal(default),
orpublic.
Creating and updating a file return its metadata:
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"slug": "my-photo",
"url": "http://localhost:8080/V1StGXR8_Z5jdHi6B-myT/my-photo",
"name": "photo.png",
"content_type": "image/png",
"size": "48213",
"checksum": "mnG7TA==",
"visibility": "internal",
"created_at": "2026-10-02T12:00:00Z",
"updated_at": "2026-10-02T12:00:00Z"
}
size is a string, as protobuf JSON encodes 64-bit integers. checksum is the
CRC32C (Castagnoli) of the content: the 4-byte big-endian value, base64-encoded,
as GCS and S3 report it. It's empty for files uploaded before checksums were
recorded.
List files
curl -H "Authorization: Bearer <api-key>" \
"http://localhost:8080/v1/files?limit=50"
Returns a JSON array of your files, newest first. To get the next page, pass
the last file's id as after; a page shorter than limit is the last.
Get a file
curl -H "Authorization: Bearer <api-key>" \
http://localhost:8080/v1/files/{id}
Returns the file's metadata.
Get a file's content
curl -H "Authorization: Bearer <api-key>" \
-o photo.png http://localhost:8080/v1/files/{id}/content
Returns the file's bytes. Supports Range requests. The ETag header is the
quoted checksum; send it back as If-None-Match to get 304 Not Modified when
the content hasn't changed.
Update a file
curl -H "Authorization: Bearer <api-key>" \
-X PATCH http://localhost:8080/v1/files/{id} \
-H "Content-Type: application/json" \
-d '{"slug": "my-photo", "name": "My photo.png", "visibility": "public"}'
Updates a file's slug (the human-readable end of its URL), name (its
original filename), and visibility (private, internal, or public).
Omitted fields are left unchanged; send "" to clear a slug or name.
Update a file's content
curl -H "Authorization: Bearer <api-key>" \
-X PUT http://localhost:8080/v1/files/{id}/content \
-H "Content-Type: image/png" \
--data-binary @photo-v2.png
Replaces the whole content; the file's ID and URL stay the same. To avoid
overwriting someone else's change, send the ETag you last saw as If-Match:
the update fails with 412 Precondition Failed if the content has changed
since.
Delete a file
curl -H "Authorization: Bearer <api-key>" \
-X DELETE http://localhost:8080/v1/files/{id}
MCP
Stashy serves a Model Context Protocol server at /mcp, so AI assistants can work with your files. It uses the same API keys as the REST API.
claude mcp add --transport http stashy http://localhost:8080/mcp \
--header "Authorization: Bearer <api-key>"
Tools: list_files, get_file, update_file, delete_file. They work with file metadata; to create files or get and update their content, use the REST API.
File access
curl http://localhost:8080/{id}
Public files open for anyone. Internal files require signing in, and private
files open only for their owner; anyone else signed in gets a 404.
Ideal for CDN or subdomain mapping (e.g., cdn.example.com/{id}).
If a file has a slug, its canonical URL is /{id}/{slug} and /{id} redirects
there. A stale or wrong slug also redirects to the current canonical URL, so
links shared before a rename keep working.
Storage backends
- memory — in-memory, ephemeral; good for development
- local — files on disk at
LOCAL_STORAGE_DIR - gcs — Google Cloud Storage
- s3 — Amazon S3 or S3-compatible storage (MinIO, R2, etc.)
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found