When something breaks
Nothing is swallowed. This page is what a refused call returns and how a page
shows it, what the reporter in every page catches on its own, where a break is
filed and how you hear about it once, app_errors, app_versions and
app_rollback, and feedback for anything either of you has to say about the
platform itself.
A refusal
Every endpoint under ./api/ refuses in the same shape — a code for the
program, a message for the person:
{"error": {"code": "not_a_writer", "message": "sign in to change this app"}}When signing in is the way through it carries a third field, signIn: the
platform's login page, with this page already set as its return address.
The codes an endpoint returns, and what each means:
not_a_writer— 401 signed out, withsignIn; 403 signed in. The app'saccessdoes not accept a write from this person. Signed out, send them tosignIn. Signed in, it is the owner's to grant — the message says so.not_a_reader— 401 signed out, withsignIn; 403 signed in. Aprivateapp answering someone who is not a member.not_found— 404. No such endpoint under/api/. The message lists the endpoints that exist: apply, query, me, graph, ws, blob, files/<path>.method_not_allowed— 405. The right endpoint, the wrong HTTP method:applyandblobare POST,blob/<eid>is GET,files/<path>is PUT.expected_websocket— 426./api/wsreached without an upgrade header.subscribe()inclient.jsdoes this correctly; a hand-rolled socket usually forgot.no_bytes— 400. An upload with an empty body.too_large— 413. One upload over 20 MB, or over 2 MB from a visitor to anopenapp. Downscale the picture on the page before sending it.visit_too_large— 413. A visitor to anopenapp sent more than 16 KB in one write. Send it in smaller pieces; the owner and editors are not held to this.too_many_writes— 429. A visitor to anopenapp wrote more than 30 times in a minute. Wait a moment and send again (Publishing and installing an app).no_such_file— 404.blob/<eid>where the eid is not a sha-256, or names bytes this app does not hold.space_full— 413. The space is at its app data ceiling (1 GB on Free, 10 GB on Plus) or photos-and-files ceiling (1 GB on Free, 50 GB on Plus). The message says which ceiling and what to do; the app is fine. File uploads check live storage and only charge growth; duplicates and smaller replacements still work. The usage tool reports the hourly storage reading. Space-owned files in the trash still count until erased; shared version snapshots do not.too_many_reports— 429. More than 30 reports from one app in one minute. A page in a render loop, not a broken endpoint.
A refusal is somebody's deliberate no, so none of them is filed as a break. The rule is the status code: a 4xx is a choice someone made, a 5xx or a thrown error is not.
How a page shows one
client.js throws an error carrying the server's own message, so a catch has
something to show without composing anything:
try { await apply(bundle) }
catch (e) { e.signIn ? location = e.signIn : show(e.message) }Those two lines are the whole of it. e.message is the endpoint's message, or
its code when there was no message, or <status> <a short line> when the body
was not a refusal at all — a 404 that returned the platform's HTML page, say,
which never gets dumped into an error message.
e.signIn is set only when signing in is the way through, and the login page
comes back to the page they were on. Check it first: a signed-out visitor who
typed something into a public app has their work sitting in the form, and
sending them through the sign-in is the only path that keeps it.
Better still is not to be refused. me() on load reports reads, writes and
signIn before anyone types.
The reporter
The platform injects ./api/report.js into every HTML page it serves — first
inside <head>, else first inside <body>, else at the end of a document with
neither, and once. There is nothing to import and no opt-in. It catches four
things:
- A script error. The message, the stack, the file and the line.
- A resource that never loaded — a script, stylesheet, image, or a module in a graph one of them pulled. It listens in the capture phase, because that failure fires on the element and does not bubble. This is the one that hurts most: a page paints a heading and empty space and nothing else reports it.
- An unhandled rejection. Reported as
unhandled rejection: <the reason>. - A call to one of the app's own
./api/endpoints that was refused. It wrapsfetchand watches same-origin responses, passing everything through untouched.
Two limits keep it from becoming the problem. It reports at most 20 things per page, so a render loop that throws every frame does not write a thousand rows. And it only reports what happened on the app's own origin — a script from somewhere else that 404'd, or an analytics beacon an ad blocker refused, was never the app's file.
It makes no other judgement. It sends the response's status and body along with each refusal, and the platform applies the one rule: a 4xx was somebody's choice and files nothing.
When a break leaves the person looking at nothing, the page tells them so itself
— a full-page "Something went wrong. Your assistant has been told." in the
platform's own colors. Only when the page is bare: under 80 characters of text
and nothing that draws pixels in it, judged after load or two seconds,
whichever comes first, and only once. A page that painted is left alone.
The response headers also name the app's report endpoint as a NEL reporting endpoint, so the browser sends network failures that never reached the page at all.
POST ./api/report
The endpoint underneath, in case a page wants to file something itself:
POST ./api/report
content-type: application/json
{"message": "…", "stack": "…", "url": "…", "line": 12}
→ 204It also accepts the browser's own Reporting API format — an array of
{type, url, body} — for CSP violations, crashes, deprecations and network
errors.
Anyone may post: a break belongs to whoever was looking at the page, and asking
a stranger to sign in first would lose exactly the breaks nobody sees. Junk gets
a 204 and writes nothing — a malformed body is the sender's bug, not a break in
this app. Past 30 in a minute, per app, it returns 429 too_many_reports.
A page rarely needs this. The reporter above already covers everything it would send.
Where a break is filed
An exception entity in the app's own store, carrying at, the request
it happened on, the version the app was serving, the message and the
stack. Every source of a break writes the same shape: a page reporting itself,
a route that threw on the way in, and a worker that returned 5xx.
The version number is read fresh rather than from the cache, on purpose. The likeliest moment for a break is right after a deploy, and it must name the release it happened on and not the one before.
An exception has no doc component, and a listing leaves exception and
error rows out unless the filter names one — .doc! is the person's own rows
and never the platform's crashes. Asking for the stamps is not asking for these:
.created! alone does not drag them in. When you want them from a query rather
than from app_errors, name them: .exception!.
Hearing about it once
New breaks are added to the end of the next reply from a tool that changes
something, under a heading, one line each. A read-only tool (app_list,
app_versions, memory_recall) says nothing about them and leaves them unseen:
## unseen errors
- E-84 2026-08-14T10:02:11.004Z exception recipes v3: page /recipes/ —
boom is not a functionSending them marks each one, so the next reply does not repeat them. The mark
records that you were told, not that anything was fixed: app_errors still
lists them.
Only what is still new is included. A break naming a version below the one the
app serves now was made by code a later release replaced, and one naming no
version at all predates the version counter; neither interrupts a reply. Both
are still open, and app_errors still lists them — this rule decides what is
worth telling you about, not what is still broken.
app_errors
app_errors(app, fixed?, seen?, space?)Everything still open in that app — what a page threw in someone's browser, what a request threw on the way, what the platform reported — whether or not you have already been told. Open means not archived.
fixed accepts the ids you have fixed and archives them, which is what stops
them showing here and in the unseen block. An id off a line (E-84) or a bare
eid both work; a list, or a single one bare. If nothing matched, the call is
refused and the message says so — that is how you learn one was already
archived.
seen archives the same way, for breaks you are done with whether or not you
fixed them, and it accepts a bound so you never have to list six ids:
all— everything open in the app.v3— everything up to and including that deploy.2026-08-14, or a whole instant off a line — everything at or before it. A bare day means the end of that day.
Ids work there too, so one call can mix them. A break naming no version is covered by any version bound, and one naming no time by any time bound: nothing can tell whether those are still happening.
Archiving is a write, so it needs a writer. Reading the list does not: a viewer of the space still gets to see what is broken.
Beside the lines it returns the same breaks as data, grouping the ones that share an app, a message and a place into one card with a count — a render loop that threw twenty times is one thing to fix — and a card names every id in its group, so archiving one archives them all.
You will rarely archive by hand, because new files close what the old ones
broke. Every app_deploy, app_install and app_rollback archives every
open break from an earlier version (and every one that names no version at all,
since nothing can tell whether those are still happening); the deploy's reply
reports how many. And every app_files write archives the open breaks that
named the files it just wrote — a page's "failed to load app.js" is fixed by
writing app.js, without waiting for a deploy, because the files serve live. A
break the new files still produce is written again the next time it happens.
app_versions
app_versions(app, space?)Every deploy, newest first, with when it went out and what changed in it. An app keeps its last 20.
yourname/recipes: 3 versions
- v3 (live) 2026-08-14T10:01:00.000Z — changed index.html
- v2 2026-08-13T18:40:12.000Z — restored v1, changed index.html, style.css
- v1 2026-08-13T09:12:44.000Z — 4 filesA version is a manifest — each path against the sha-256 of its bytes — not a
copy of the bytes, so a file unchanged across twenty deploys is stored once.
That is also how a version made by a rollback knows to report restored v1: its
files differ from the version directly below it, and match some earlier version
exactly.
Read it when the person says the app used to work, so you name the version they mean.
app_rollback
app_rollback(app, version?, space?)Puts the app back the way it was. Leave version out for the deploy before the
live one — "that change broke it" almost always means the one under the newest.
Name one off app_versions for anything else.
What moves: every file that version named, restored from its pinned bytes, and
every file the app has now that the version did not name, deleted. Then the
whole release runs again over those files — the components its vocab.json
declares, the commands its tools.json declares, its worker.js re-uploaded —
because everything a deploy plants is a file, so restoring the files is the
whole of a rollback.
What never moves: the app's data. Every row, every upload, everything the store learned. Only the files.
A rollback goes out as a new version, so nothing is lost and a rollback can itself be rolled back. The reply names both numbers:
put yourname/recipes back to v2, live now as v4:
https://yourname.yaks.fyi/recipes/ — changed index.htmlTwo refusals: an app with one deploy or none has nothing earlier to go back to, and a version number the app no longer keeps is refused, with the ones it does keep listed in the message. Pruning past the last 20 deletes only bytes no kept version names, so the oldest rollback an app still offers always has its files.
app_delete and app_restore
app_delete(app, space?, forever?)
app_restore(app, space?)The undo of a deploy is a rollback; the undo of a delete is a restore.
app_delete does not erase anything. It puts the app in the trash and keeps
every byte of it: its files, its store, its deploys, and its slug, which stays
reserved so a restore is exact rather than approximate.
What changes the moment it goes in: its address answers nothing on the web, its commands stop being offered and its views leave your resource list, it is nobody's front page even if it was one, and letters to its mailbox bounce. What does not change: anything it saved.
app_restore takes it back out and puts all of that back. You have 30 days;
app_list shows a Trash section with the days each one has left, and the person
can restore one from their space's own page without an assistant at all. After
30 days the platform erases it, and then there is nothing to restore.
app_delete(app: 'scratch')
→ yourname/scratch is in the trash. https://yourname.yaks.fyi/scratch/ stops
answering and its commands have gone with it; nothing it saved was
touched.Two things to know before you call it. The slug is held for the whole 30 days,
so app_new at that address is refused, with the reason — restore it or erase
it, but do not build a second app on top of one the person may want back. And
forever: true skips the trash: files, data and address gone with no undo. Use
it only when the person has said they mean exactly that.
space_delete and space_restore
space_delete(space, forever?)
space_restore(space)A whole space goes to the same 30-day trash, and the same is true of it: nothing
is erased, every app in it is kept whole, and the address is held so nobody else
can take it. While it sits there every hostname of the space answers nothing,
its apps stop offering their commands and their pages, and letters to any
address under it bounce. space_restore takes it back out and all of that works
as it did before.
space_delete is still the one thing you cannot do yourself. It mails the
owner a link, good for an hour, and reports what that link would take offline;
tell them to check their email. They can also do it themselves, signed in, at
https://yaks.fyi/space/<slug>/delete.
space_delete(space: 'oldlab')
→ nothing is deleted. oldlab is still there, and an assistant cannot delete
a space: a letter is on its way…The person restores it from the space's own address — signed in,
https://<slug>.yaks.fyi/ is the one page a trashed space serves, and it has
the button. space_new at that slug is refused for its owner, with the reason.
forever: true mails a link that erases the space on the spot instead, with
nothing kept: only when the person has said they mean exactly that.
Nothing is lost by a simple mistake
Every destructive thing on yaks.app has a way back, and each tool's own description ends by naming it. Four of them, at four sizes:
- A deleted app or space is in the trash for 30 days, above, and
app_restoreorspace_restorebrings the whole thing back: files, data, address. - A store can be put back to any moment in the last 30 days.
store_restorewinds everything the app has saved back to how it was at a time you name: a bad import, rows deleted that should not have been. The state before the restore is recorded first, so a restore can itself be undone by restoring to a moment just before it, and the answer says how. Call it with no time and it reports the window and every restore already made. - Every file write keeps what it replaced for 30 days, and
app_files(op: 'history', path)andapp_files(op: 'restore', path)list and put one back (the files page). - Every deploy can be rolled back, above.
So fix things first and ask afterwards, rather than the other way round. A change is cheap to try, and an agent that stops to ask permission before every write is paying a price nobody is charging. When the person hesitates ("will I lose everything?"), say which of these covers them. They are usually weighing a risk that is not there.
The one thing with no way back is what somebody explicitly asked to erase:
app_delete(forever: true) skips the trash, and a secret removed with
app_secret_remove was never readable to put back.
feedback
feedback(text, app?, space?)The tool for all feedback about the platform — this connector, its tools, its guide, the way an app is built or served on yaks.app. A tool that refused for no reason you could find, a tool that is missing, a reply that disagreed with what was documented, a step the person found baffling; and equally a rough edge, a wish, a feature idea, a thing that went well. Whether you ran into it yourself or the person said it, it is wanted.
Reach for it the moment it comes up. Where something is broken, work around it and carry on: nobody sees the workaround, and this is what is seen instead.
Send two things and nothing else — what the person said, in their own words, and what you tried and what happened. Who they are, their address, their space, the app if you name one, its version, the platform's release and the time all ride along on their own. Repeating them wastes the reader's attention on the half they already have.
It writes a report in the platform's own store and mails it to the people who run yaks.app, who can write back to the person's address. If the mail cannot go out, the report is kept and the reply tells you so — no need to send it again.
Three an hour, per person. The fourth is a pause, not a no: the reply says so and gives the address to write to directly if it cannot wait. Feedback is not counted against the space's monthly email ceiling — a space at its ceiling is exactly a space with something to say.
A break inside the person's own app is not this. That one is theirs, it is
already in app_errors, and fixing it is yours.
The whole guide: The guide
