tiinyapp.farm

API reference

Every route the farm serves, how requests are authenticated, what the limits are, and what an error looks like.

The farm runs as a Cloudflare Worker in front of a static site. Everything under /api/, plus the account, maker and app file routes, is handled by the Worker; everything else is a static file.

The base is https://tiinyapp.farm. Responses are JSON with Cache-Control: no-store unless stated otherwise.

Authentication

There are two credentials and they are not interchangeable.

A session cookie. Signing in with an email code or with GitHub sets __Host-farm, which is HttpOnly, Secure, SameSite=Lax and lasts 30 days. The site uses it. Signing in again replaces the previous session rather than adding one.

A bearer token. Create one on Your apps, copy it once, and send it as Authorization: Bearer farm_.... A token is farm_ followed by 40 hexadecimal characters. Only four routes accept one:

MethodPath
POST/api/seeds
PUT/api/seeds/<id>
POST/api/media
GET/api/seeds/mine

Everywhere else a token is ignored and the cookie decides. Creating and revoking tokens needs the cookie, so a token cannot mint another token. Tokens are stored as SHA-256 hashes, never in the clear, and each use records the time it was last used.

The origin rule. Any POST, PUT, PATCH or DELETE that does not carry a bearer token on one of those four routes must send Origin: https://tiinyapp.farm, or it is refused with 403 and Please submit this form from tiinyapp.farm. This is what stops another site posting with a visitor's cookie.

Sign in

MethodPathBodyAnswers
POST/api/auth/start{"email": "you@example.com"}{"sent": true} and emails a six digit code
POST/api/auth/verify{"email": "...", "code": "123456"}{"user": {...}} and sets the session cookie
GET/api/auth/githubnone302 to GitHub's authorize page
GET/api/auth/github/callbacknone302 to /submit/ and sets the session cookie
POST/api/auth/logoutnone{"signedOut": true} and clears the cookie
GET/api/menone{"user": {...}}, or {"user": null} when signed out

A code lasts ten minutes and dies after five wrong attempts. Three codes per hour per address. If you are already signed in when you start either flow, finishing it links that method to your existing account rather than making a second one; finishing it from a different account is refused with 403.

Email sign-in answers 503 when the mail credential is not configured, and GitHub sign-in answers 503 when its client id and secret are not.

Prove you own a Tiiny

MethodPathBodyAnswers
POST/api/tiinyverse/link{"profileUrl": "https://www.tiinyverse.com/users/<uuid>"}The code to put in your bio, when it expires, and what to do
POST/api/tiinyverse/verifynone{"tiinyverse": {"profileUrl": "...", "name": "...", "verifiedAt": "..."}}
GET/api/owners?profile=<url>none{"verified": true, "name": "..."}

The code lasts 24 hours. Verify fetches your public profile, looks for the exact code as a whole word, and reads your display name from the page heading. A profile already claimed by another account answers 409, an unreadable or private profile answers 422, and a missing code answers 422 with an instruction to add it and try again.

/api/owners is the public lookup the pull request checks use to confirm a manifest's owner. It needs no credential and is the one route that skips the Worker's request queue, so it stays responsive during an upload.

Your account

MethodPathBodyAnswers
POST/api/tokens{"name": "Laptop"}201 with {"token": "farm_...", "id", "name", "prefix", "createdAt", "lastUsedAt"}
GET/api/tokensnone{"tokens": [{"id", "name", "prefix", "createdAt", "lastUsedAt"}]}
DELETE/api/tokens/<id>none{"revoked": true}
POST/api/maker{"bio": "...", "links": {...}, "avatarKey": null}{"user": {...}}
PUT/api/maker/visibility{"public": true}{"public": true}

The full token is in the creation response and nowhere else afterwards; only its prefix is kept for display. Five live tokens per account. Creating one needs a verified Tiiny profile.

A bio is at most 600 characters. Maker links are github, website and youtube only, and each must be an HTTPS URL. An avatar key must be an image you uploaded to your own account. Turning your profile off leaves your maker page reachable only to signed-in visitors.

Images

MethodPathBodyAnswers
POST/api/mediaThe image bytes, with an image content type201 with {"key": "media/...", "url": "https://tiinyapp.farm/media/..."}
DELETE/api/media/<key>none{"deleted": true}
GET, HEAD/media/<key>noneThe image

Up to 2 MiB. The type is read from the file's own first bytes, not its name, and only PNG, JPEG and WebP are accepted; anything else answers 415. Uploading needs a verified Tiiny profile. You can only delete your own images. Served images are immutable and cached for a year.

curl --fail-with-body \
  -H "Authorization: Bearer $FARM_TOKEN" \
  -H "Content-Type: image/png" \
  --data-binary @art/icon.png \
  https://tiinyapp.farm/api/media

Apps

MethodPathSendsAnswers
POST/api/seedsmultipart/form-data201 with {"id", "prUrl", "statusUrl"}
PUT/api/seeds/<id>multipart/form-dataThe same, for an app you already own
GET/api/seeds/minenone{"seeds": [...]}, your submissions with their checks and reviews

These are the routes farm publish and farm status <id> use. The form fields are:

FieldMeaning
idThe app id. It cannot change on an update
name, pitch, description, version, licenseThe catalog text. version is three numbers
tagsA comma separated list. The site sends one category
permissionsA comma separated list of microphone, files, network, device
commandA start command, or entry as JSON, or entry=null for a library
repo, homepage, videoOptional HTTPS links
mediaJSON of already-uploaded image URLs: icon, header, gallery
screenshotsJSON array of already-uploaded image URLs
python, ports, models, npuUnitsRuntime needs. ports and models are comma separated
health, selfcheckOptional health path, and selfcheck=true to declare the offline check
releaseUrlA public HTTPS URL that downloads the .tar.gz directly
archiveOr the .tar.gz itself, up to 50 MB, with a simple filename

Send a release URL or an upload, never both. A release URL is fetched with a ten second deadline and may redirect up to three times; the URL must be HTTPS on port 443, with no credentials, no fragment, and a real public hostname. The first two bytes of whatever arrives must be gzip. The farm measures the SHA-256 and the exact size itself; numbers you supply are not used.

An update must carry a strictly newer version when it changes the release, and may keep the current version for a text-only change.

curl --fail-with-body \
  -H "Authorization: Bearer $FARM_TOKEN" \
  -F 'id=my-app' \
  -F 'name=My App' \
  -F 'pitch=One useful line' \
  -F 'description=What the app does and what users should know.' \
  -F 'version=0.1.0' \
  -F 'license=MIT' \
  -F 'tags=developer-tools' \
  -F 'entry={"command":"python -m my_app"}' \
  -F 'permissions=network,device' \
  -F 'repo=https://github.com/example/my-app' \
  -F 'archive=@my-app-0.1.0.tar.gz;type=application/gzip' \
  https://tiinyapp.farm/api/seeds

Each entry in GET /api/seeds/mine carries id, name, version, icon, state, prUrl, a checks array of {name, status}, a reviews array, thumbs, comments, and canUpdate. When GitHub cannot be reached the entry carries unavailable: true and its stored state instead. States you will see are preparing, label pending, awaiting review, draft, closed, merged, published, submission failed and submission uncertain.

Three answers are 202 rather than an error, and each says what to do: an interrupted review request that may still have succeeded, a submission that reached review but whose label did not, and the same form sent again to retry that label.

Comments and thumbs up

MethodPathAnswers
GET/api/seeds/<id>/social{"thumbs": 0, "mine": false, "comments": [...]}
POST/api/seeds/<id>/thumbThe same view, with your thumb toggled on or off
POST/api/seeds/<id>/comments201 and the same view
DELETE/api/seeds/<id>/comments/<commentId>The same view

Reading needs nothing. A thumb needs a signed-in account. A comment needs a verified Tiiny profile, is 1 to 1,000 characters, and is limited to five per hour; deleting your comments does not give the limit back. A comment can be deleted by its author or by a farm admin. Only apps in the deployed catalog have a conversation, so an unknown id answers 404, and the wrong method for a route answers 405.

Each comment comes back as {id, author: {handle, name, avatar}, text, at, canDelete}. The author name falls back to "A maker" when a profile is not readable, and an email address is never part of it.

Check for a new release

Arrives with the release path. This route is on the open pull request #5.

MethodPathAnswers
POST/api/seeds/<id>/release-check{"status", "message", "checkedAt", "version", "prUrl"}

Only the app's own verified maker may call it, and only once a minute per app; a second call inside that minute answers 429 with the number of seconds left. status is one of found, listed, none, manual or untracked, and message is the sentence shown to the maker. A found answer also carries the pull request URL. Without the farm's GitHub App credentials the two answers that need no credential still work and anything further answers 503.

Files and pages the Worker serves

PathWhat
/media/<key>An uploaded image, immutable, cached for a year
/seeds-files/<id>/<version>/<name>.tar.gzAn archive uploaded through the site, served as a download
/makers/<handle>/A maker's public page, built live from the catalog
/makers/<handle>/card.pngThat maker's share card
/account/Your apps. Redirects to /submit/ when you are not signed in
/manifests/<id>.jsonOne app manifest, the file the installer reads
/catalog.jsonEvery manifest in one array
/categories.jsonThe tag to category map and the category order
/llms.txtThe publishing guide as plain text, for assistants

Three old paths answer 301: /plant to /install/, /seeds to /submit/, and /farm to /account/.

Rate limits and sizes

ThingLimit
Email codes3 per hour per address
Submissions5 per hour per account
Comments5 per hour per account
Release checks1 per minute per app
API tokens5 live per account
JSON request body16 KiB
Image upload2 MiB
Archive upload50 MB, 30 seconds
A form text field12,000 characters
Comment text1,000 characters
Bio600 characters
Any outbound fetch the farm makes10 seconds, and a bounded body

Errors

Every failure is JSON with one error field holding a sentence meant for a person:

{"error": "Verify you own a Tiiny before submitting an app."}
StatusMeans
400The request was wrong: a bad field, a missing value, an invalid manifest
401Sign in first
403Signed in but not allowed: not the owner, not a verified Tiiny owner, or a missing origin
404No such route, app, comment or token
405Right path, wrong method
408An upload or a fetch took too long
409A conflict: an id someone else owns, a profile already claimed, a sixth token
413Too large
415Wrong content type
422Something outside the farm did not cooperate: an unreadable profile, a release link that did not answer 200
429A rate limit. The message says how long to wait
502GitHub, the mail service or another dependency failed
503A credential is not configured on the farm yet

An unexpected error answers 502 with The request could not be completed. Please try again. rather than the underlying text, because those can contain credentials.

All documentation Install an app Submit an app