yaks.app

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.app/<app>/. No build step is needed: what you write is what the browser gets, except TypeScript and npm imports, which app_deploy compiles. Every other HTML file is a page at its own address, and a site any tool builds serves as built from a folder of the app (code). Every app comes with its own store, a graph of entities, and a small client for reading and writing it from the page. Apps in the same space can work with one entity across those stores.

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 from the page's own folder: the platform gives each page a <base href> at its folder under the app's address, so the same files work wherever the app is served or installed. A page in a folder reaches the client at ../api/client.js.

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

app_deploy releases the app's draft. Open pages get a yak-release event with {version, reload} and an optional or required Reload notice, never an automatic reload. An app can take over the notice without losing input. Use reload: 'required' on app_deploy for a changed contract the vocabulary cannot describe; otherwise the platform derives the level.

An icon.svg beside index.html, square and without <text>, is drawn as 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. An upload is held to the space's room and the app's blob word, so downscale a photo on the page to keep it quick to send.

Deeper: Files and pictures — releases, yak-release, app_deploy's reload, the app's icon, uploads, pictures, and a gallery that never shows one twice.

Components

A component is a named set of properties 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 property; 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 properties, and vocab.json for components of your own.

Deeper: What players see, and what is kept — sync, durable, pace and save: what players see, what stays in a browser, and what the store keeps.

Games

A game's page reads the graph, computes rules and writes bundles. @yaks/game provides rules, controls and HUD parts; @yaks/stage draws ordinary geometry and sound, and @yaks/voxel/three supplies a voxel look. One elected page advances shared creatures; the others draw their relayed values.

Deeper: Build a multiplayer game — Holdfast's working multiplayer pattern, toolkit dependencies, shared time, owner handoff and dropped orders.

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 property, 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. Apps in one space can each add their own component to it, or one app can use a component another app declares. Each component has one home, so a page or command reads and writes it there without copying rows between apps. Each app's access still decides who can read or write its part. graph_query with no app named reads every app the person can reach.

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 of its own, so the person's agent can act on it without a page open: a $defs entry of vocab.json marked "tool": true, the way a yaks package declares its tools, with a description, an argument schema, 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": {
  "tool": true,
  "description": "Log a run for the club leaderboard",
  "input": { "who":   { "type": "string" },
             "miles": { "type": "number" } },
  "required": ["who", "miles"],
  "apply": { "entity": { "eid": "$run" },
             "jog": { "who": "$who", "miles": "$miles" } } }

Deeper: Commands of your own — the whole reference for a command, and the view protocol.

Asking a model

An app asks a model with rows, not a key: a page writes an entry in a transcript (any entity wearing session) with using { model, instructions }, the app's own store asks the model, and the reply lands in the same transcript for any page subscribed to see. questions beside the entry are answered by the typesafe/jev model as answer rows (a choice, a yes-or-no, a score), and a command marked "model": true is one the model may call. One call answered at once is a post to ./api/ai/run, or env.AI.run in a worker, and a question about a picture is one of those, the picture inside it. Every call spends the owner's monthly account budget, weighed in dollars by the model's price.

Deeper: Asking a model — the rows that ask and the rows that come back, typed questions, one call at once, asking about a picture, who may ask, and the models and their prices.

Voice

The people in an app can talk: a page joins a call through the app's own ./api/rtc/ door with @yaks/rtc, publishes the microphone, and hears the voices it asks for. Who is speaking is the relayed rtc component on the entity they speak as. Every voice heard spends the same monthly allowance models do.

Deeper: Voice — speaking, hearing, placing a voice in space, who may speak, and what it costs.

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: store_memory_save keeps a sentence verbatim with a line of context, and store_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 key the person connected (store_connection_need) arrives as env.NAME, which is the reason to write a worker at all.

A worker and a page's scripts may be TypeScript and may import npm packages: name the packages in a package.json, and app_deploy compiles them, pinning the versions it installed in package-lock.json.

Deeper: Code of your own — env, routes, keys, limits, whole workers to copy, and TypeScript and npm packages.

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.

Mail

Every app has a mailbox at <space>.<app>@yaks.app. 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.app address still working. domain_attach gives the CNAME pointing at origin.saas.yaks.app and the ownership TXT record to add. The domain serves only after ownership is verified and Cloudflare is ready; domain_status says what is still needed. Custom domains need Plus.

Deeper: A custom domain — the records 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 property, & joins, .entity.eid=<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 is kept for the app and the person's agent hears about it once; app_errors lists what is still open, and app_list says how many. 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.