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 lists the
package's versions in S3 (one entry per version, not per
file) and serves the file from the highest matching
version. If that version does not contain the file, the
request returns 404. 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 |
compression.enabled |
Generate Brotli and gzip variants during upload | true |
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 |
| HEAD | /gimme/:package/*file |
— | Read file headers without its body |
| 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
Package names may contain only letters, digits,
., _, and -.
Versions must be full semver versions such as
1.0.0; prerelease and build metadata are accepted.
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
# Read the SHA-384 SRI token without downloading the body
curl -I http://localhost:8080/gimme/my-lib@1.0.0/my-lib.min.js
Newly uploaded files expose their Subresource Integrity
token through Gimme-Integrity. The package
listing can copy ready-to-use JavaScript and CSS snippets;
legacy objects remain available without the header.
The generated crossorigin="anonymous"
attribute requires an
Access-Control-Allow-Origin response header.
Gimme defaults to *, so this works out of the
box. Setting cors.allowed_origins to a fixed
list silently breaks SRI for cross-origin consumers whose
origins are not included.
Delete a package
curl -X DELETE http://localhost:8080/packages/my-lib@1.0.0 \
-H "Authorization: Bearer $TOKEN"
# → 204 No Content
Only that exact version is deleted. Versions must be full
semver; a partial or invalid version returns
400 Bad Request.
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.
Compressible files are stored with Brotli and gzip variants during upload.
Requests negotiate them with Accept-Encoding, and responses emit
Vary: Accept-Encoding so browsers and intermediate caches keep
each representation separate. Missing variants fall back to the identity object;
older packages must be re-uploaded to gain compressed variants.
| 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 |
Every file response also carries ETag and
Last-Modified. Gimme answers
304 Not Modified when
If-None-Match matches, or when
If-Modified-Since matches and
If-None-Match is absent. Revalidating a
partial version therefore transfers only headers when
the file has not changed, instead of downloading the
body again.
Level 2 — Internal Redis cache optional
When enabled, the resolved S3 path for partial versions is stored in Redis, skipping the package version listing (one entry per version, not per file) 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.