The front page, and routing the space
A space has one address of its own — <space>.yaks.fyi/ — and one app can
answer there. That app is the space's front page: it is served at that address
rather than redirected to, and it also answers every path in the space that no
other app claims.
So it is the space's router as well as its homepage. By default it routes by being the fall-through — the app whose name owns a path gets that path, and everything left over comes to the front page. It can also opt in to seeing paths another app owns, before that app does.
Back to the map: The guide
Which app it is
app_set(app, home: true) this app is what <space>.yaks.fyi/ opens
app_set(app, home: false) the space has no front page againNo app becomes the front page by being made first. Until somebody sets one a
space has none, and its bare address lists the apps a visitor may open;
app_list reports which app it is, if any. Only the space owner may move it,
since everyone handed the space itself lands there.
Managing your apps. https://yaks.fyi/manage opens your account's app
library. Each space's management pages live at <space>.yaks.fyi/_yaks,
including assistant setup, settings, and trash. These stay available when a
custom app becomes your homepage; that app cannot replace them.
Being the front page is a component stored on the app, home, and at most one
app in a space has it. Moving it takes effect on the next request: nothing is
copied, no file moves, no address is rewritten. What changes is which app
answers the bare hostname — and the front page's own /<app>/, which becomes a
redirect to it.
That redirect is temporary, not permanent, because the owner can move home to
another app at any time. Hand out the bare address; a link somebody already
holds to /<app>/ still arrives.
Getting back to the default. app_set(app, home: false) puts the space back
to its default page, and so does deleting the app: home is stored on the app,
so a trashed app is nobody's front page and an erased one takes home with it.
Restoring it makes it the front page again — home was never removed, only
ignored while the app sat in the trash. app_rollback puts an earlier version
of a front page back where it went wrong, and setting home: true on another
app moves home there instead. With no front page, / is the platform's list
of the space's apps again, a path no app claims is a 404, and a letter to
<space>@yaks.fyi is refused by name — the sender is told to write to
<space>.<app>@yaks.fyi.
The order a request is answered in
For a request to <space>.yaks.fyi<path> — or to a domain of the person's own,
which goes through the same five steps at their own address, whether that domain
carries the space or one app mounted at its root
(A custom domain) — the first step that answers wins:
- The platform's own paths.
/login,/connect,/_yaks,/mcp, and every app's/api/…store endpoints. The platform answers these and no app routes them. - An app's slug, which owns the first path segment.
/garden/…is the garden app — its ownworker.jsfirst where it has one, its files behind that. An address the app used to live at redirects to it. - The front page's files, for every path no app claims.
/photo.pngis its file and/aboutits page, at the bare hostname. - The space's index at
/, when the space has no front page or the front page has nothing at/. A space that exists always answers with a page, never a 404. - Everything else goes to the front page's
worker.jswhere it has one, and is a 404 where it does not.
Steps 3 and 5 are one request, and the front page is handled exactly the way step 2 handles any app: its worker first, its files behind it. A worker that returns 404 is how it passes the request back — it answers the routes it names and leaves the pages, stylesheets and pictures to the platform (Code of your own).
So step 4 is what is left when neither half of the front page has anything at
/, and step 5's 404 is what is left when there is no front page at all.
Step 2 is the one to hold on to when you are writing the pages: an app's slug
beats the front page at that address. A front page that wants a page at
/garden has to be in a space with no app called garden — or claim the path
with first.
Answering first
first is the opt-in: the paths the front page's worker sees before the app
whose name owns them, written as globs.
app_set(app: 'home', first: ['/recipes/*', '/*/print'])Now /recipes/lemon reaches the front page's worker instead of the recipes app,
and so does /garden/print. What the worker does with such a request is up to
it: a redirect, a decoration, a page of its own — or a 404, which passes the
request back down to the app that owns it, the same pass-through a worker makes
everywhere else on yaks.app.
* matches any run of characters, slashes included. There is one wildcard and
no other pattern syntax: /recipes/* is everything under /recipes/,
/*/print is /print under anything.
The globs are stored in a column of the same home component that marks the
front page, home{first} — a JSON list in one text column, since a column holds
a single value:
{ "entity": { "eid": "<the app>" },
"home": { "first": "[\"/recipes/*\", \"/*/print\"]" } }One component, one place: an app is the front page because it has home, and
what it routes first is written in the same row. So only a front page can carry
globs at all — app_set(app, first: [...]) on an app that is not one is
refused, and passing home: true with it makes the app the front page and sets
its routing in one call.
app_set(app, first: []) puts every path back where it was, leaving the app the
front page it was. Empty is the ordinary state — a front page is plain files
like any other app until somebody changes that — so reach for first when the
front page is meant to route the whole space, and not before. Setting
home: false removes the home component altogether, globs with it.
What is never routable
A glob may not name a path the platform answers itself. These are those paths, and a glob overlapping one is refused whole, before anything is written:
/login /login/* /connect /mcp
/_yaks /_yaks/*
/api/* /*/api/* /platform /platform/*/api/* is there beside /*/api/* because the front page is served at the bare
hostname, where its own store endpoints are /api/… with no slug in front of
them — one set of endpoints, two ways to write the address.
The refusal names the glob and the path it collided with, so /* is refused for
covering /login rather than for being broad. /recipes/* is fine even though
a request under it could have gone somewhere else: a glob that merely contains a
platform path still loses to that path when the request arrives.
Two rules
It fails open. The platform stays the outer router and the front page's
worker is middleware it consults. A worker that throws, returns 404, or takes
longer than a second is skipped, and the platform routes as if it were not there
— the request lands on the app that owns it, exactly as it did before anyone
wrote a first. A break is recorded as an exception on the front page, so
app_errors lists it and the person's agent hears about it
(When something breaks).
A broken router means the customizations stop applying. It never means the space is down.
It acts as the visitor. The request the front page's worker sends onward
carries the person looking, never the app it is routing to. env.STORE there
reads and writes as them, the way it does in any app's worker, so a route may
redirect, decorate or short-circuit — and cannot read a store the visitor could
not have read themselves. Routing is not a way around another app's access.
Moving the space's address
space_set(slug: 'ada-cooks') the space is now ada-cooks.yaks.fyi
space_set(title: 'Ada cooks') just the name it is calledA title, the space's or an app's, is its name: one line, up to 80 characters. A newline in one is folded to a space, and a longer one is refused with the count.
Only the space's owner may move it. Every app in the space moves with the
address — files, data and all — and each keeps the path it had, so
ada.yaks.fyi/cookbook/ becomes ada-cooks.yaks.fyi/cookbook/.
The address it leaves keeps working. It redirects to the new one with the
path and the query kept, permanently, so a link somebody was given a year ago
still opens the page it always did. Letters to <old>.<app>@yaks.fyi still
arrive. And it stays reserved: nobody else can take it, and neither can the
person who left it, until they say to forget it.
space_set(forget: 'ada') the old address stops redirecting, and is freeForgetting is the one part that breaks something: every link and every letter still aimed at that address stops finding anything. Only do it when the person has said they are done with it — and then anyone, including them, can take it.
app_set(app, slug) is the same move one level down, for one app's path within
the space.
A few addresses are kept for yaks.app itself, because at <slug>.yaks.fyi or
<slug>@yaks.fyi they would read as the platform speaking: login, support,
security, billing, status, admin, www, mail, docs, api and the
like. space_new, space_set, app_new, app_set and app_install refuse
one with a sentence saying so, and signing in as support@… gives the space
support2.
What a move never touches: the app's own data (its store is named for the app, not for the address), any domain of the person's own aimed at the space or one of its apps, who is a member, what is published, or what the stats have counted.
Mail at the space's own address
<space>@yaks.fyi is the front page's mailbox — the same address every app has,
with the app part left off (<space>.<app>@yaks.fyi is any other one). A letter
written there lands in the front page's store, as the entity every arrival lands
as: doc for the subject and the words, mail for the envelope, attachments
filed as blobs and linked to it. Mail: an app's own address describes the
whole shape.
Custom mail behavior for the space is whatever the front page does about those rows. A letter is an ordinary write to its store, so anything subscribed sees one land, and the front page's own code decides what happens next — file it, answer it, turn it into a row of the app's own. There is no mail router and no hook to register; the letters are simply in a store you can read.
subscribe('.mail!&.doc?', triage)Nothing about the mailbox itself changes with a front page. mail_list reads
the same rows back from an agent's side, mail_send sends from the same
address, and a space with no front page is named in the refusal when somebody
writes to it, so the sender knows to write to <space>.<app>@yaks.fyi instead.
What a front page is not
- A way to shadow another app. Step 2 is the rule;
firstis the exception the owner writes down, glob by glob, and it is refused wherever it names the platform's own paths. - A gateway with more rights than a visitor. It routes as whoever is looking. If they may not read the app the request was going to, neither may the route in front of it.
- Something a space needs. Most spaces have no front page at all, and their bare address lists the apps instead. Make one when there is a homepage to make.
