Your own CDN, anywhere you want.
Upload packages and serve static assets via a simple REST API, backed by any S3-compatible storage you already own or trust.
# 1. Get a token
curl -X POST https://cdn.example.com/tokens \
-u admin:password \
-H 'Content-Type: application/json' \
-d '{"name": "ci", "expirationDate": "2027-12-31"}'
# 2. Upload your library
curl -X POST https://cdn.example.com/packages \
-H "Authorization: Bearer $TOKEN" \
-F "file=@my-lib.zip" \
-F "name=my-lib" \
-F "version=2.1.0"
<!-- Pin to an exact version — cached 1 year -->
<link rel="stylesheet"
href="https://cdn.example.com/gimme/my-lib@2.1.0/my-lib.min.css">
<script type="module"
src="https://cdn.example.com/gimme/my-lib@2.1.0/my-lib.min.js">
</script>
<!-- Or use a partial alias — resolves to latest 2.1.x -->
<script src="https://cdn.example.com/gimme/my-lib@2.1/my-lib.min.js"></script>
-
Any S3 backend
AWS S3, Garage, R2, OVH, Scaleway — plug in the storage you already use or pay for.
-
Opaque token uploads
Cryptographically random opaque tokens protect write endpoints. Public asset serving needs no authentication — CORS configurable.
-
Semver aliases
pkg@1.0resolves to the latest1.0.xat request time. Pin withpkg@1.0.3for immutability. -
Two-level cache
HTTP
Cache-Controlheaders out of the box, plus optional Redis/Valkey for internal version resolution. -
Prometheus metrics
HTTP traffic, S3 latency, cache hits and package lifecycle — all exposed on
/metrics. -
Docker & Kubernetes ready
Pre-built image, Compose stacks with Garage + monitoring, Kubernetes manifests and Helm chart included.
Architecture
Gimme is a thin, stateless Go service. It sits between your clients and an S3-compatible object store, handling authentication, semver resolution, and streaming. A reverse proxy in front is recommended for production.
Package naming convention
Objects are stored in S3 as
<package>@<version>/<file>:
my-lib@2.1.0/my-lib.min.js
my-lib@2.1.0/my-lib.min.css
my-lib@2.1.1/my-lib.min.js ← next patch
Semver partial resolution
A request for my-lib@2.1/file.js triggers a
ListObjects call in S3, sorts all matching
versions, and redirects to the highest patch. With Redis
enabled, the resolved path is cached — S3 is only hit on
a cache miss.
Quickstart
Garage is a lightweight open-source S3 store. The stack provisions itself — no manual bucket or key setup needed.
git clone https://github.com/ziggornif/gimme
cd gimme/examples/deployment/docker-compose/with-garage
docker compose up -d
Gimme starts at http://localhost:8080.
Prometheus and Grafana are included. An optional
Valkey cache is pre-configured — just enable it in
gimme.yml.
init-garage service creates
the bucket and writes the configuration
automatically on first run.
Gimme works with any S3-compatible provider — AWS
S3, OVH Object Storage, Scaleway, Clever Cloud
Cellar, Cloudflare R2 and more. Create a bucket and
fill in gimme.yml:
admin:
user: gimmeadmin
password: change-me
port: 8080
secret: your-secret-at-least-32-chars-long
s3:
url: s3.amazonaws.com # or s3.fr-par.scw.cloud, etc.
key: your-access-key
secret: your-secret-key
bucketName: gimme
location: eu-west-1
ssl: true
docker run -p 8080:8080 \
-v "$(pwd)/gimme.yml:/config/gimme.yml" \
ziggornif/gimme:latest
Requires Go 1.26+ and a running S3-compatible backend.
git clone https://github.com/ziggornif/gimme
cd gimme
cp gimme.example.yml gimme.yml
# Edit gimme.yml with your S3 credentials
make build && ./gimme
For live reload during development (requires air):
make watch
Your first upload in 3 steps
Create an access token
curl -s -X POST http://localhost:8080/tokens \
-u gimmeadmin:gimmeadmin \
-H 'Content-Type: application/json' \
-d '{"name": "my-token", "expirationDate": "2027-12-31"}'
expirationDate,
the token expires in 90 days.
Upload a ZIP package
curl -X POST http://localhost:8080/packages \
-H "Authorization: Bearer $TOKEN" \
-F "file=@my-lib.zip" \
-F "name=my-lib" \
-F "version=1.0.0"
# → 201 Created
Use your assets
<script src="http://localhost:8080/gimme/my-lib@1.0.0/my-lib.min.js"></script>
/gimme/my-lib@1.0.0/<file>
with full CORS support.
Configuration
Config is loaded from gimme.yml (local
directory or /config/gimme.yml inside
Docker). Every value can be overridden by an environment
variable — Viper maps them automatically with the
GIMME_ prefix (e.g.
GIMME_S3_URL, GIMME_ADMIN_USER).
admin:
user: gimmeadmin
password: gimmeadmin
port: 8080
secret: your-secret-at-least-32-chars-long
s3:
url: your.s3.endpoint
key: your-access-key
secret: your-secret-key
bucketName: gimme
location: garage # region as defined by your backend
ssl: false # set false for local Garage
metrics: true # expose /metrics (Prometheus)
# Token store: "file" (default), "redis", or "postgres"
# tokenStore:
# mode: file # "file" | "redis" | "postgres"
# pg_url: postgres://gimme:password@localhost:5432/gimme?sslmode=disable
# Redis — required when tokenStore.mode is "redis" or cache.enabled is true
# redis_url: redis://localhost:6379
# cache:
# enabled: false # optional version-resolution cache
# type: redis
# ttl: 3600
# file_path: /tmp/gimme-tokens.enc # used only when tokenStore.mode is "file"
| Key | Description | Default |
|---|---|---|
secret |
Token signing secret (min 32 bytes) | required |
admin.user |
Admin username (Basic Auth) | required if basic |
admin.password |
Admin password (Basic Auth) | required if basic |
port |
HTTP server port | 8080 |
s3.url |
S3 / Garage endpoint URL | required |
s3.key |
S3 access key | required |
s3.secret |
S3 secret key | required |
s3.bucketName |
S3 bucket name | gimme |
s3.location |
S3 region / Garage zone | required |
s3.ssl |
Enable TLS for S3 connection | true |
metrics |
Enable /metrics Prometheus
endpoint
|
true |
cache.enabled |
Enable internal Redis cache | false |
cache.type |
Cache backend type | redis |
cache.ttl |
Cache entry TTL in seconds | 3600 |
redis_url |
Redis / Valkey connection URL. Required
when tokenStore.mode is
redis or
cache.enabled is
true.
|
"" |
cache.file_path |
Path to the encrypted token file used
when tokenStore.mode is
file
|
/tmp/gimme-tokens.enc |
tokenStore.mode |
Token persistence backend.
file stores tokens in an
encrypted local file (no external
dependency). redis stores
tokens in Redis — requires
redis_url.
postgres stores tokens in
PostgreSQL — requires
tokenStore.pg_url.
|
file |
tokenStore.pg_url |
PostgreSQL connection URL. Required when
tokenStore.mode is
postgres.
|
"" |
auth.mode |
Admin auth mode (basic or
oidc)
|
basic |
auth.oidc.issuer |
OIDC issuer URL | required if oidc |
auth.oidc.client_id |
OIDC client ID | required if oidc |
auth.oidc.client_secret
|
OIDC client secret | optional |
auth.oidc.redirect_url |
OIDC redirect URI | required if oidc |
auth.oidc.secure_cookies
|
Use Secure flag on session
cookies (disable only for local HTTP
dev)
|
true |
cors.allowed_origins |
List of allowed CORS origins. Defaults
to all origins (*) if
empty.
|
[] (all origins) |
tokenStore.mode: file),
tokens are persisted to an encrypted local file —
no external dependency needed. Set
tokenStore.mode: redis and provide
redis_url to share tokens across
multiple instances. Set
tokenStore.mode: postgres and provide
tokenStore.pg_url for deployments
that already have a PostgreSQL database.
secret must be
at least 32 bytes long. Gimme
will refuse to start with a shorter value.
Deployment
-
Docker Compose + Garage
Zero-setup stack with self-hosted Garage storage, Valkey cache and Prometheus/Grafana monitoring bundled in.
View on GitHub -
Docker Compose + managed S3
Connect to any managed provider (AWS S3, OVH, Scaleway, Cellar, R2). Monitoring included.
View on GitHub -
Kubernetes
Namespace, Deployment, Service and Ingress manifests. Liveness and readiness probes pre-configured.
View on GitHub -
Helm chart
Production-ready Helm chart published on GHCR. Secrets, Ingress, HPA and Redis cache configurable via
View on GitHubvalues.yaml. -
systemd
Run as a Linux system service with automatic restarts and environment file support.
View on GitHub
Helm chart
The Helm chart is published on GHCR (OCI) and provides a
production-ready deployment with credentials in a
dedicated Kubernetes Secret, optional
Ingress with TLS, HPA and Redis cache.
Install
helm install gimme oci://ghcr.io/ziggornif/charts/gimme \
--namespace gimme \
--create-namespace \
--set credentials.secret=<signing-secret-min-32-chars> \
--set credentials.admin.user=<admin-user> \
--set credentials.admin.password=<admin-password> \
--set credentials.s3.key=<s3-key> \
--set credentials.s3.secret=<s3-secret> \
--set s3.url=<s3-endpoint-url>
Or with a values.yaml file:
helm install gimme oci://ghcr.io/ziggornif/charts/gimme \
--namespace gimme --create-namespace \
-f my-values.yaml
Minimal values.yaml
credentials:
secret: "your-secret-at-least-32-chars-long"
admin:
user: "admin"
password: "strongpassword"
s3:
key: "your-s3-access-key"
secret: "your-s3-secret-key"
s3:
url: "https://s3.example.com"
bucketName: gimme
location: eu-west-1
ssl: true
Enable Ingress with TLS
ingress:
enabled: true
className: nginx
host: cdn.example.com
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
tls:
enabled: true
secretName: gimme-tls
Enable Redis cache
cache:
enabled: true
type: redis
ttl: 3600
redisUrl: redis://my-redis-service:6379
Enable HPA (auto-scaling)
hpa:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
Upgrade / Uninstall
# Upgrade
helm upgrade gimme oci://ghcr.io/ziggornif/charts/gimme \
--namespace gimme -f my-values.yaml
# Uninstall
helm uninstall gimme --namespace gimme
Single Docker container
docker run -p 8080:8080 \
-v "$(pwd)/gimme.yml:/config/gimme.yml" \
ziggornif/gimme:latest
Health probes
| Endpoint | Type | Condition |
|---|---|---|
GET /healthz |
Liveness | Process is alive |
GET /readyz |
Readiness | S3 bucket is reachable |
API Reference
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | / |
— | HTML homepage (ReDoc API docs) |
| GET | /admin |
Admin auth | Admin UI (token management) |
| POST | /tokens |
Admin auth | Create an opaque access token |
| DELETE | /tokens/:id |
Admin auth | Revoke an access token |
| POST | /packages |
Bearer token | Upload a ZIP package |
| DELETE | /packages/:package |
Bearer token |
Delete a package
(name@version)
|
| GET | /gimme/:package |
— | List files in a package (HTML) |
| GET | /gimme/:package/*file |
— | Serve a static file |
| GET | /metrics |
— | Prometheus / OpenMetrics endpoint |
| GET | /docs |
— | Interactive Swagger / ReDoc docs |
| GET | /healthz |
— | Liveness probe — process alive |
| GET | /readyz |
— | Readiness probe — S3 bucket reachable |
Create a token
gim_<hex>). JWT
tokens from previous versions are invalid —
regenerate via /admin or
POST /tokens. The raw token is returned
once and never stored again.
curl -s -X POST http://localhost:8080/tokens \
-u gimmeadmin:gimmeadmin \
-H 'Content-Type: application/json' \
-d '{"name": "my-token", "expirationDate": "2027-12-31"}'
{"id": "550e8400-...", "name": "my-token", "token": "gim_4a7b9c2d1e3f...", "createdAt": "...", "expiresAt": "..."}
Upload a package
curl -X POST http://localhost:8080/packages \
-H "Authorization: Bearer $TOKEN" \
-F "file=@my-lib.zip" \
-F "name=my-lib" \
-F "version=1.0.0"
# → 201 Created
Serve a file
# Exact version — cached for 1 year
curl http://localhost:8080/gimme/my-lib@1.0.0/my-lib.min.js
# Partial alias — resolves to latest 1.0.x, cached 5 minutes
curl http://localhost:8080/gimme/my-lib@1.0/my-lib.min.js
Delete a package
curl -X DELETE http://localhost:8080/packages/my-lib@1.0.0 \
-H "Authorization: Bearer $TOKEN"
# → 204 No Content
Caching
Two independent caching levels that compose naturally:
Level 1 — HTTP Cache-Control zero dependency
Gimme emits Cache-Control headers on every
response. Any HTTP cache honours them automatically.
| Version type | Example | Cache-Control |
|---|---|---|
| Pinned (3-part semver) | pkg@1.0.0 |
public, max-age=31536000,
immutable
|
| Partial alias |
pkg@1.0 or
pkg@1
|
public, max-age=300 |
| Not found (404) | any | no-store |
Level 2 — Internal Redis cache optional
When enabled, the resolved S3 path for partial versions
is stored in Redis, skipping ListObjects on
repeat requests. File bodies are always streamed
directly from S3 — only the path lookup is cached.
redis_url: redis://localhost:6379
cache:
enabled: true
type: redis
ttl: 3600
Cache entries are invalidated when a package is deleted
via DELETE /packages/:package.
Partial-version entries expire naturally via TTL.
Monitoring
Every instance exposes a /metrics endpoint
in
OpenMetrics
format. In addition to standard Go runtime metrics,
gimme tracks:
| Metric | Type | Description |
|---|---|---|
gimme_http_requests_total
|
Counter | HTTP requests by route, method and status code |
gimme_s3_operation_duration_seconds
|
Histogram | S3 operation latency by operation name |
gimme_cache_hits_total |
Counter | Cache hits on partial-version resolution |
gimme_cache_misses_total
|
Counter | Cache misses on partial-version resolution |
gimme_packages_uploaded_total
|
Counter | Packages successfully uploaded |
gimme_packages_deleted_total
|
Counter | Packages successfully deleted |
A pre-configured Prometheus + Grafana stack ships with
both Docker Compose examples. Once running: Prometheus
at :9090, Grafana at :3000.