gatething Get in touch

One bucket · one gate · a key per app

One gate in front of the bucket.

Every app that takes uploads writes its own bucket policy, size limits, type checks and sanitising, and nobody decides when anything gets deleted. gatething is one gate: a key per app, the rules in one file, and a sanitize endpoint anything can call.

Act I

Every app, its own bucket rules.

A shop wants customers to upload a photo. Storing a file safely turns out to be five decisions, and every app makes them again.

01 · A bucket

A bucket, and its policies.

Create it, write a CORS policy in JSON, a lifecycle rule, and an access key the app keeps in its environment. For this app only.

storage-provider.example/buckets/shop-uploads
shop-uploads Object storage
CORS [{"AllowedOrigins": ["https://shop.example"]}]
Lifecycle none
Save policies
CORS policy saved.
Access key created for shop-backend.

02 · The checks

Then the checks, in the app.

A size limit, allowed types, a filename that won't break anything, and metadata stripping you wrote yourself. Is it right? Probably.

upload.ts lines 1–18
const MAX = 10 * 1024 * 1024; // the other app says 25
const TYPES = ["image/png", "image/jpeg"]; // the other app allows svg
export async function upload(req: Request) {
const file = (await req.formData()).get("file") as File;
if (file.size > MAX) return new Response("too big", { status: 413 });
if (!TYPES.includes(file.type)) return new Response("no", { status: 415 });
const name = file.name.replace(/[^a-z0-9.\-_]/gi, "_"); // good enough?
const clean = await stripExif(await file.arrayBuffer()); // hand-rolled
await s3.putObject({
Bucket: process.env.BUCKET,
Key: `uploads/${crypto.randomUUID()}-${name}`,
Body: clean,
});
// when does this get deleted? nobody decided.
}

03 · The next app

Then again, slightly different.

The booking app copied the handler and changed it. Now one app allows SVG, and an SVG can carry a script.

~/src
$ diff shop/upload.ts booking/upload.ts
< const MAX = 10 * 1024 * 1024;
> const MAX = 25 * 1024 * 1024;
< const TYPES = ["image/png", "image/jpeg"];
> const TYPES = ["image/png", "image/jpeg", "image/svg+xml"];

04 · What's in there?

And the bucket only grows.

Nobody decided how long a file lives, so every file lives forever, and nobody knows which ones are safe to delete.

~
$ storage ls s3://shop-uploads --recursive --summarize | tail -2
Total Objects: 48,210
Total Size: 212.4 GiB
# oldest: 2021. still referenced? no idea.

That was two apps.

policies by hand
2
rules copied into code
3
holes
2
files with an expiry
0

Here's the same upload, through the gate.

Act II

The gatething way.

One bucket, and gatething in front of it. Apps never hold a bucket key; they hold a gate key, and the gate applies their rules.

01 · The rules, once

Every app's rules in one file.

How big, which types, how long it lives and how it's cleaned, per app. Defaults cover what nobody should allow, like SVG.

gate.toml lines 1–18
# one bucket, one gate, a rule per app. Apps never hold bucket keys.
[apps.shop]
prefix = "uploads/shop/"
max_size = "10 MB"
types = ["image/png", "image/jpeg", "image/webp"]
lifetime = "90 days" # then it's deleted, on schedule
sanitize = ["strip-metadata", "normalise-names"]
[apps.booking]
prefix = "uploads/booking/"
max_size = "25 MB"
types = ["application/pdf"]
lifetime = "7 years" # receipts: the law decides this one
sanitize = ["strip-metadata", "reject-scripts"]
[defaults]
svg = "reject" # an svg is a script with a picture

02 · A key per app

A key that can only reach its own files.

The shop's key writes to the shop's prefix and nowhere else. Revoking it doesn't touch any other app.

~
$ gatething keys create shop
gk_live_7e2a… · uploads/shop/ · read, write
✓ the only key shop needs for files

03 · Upload through the gate

The app sends the file. The gate does the rest.

Size, type, sanitising and an expiry date are applied on the way in. The app gets back an id and what was cleaned.

~/shop
curl -X PUT https://gatething.com/api/v1/files \
-H "Authorization: Bearer $GATE_KEY" \
-F file=@receipt-photo.jpg
{
"id": "uploads/shop/01J8Z6-receipt-photo.jpg",
"size": "1.1 MB",
"sanitized": ["removed GPS location", "removed camera serial"],
"expires": "2026-12-27"
}

04 · Sanitize, without storing

And sanitising on its own.

Anything can send a file or some input to the sanitize endpoint and get the clean version back, stored or not. The rules live in one service instead of in every app.

~
curl -X POST https://gatething.com/api/v1/sanitize \
-H "Authorization: Bearer $GATE_KEY" \
-F file=@avatar.svg
415 · svg rejected: scripts aren't sanitised, they're refused
$ curl -X POST gatething.com/api/v1/sanitize -F file=@photo.jpg -o clean.jpg
200 · clean.jpg · removed GPS location, camera serial

Act III

What the gate decides.

Storing a file is a handful of decisions. gatething makes them once, writes them down, and applies them to everything that stores a file.

  1. 1

    What gets in

    A size limit and a list of types per app, and defaults for what nothing should accept. The check happens at the gate, not in every handler.

  2. 2

    How it's cleaned

    Metadata stripped, names normalised, scripts refused. The same policies behind uploads are open on their own at /api/v1/sanitize.

  3. 3

    How long it lives

    Every file gets an expiry when it arrives, and the gate deletes it on schedule. The bucket is maintained, not just filled.

File uploads, both ways
Measure Per app gatething
Bucket keys in apps one per app none
Size and type limits copied, then drifted one file
Sanitising hand-rolled per app one service, one endpoint
Deleting old files nobody decided an expiry on every file

Storing files in more than one app?

Tell me what your apps take in. gatething is where every project here will put its files.

Get in touch