Open source v1 · Go · S3-compatible

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"
  • 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.0 resolves to the latest 1.0.x at request time. Pin with pkg@1.0.3 for immutability.

  • Two-level cache

    HTTP Cache-Control headers 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.

Browser / CLI
Nginx / CDN optional
gimme :8080
Redis / Valkey optional (tokens + cache)
S3 storage

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.

The init-garage service creates the bucket and writes the configuration automatically on first run.

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"}'
Without 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>
Assets are served from /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)
Token store mode. By default (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 values.yaml.

    View on GitHub
  • 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

Breaking change: Tokens are now opaque strings (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:

Browser
Level 1 Nginx / CDN HTTP Cache-Control
Level 2 gimme + Redis version resolution
S3

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.