Quickstart — serve a real endpoint in 15 minutes
Pull the published image, run it against a ready-made example project (real sample data included), call an endpoint, read the generated docs, and watch a bad request fail safely — about fifteen minutes, no cloud required.
New to Offloader? Read What Offloader is, in plain language first (5 minutes) — it defines the words used below.
0. Get the example and the image
You serve from a project — an offloader.yml plus its datasets, endpoints, keys, and a
snapshot. You can't just start the bare container; it needs a config and data to serve. The
repo ships a complete, working project (with sample data) in examples/customer-analytics/, so
clone it and pull the image:
git clone https://github.com/andrewdryga/offloader.git
cd offloader
docker pull ghcr.io/andrewdryga/offloader:edge # rolling build of main; or pin a release tag
Nothing is baked into the image — the example project is mounted from your clone at run time, which is exactly how you'll later mount your own project.
Prefer to build the image yourself?
docker build -t offloader:dev -f server/Dockerfile server
Then use offloader:dev in place of the ghcr.io/… image in every command below.
1. Run the container
Mount the example project read-only; secrets come from env vars; the admin port binds to loopback:
docker run --rm \
-e OFFLOADER_CONFIG=/etc/offloader/offloader.yml \
-e OFFLOADER_SECRET_KEY_BASE="$(openssl rand -base64 48)" \
-e OFFLOADER_ADMIN_TOKEN="$(openssl rand -base64 24)" \
-p 4000:4000 \
-p 127.0.0.1:4001:4001 \
-v "$PWD/examples/customer-analytics:/etc/offloader:ro" \
-v offloader-cache:/var/lib/offloader/cache \
ghcr.io/andrewdryga/offloader:edge
Wait until the admin readiness probe returns 200 (it stays 503 until the snapshot is materialized):
curl -fsS http://127.0.0.1:4001/ready
# {"status":"ok","ready":true}
2. Call your first endpoint
The example ships a demo key offl_demo_acme_key bound to tenant_acme:
curl -H "Authorization: Bearer offl_demo_acme_key" \
"http://localhost:4000/v1/endpoints/customer_usage_summary?from=2026-05-30&to=2026-06-01"
{
"data": [ { "account_id": "acct_zephyr", "active_users_total": 244, "api_calls_total": 56839, "storage_gb_avg": 34.300000000000004 }, "…" ],
"meta": {
"request_id": "…", "endpoint": "customer_usage_summary", "version": 1,
"snapshot_id": "2026-06-01T00:00:00Z_r0007", "generated_at": "2026-07-01T18:21:10Z", "row_count": 2,
"serving_mode": "local_table", "cache": "miss",
"freshness": { "watermark": "2026-06-01T00:00:00Z", "age_seconds": 2658070, "max_staleness_minutes": 120, "stale": true }
}
}
Every response carries snapshot_id and freshness — you always know exactly which snapshot
you're reading. (stale:true here just means the demo data is older than its 120-minute
tolerance.)
3. Read the generated docs
The endpoint catalog and OpenAPI spec are generated from your endpoint definitions and served on the admin port (never the API port):
curl -s http://127.0.0.1:4001/docs | jq '.endpoints[].name'
curl -s http://127.0.0.1:4001/openapi.json | jq '.paths | keys'
A product engineer integrates from these — params, defaults, limits, response shape, error families, and curl/TypeScript/Python snippets are all there.
4. See a failure fail safely
Bad input is rejected with a named error family — never a leak, never a 500:
# an unknown/undeclared param (e.g. smuggling a tenant) -> 422 invalid_param
curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer offl_demo_acme_key" \
"http://localhost:4000/v1/endpoints/customer_usage_summary?from=2026-05-30&to=2026-06-01&tenant_id=tenant_globex"
# 422
The failure lab has the full set (bad manifest, stale dataset, forbidden tenant, …) and the exact error each returns.
5. Start your own project
The bundled example proves the path; to serve your data, scaffold a fresh project with the
offloader CLI (an optional helper — not part of the container). Install or update it:
curl -fsSL https://offloader.dryga.com/install.sh | sh
No installer? build from source (needs Go)
cd tools && go build -o offloader . # then ./offloader <command>
Scaffold a starter project and check it exactly the way the container does:
# a complete, valid, fully-commented project (one dataset + endpoint + demo key)
offloader init --out my-project
# validate the whole tree before you run anything
offloader validate --config my-project/offloader.yml
The two files you edit are small and declarative — no SQL. Abridged from what init just
generated:
# my-project/datasets/events.yml — the table + the columns you serve
id: events
manifest: data/events/manifest.json # point this at your snapshot (Parquet + a manifest.json)
tenant_column: tenant_id
schema:
- { name: event_date, type: DATE }
- { name: tenant_id, type: VARCHAR }
- { name: account_id, type: VARCHAR }
- { name: event_count, type: BIGINT }
# my-project/endpoints/events_by_day.yml — the REST contract over that dataset
name: events_by_day
version: 1
dataset: events
tenant: { column: tenant_id }
params:
- { name: from, type: date, required: true }
- { name: to, type: date, required: true }
query:
group_by: [account_id]
select:
- { as: account_id, column: account_id }
- { as: event_count_total, column: event_count, agg: sum }
filters:
- { column: event_date, op: gte, param: from }
- { column: event_date, op: lte, param: to }
columns: [account_id, event_count_total]
Point manifest: at a snapshot of your data, then serve it locally in one command:
offloader serve my-project/ # validates the config, pulls the image, runs it against your project
That's the shortcut for the full docker run in step 1 — handy for a POC. The
config guide walks through every file; the
config reference lists every field.
Let an AI agent migrate your existing endpoints
Already have analytics endpoints (raw SQL, an ORM, a hand-rolled API)? Point a coding agent at both your code and this repo, and paste:
You are migrating our existing analytics endpoints to Offloader. Read Offloader's
docs/config-reference.mdand theexamples/customer-analytics/project for the exact schema. For each of our current endpoints: (1) write adatasets/<id>.ymlwhoseschemamatches the columns it returns; (2) write anendpoints/<name>.ymlreproducing its params, filters, aggregation, ordering, and limits using only Offloader's declarativequery:— no raw SQL; (3) preserve tenant isolation by setting the dataset'stenant_columnand the endpoint'stenant:. Then runoffloader validate --config my-project/offloader.ymlandoffloader endpoint testagainst a running instance, and fix every reported error until both pass. Finally, list any endpoint you could not express and exactly why.
It produces a project you own and can diff — review it before serving.
6. Next
- Config guide — publish your own datasets and endpoints, and load
config from a
gs:///s3://bucket (fully stateless, with zero-downtime hot-reload). - Operator guide · Deployment — run it in production: size, upgrade, roll back, diagnose.
- Hand the generated endpoint docs (step 3) to a product engineer to integrate against.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| Container exits immediately | Missing OFFLOADER_SECRET_KEY_BASE, or OFFLOADER_CONFIG not mounted. |
/ready stays 503 |
The snapshot hasn't materialized — check docker logs and admin /diagnostics. |
| Endpoint returns 401 | Missing/invalid/revoked key. Mint one with offloader keys create. |
| Endpoint returns 404 | Unknown endpoint, or the key isn't granted it (indistinguishable, on purpose). |
| Endpoint returns 422 | A param is missing/mistyped/out of range, or an undeclared param was sent. |