Building a yaks app
The platform is yaks.app, written the way its address is: lowercase, with the
.app. An app built on yaks.app is a yaks app.
A yaks app is an index.html and whatever files sit beside it, served live at
<space>.yaks.fyi/<app>/. There is no build step and no framework: what you
write is what the browser gets. Every app comes with its own store, a graph of
entities, and a small client for reading and writing it from the page.
Make one with app_new, write files with app_files (the whole set in one
call, as files: [{path, content}, ...]), then app_deploy, and give the
person the URL.
The person can also make or update one without you: the drop zone on their
space's own page accepts a .zip of an app's files, or a single index.html.
This page is the map. Each section says what a subject is and names the page
that covers it; the pages are the reference. Add .md to any page's address for
the markdown an assistant reads.
The store, from a page
Every app is served a client at ./api/client.js with six functions, all
talking to the app's own store: apply saves, query lists, search searches,
subscribe is a query that keeps calling back as rows change, upload saves
bytes, and me reports who is looking. Write every address in the app as a
relative path: the platform gives each page a <base href> at the app's own
address, so the same files work wherever the app is served or installed.
import { apply, me, query, search, subscribe, upload }
from './api/client.js'
await apply({ entity: { eid: '$cake' },
doc: { title: 'Lemon cake', body: '3 lemons...' } })
let recipes = await query('.doc!')An entity is a bundle, {entity: {eid}, ...components}; a $alias in place of
the eid creates one. Who may read and write is the app's access: public,
open or private. Ask me() on load and shape the page before anyone types.
Deeper: The store, from a page — every function of the client, the HTTP endpoints underneath, and who may read and write.
Files
An icon.png beside index.html is the app's icon on a home screen. upload
takes a file and returns where its bytes live, addressed by their own SHA-256,
and a row that points at them is what a gallery draws. One upload is 20 MB at
most, so downscale a photo on the page before sending it.
Deeper: Files and pictures — the app's icon, uploads, pictures, and a gallery that never shows one twice.
Components
A component is a named set of columns an entity carries. The platform's own,
doc, task, completed, comment, person, attachment, image and the
rest, are shared by every app. An app declares components of its own in a
vocab.json at its root, and app_deploy installs them in that app's store:
{ "$defs": { "recipe": { "properties": {
"serves": { "type": "number" },
"minutes": { "type": "number" } } } } }A later deploy may add a column; one that already has rows is never dropped or retyped. A new capability is a new component, not a new tool.
Deeper: Components: the platform's, and your own — every component with its columns, and vocab.json for components of your own.
Coming back later
An app has no cron and nothing sitting awake. Any row can carry a wake
component naming a time and a recurrence, and the store comes back to it then,
stamping it fired. What a firing does is a rule declared in vocab.json, and
an app's own commands can be scheduled the same way.
Deeper: Coming back later — every column, recurrence and time zones, pausing and resuming, what a rule may match, scheduling a command, and why there is no cron trigger to configure.
An entity spans apps
An eid means the same entity everywhere. Two of the person's apps can each keep
their own component on one entity, with no copy and no sync between them: a
reading list keeps the book, a lending app keeps the loan. A component lives
with the app that declares it, and graph_query with no app named reads every
app the person has at once.
Deeper: One entity, two apps — which app a component lives in, and a two-app pair end to end.
Commands of your own
An app can carry commands in a tools.json beside vocab.json, so the person's
agent can act on it without a page open: a description, typed arguments, and one
action, an apply template or a query. They are listed by commands and run
by command; they never join the connector's own tool list, which is fixed and
the same for everyone.
{ "log_run": {
"description": "Log a run for the club leaderboard",
"input": { "who": "text", "miles": "number" },
"apply": { "entity": { "eid": "$run" },
"jog": { "who": "$who", "miles": "$miles" } } } }Deeper: Commands of your own — the whole tools.json reference, and the view protocol.
The notes an app keeps
A rule the person wants followed every time goes in a NOTES.md beside
index.html: weights in grams, one photo per recipe. Any agent that can reach
the app is given it, so the rule is stated once and followed from then on.
Deeper: The notes an app keeps — what belongs in the file, its size limit, and the three places it is handed over.
What the person said
The other half is the person's own words, kept for the whole space:
memory_save keeps a sentence verbatim with a line of context, and
memory_recall finds it again by what it is about. Save what they say the
moment they say how they want something built or handled.
Deeper: What the person said — the shape of a memory, what context is for, and how a recall is ranked.
Code of your own
An app is pages until you give it a worker.js, and then it has a server. Every
request that is not /api/... reaches it first, and anything it answers with a
404 falls through to the files. env.STORE is the app's own store as the person
looking, and a secret set with app_secret_set arrives as env.NAME, which is
the reason to write a worker at all.
Deeper: Code of your own — env, routes, secrets, limits, and whole workers to copy.
Home
The space's front page, set with app_set(app, home: true), is served at the
bare address and is the space's router as well as its homepage: it sees every
path no other app claims, and can ask for paths other apps own. A router that
breaks fails open, so it never takes the space down.
Deeper: The front page, and routing the space — the five steps a request goes through,
the first globs, why a broken router fails open, and where the space's mail
lands.
Saving from another site
An app can take a page off somebody else's website and keep it: a worker route that fetches the address and reads the metadata the page carries about itself, and a bookmarklet that opens that route with the address of the page the person is on.
Deeper: Saving from another site — the whole clipper, the bookmarklet, and what to do when a site refuses.
Sharing an app
Who may read and write an app is its access and its guest list; member_add
invites someone by email. A deployed app can be published under a name the whole
platform shares, and anyone can install a copy into their own space. A copy
shares nothing but the code, and stays on the version it was installed from
until its owner updates it.
Deeper: Publishing and installing an app — access, members, publishing, installing, pinning.
Every app has a mailbox at <space>.<app>@yaks.fyi. Mail in both directions is
rows in the store: sending is one apply with the recipient, the letter and a
deliver request, and a letter that arrives is stored as an entity the page can
subscribe to.
Deeper: Mail: an app's own address — the bundle that sends a letter, what comes back, how an arriving letter is stored, and the limits.
Selling things
A seller on Plus connects a Stripe account to their space once, and after that
any app in the space posts a cart to ./api/pay/checkout and sends the buyer to
the page it returns. A card number never reaches the app, and a paid order is
written into the app's store as an order row.
Deeper: Selling things — connecting the account, the whole shape of the checkout endpoint, what an order contains, and who can read one.
A custom domain
A space, or one app of it, can also answer at a domain the person already owns,
with the .yaks.fyi address still working. domain_attach returns the one
record to add, a CNAME at the hostname pointing at origin.saas.yaks.fyi, and
domain_status says how far it has got. Custom domains need Plus.
Deeper: A custom domain — the record to add and where to type it at each registrar, the apex, moving DNS to Cloudflare, and what each pending state means.
The filter grammar
One grammar everywhere: .doc! selects rows that have a component, .doc? asks
for one beside the rows selected, .recipe.minutes<=30 filters a column, &
joins, id=<eid> fetches one entity whole, limit= windows the answer, and a
bare word is a full-text search. A row comes back with the components the filter
names, so ask for what you will draw.
Deeper: Querying: the filter grammar — every operator, with worked examples.
When something breaks
A refusal returns a code for you and a message for the person, and client.js
throws the message. Every page the platform serves reports its own errors, so a
break shows up in the app's store and the person's agent hears about it once;
app_errors lists what is still open. Every destructive thing has a way back:
the 30-day trash, store_restore, file history, and app_rollback.
Deeper: When something breaks — every refusal, app_errors, and rolling back.
Who visited
Every HTML page the platform answers for an app is counted, and nothing that
could identify a visitor is kept. app_stats returns the visits per day, the
pages opened, the sites that linked there and the countries.
Deeper: Who visited — what a page view records, what it never does, and the three places to read it.
Feedback on yaks.app itself
Anything either of you has to say about yaks.app itself, a tool that refused for
no reason you could find, a page of this documentation that taught you the wrong
thing, a wish, goes to feedback(text, app?), and it reaches the people who run
yaks.app as mail they can answer. An error inside the person's own app is not
this; that is in app_errors, and fixing it is yours.
