developer.hoje.ai — build and ship on hoje.ai

Why build here

Your app, in front of the businesses already running their day on hoje.ai.

hoje.ai is a production platform people sign into. An app here lives inside one person's space, beside the tools she already uses — and she agrees to exactly what the app asks for before anything installs.

This is the business case, measured 2026-09-19. The road below it is the same honest walk this page has always carried, with its stopping point in view.

Who you reach

The people already on the platform — not a customer list you bring with you.

LineStateLicenceEvidence
hoje.ai serves real people in production today.liveahttps://hoje.ai/ answered 200 on 2026-09-19
A real business runs its whole product here, at its own domain.liveahttps://agenda.luveiga.com/ answered 200 on 2026-09-19

No publisher's app is on the platform yet. The road below is where that changes, and its rule names the day it was true.

What the catalog gives you

Distribution is the shelf: what a person opens when she looks for an app like yours.

LineStateLicenceEvidence
The apps a person already has are drawn in her portal's nav, in her own language.livebsrc/uis/portal/registry.tsx
A store she can browse — every published app with its ask in plain words, install in one step, uninstall just as easily.not yet—Moves when something implements the browse and the install act behind it. The store is specified to the row — the disclosure, the one-step install, the uninstall — and nothing serves it yet.

What shipping earns you

The honest version of the flywheel — what is real today, and what the first publishers set.

LineStateLicenceEvidence
Your app's ask — which parts of a person's space it touches, with which verbs — is written before anything installs, and she agrees to it, or she doesn't.livecsrc/protocols/app-data/manifest.ts. No deployed worker runs it yet; it is a published contract you can read and build against today.
Every app a partner ships makes the platform's multi-app story truer and every tenant harder to leave.being built—The platform side runs in production; the publishing side has never carried an outside app. Moves when the first manifest from outside this repository is published.
A developer program — registration, official listing, the terms a shipped app is carried on.not yet—Moves when something implements either half of registering or listing. Nothing does.

From an idea to the road

The path does not end here, and it does not start a second copy of itself. The road, the contract and the state words are this page's own, below this section.

  • The road — the six-step walk from nothing to an installed app, with the rule drawn where it stops.
  • The contract — the manifest, printed in full and readable with nothing installed.
  • Running today — the narrow facts, each saying the small true thing.
  • The three words — what live, being built and not yet mean on this page.

Lede

An app on hoje.ai is a frontend bundle and a manifest.

The manifest declares, before anything installs, exactly which parts of a person's space your app touches and with which verbs. They read that ask and agree to it, or they don't.

You cannot ship one yet.

Every app running on hoje.ai today is one we built — its code is in our build. Nothing here mounts code from a publisher's own space, and there is nowhere to register as a developer.

This page is the route and the point where it stops. Measured 2026-09-07.

Running today

The narrow facts. Each says the small true thing; the larger promise built on it is a step in the road below and carries its own state there.

LineStateLicenceEvidence
hoje.ai is in production, and people sign in to it.liveahttps://hoje.ai/ answered 200 on 2026-09-07
A business runs its own product on this platform, on its own domain.liveahttps://agenda.luveiga.com/ answered 200 on 2026-09-07
The app manifest is a published contract, not a proposal — a closed grammar with two reference manifests written against it. No deployed worker runs it yet; it is code you can read and build against today.livecsrc/protocols/app-data/manifest.ts, with src/protocols/agenda/manifest.ts and src/uis/note/manifest.ts written against it
Every deployable here is one entry in one manifest. A push to the main branch ships it to staging; production is a deliberate promote of a build that already passed.liveahttps://hoje.ai/ and https://agenda.luveiga.com/ are what it promoted; src/workers/deployables.ts is the manifest

The last line is about our pipeline rather than yours, and it is on the page on purpose. "Who is on the other end of this, and do they know how to ship?" is the question a developer evaluating a platform actually asks, and a feature list never answers it.

The road

Six steps from nothing to an app someone on hoje.ai has installed, in the order you would walk them. One rule is drawn across the road at the point it stops.

#StepStateLicenceEvidence
1Read the contract. What an app declares about itself: its name, its kind, its entry, its version, its publisher, the access it asks for, and one line in your own voice.livecsrc/protocols/app-data/manifest.ts. No deployed worker runs it yet; it is code to read and build against, and the whole of it is printed below.
—The road stops here — 2026-09-07———
2Build a frontend against a person's space. Your bundle mounts inside their shell, confined to the region your ask named, addressing only keys relative to it.not yet—Moves when something mounts a bundle it did not itself build and hands it the interface its ask named. Nothing does that today.
3Publish your manifest into your own space, at one immutable address per version.not yet—The addresses and the rule that classifies the write are real code — src/protocols/app-data/addresses.ts. Moves when that runs inside something deployed; today it is reachable only from a test, and no publisher outside our own repository has ever written one.
4Someone browses what you published, sees the exact access you asked for before anything installs, and installs in one step.not yet—Moves when there is a place to browse published apps and an install act behind it. Neither is built.
5Your app hands off to the next one, because what it wrote sits in a shared structure the next app can read.not yet—The ask grammar already names shared structures. Moves when their layout is fixed, so two apps naming the same structure agree on what is in it.
6Register as a developer and list in the official catalog.not yet—The policy is settled: publishing from your own space is self-serve, official listing is vetted. Moves when something implements either half. Nothing does.

Below the rule the steps keep their numbers, their words and their evidence. They lose only their promise. They are still the real route, which is what you came for.

The contract

The one thing on this page you can act on today, printed rather than linked — so it is readable with nothing installed, no account, and no access to our code.

A manifest is a data record, not a config file we host. It carries name, kind, entry, version, publisher, access, and an optional pitch — one line in your own voice, shown to the person at the moment they decide to install.

Two kinds. vendored — the platform build carries your code, and entry is a registry id. bundle — your code loads from your own space and mounts sandboxed, and entry is an address. Same record, same install, and only the mount mechanics differ.

One kind is reserved, and it is the one you will guess. declarative parses and is then refused — it is never stored. It is held for a second wave.

Four verbs, and no more: read, receive, observe, delete. Your ask enumerates regions and verbs, and it is exactly the scope of the interface the platform injects when your app mounts. It is not a statement of intent about what you will do; it is the thing itself. delete is the destructive one: name it only on the slices your app removes records from — deleting a record takes exactly the leaf at its address, and anything wider is calls you compose yourself.

Two regions. private is your app's own region of that person's space. shared names a shared structure — the seam that lets their next app pick up where yours left off.

Versions are immutable. A new version is a new address. An overwrite is classified a defect and is not applied.

One address shape: orgs/<org>/apps/<app>/manifest/<version>.

The three words

Three state words appear on this page, and only three.

  • live — running in production today.
  • being built — the pieces are running; the thing that joins them is not finished.
  • not yet — a sentence. Nothing that does this is finished.

A word is only as good as what licenses it, so each is read one of three ways: a — an address answers in production. b — the code behind it is bundled in something deployed and covered by a test that actually runs. c — the code is at a named path in our public, versioned repository and covered by a test that actually runs. c licenses only that a published artifact exists and says what this page says it says — never that anything happens with it, which is why every c line above carries, in your view, the note that no deployed worker runs it.

Building an app that installs into a hoje.ai portal

You build one app from one sentence. You emit a single JSON object and nothing else — no prose, no code fence. The platform publishes what you emit into the person's own space and asks her to read the access it declares before anything installs.

What you emit

{
  "app": "kebab-slug",
  "name": "Name shown in her nav",
  "pitch": "One line, in your own voice, saying what the app is for.",
  "access": [{ "region": "private", "verbs": ["read", "receive"] }],
  "bundle": "<!doctype html>…one complete document…"
}
  • app — lowercase letters, digits and -, starting with a letter or digit. It names the app's region in her space.
  • name and pitch — plain text, no markup. The platform writes every other sentence she reads; you cannot soften or reword the access it describes.
  • access — the grant ask, and the whole of what the app may touch. Ask for the narrowest that keeps the sentence true. { "region": "private" } is the app's own space; verbs come from the closed set read, receive (write), observe, delete. A shared structure is { "region": "shared", "structure": "<name>", "verbs": [...] } and only a structure the platform names exists. The platform refuses an ask wider than the caller allows and installs nothing. The ask is also what the app's records are held to (below): this is the app's declared data structure — its own private region, and nothing else unless the ask names it.
  • bundle — a frontend-only app as ONE self-contained HTML document: markup, style and script inline. No external script, stylesheet or font, no network call, no build step. It is served from her space and runs in a sandboxed frame with no storage of its own.

How the bundle keeps its records

Inside the shell the frame has no localStorage and no network. The host keeps the app's records in her space, in the app's own private region, and only as far as the access you declared allows. It speaks to the document through postMessage:

  • the document asks for its records with parent.postMessage({ hoje: "load" }, "*")
  • the host answers { hoje: "records", records: [...] }
  • the document hands the whole list back with parent.postMessage({ hoje: "save", records: [...] }, "*")
  • when the host will not keep a save, or cannot read the records, it answers { hoje: "refused", reason: "…" } and nothing changes; show the reason, and keep the list you already had

What the ask must say for this to work: a private entry with read for load to return anything, and receive for save to keep anything. An ask without them gets an empty list and a refused save. A shared entry is shown to her and scopes the app, but the host does not carry it: load and save reach the private region only, so do not promise her a calendar or any shared structure the document cannot write.

Keep the form inert until the host has answered load, or a save made earlier overwrites what the host holds. Records are a JSON array of plain objects, at most 256 KiB written down. The document names no address and cannot reach another app's records. Opened bare (no parent frame), the same document may fall back to its own localStorage.

The app-data grammar

The platform keeps everything an app owns at addresses it composes; the document never writes one. They are what your access resolves to, so know their shape:

orgs/<org>/apps/<app>
orgs/<org>/apps/<app>/manifest/<version>
orgs/<org>/shared/<structure>/<resource>
  • The first is the app's root, and its private region is everything beneath it.
  • The second is one published manifest. A version is immutable: an update is a new version at a new address, and a write over a version that stands is refused.
  • The third is one record of a shared structure. A shared structure belongs to the org, never to one app: a record spelled under an app root is refused, and so is a structure the platform has not registered. The only registered structure is agenda.
  • Every segment is non-empty and holds no /, \ or ...
  • A write that breaks the grammar is refused with a code, and nothing is stored. The manifest codes are defect:invalid-record, defect:unknown-kind, defect:kind-reserved, defect:version-address-mismatch, defect:version-overwrite, defect:empty-access-ask, defect:empty-verbs, defect:unknown-verb and defect:region-outside-grammar. The placement codes are defect:shared-placement-escape, defect:unknown-shared-structure, defect:non-canonical-shared-placement and defect:app-keyed-shared-placement.

The manifest

The platform writes the manifest from what you emit; you never hand it one. It is a record with name, kind, entry, version, publisher, access and, optionally, pitch.

  • kind is vendored or bundle. What you emit is always a bundle: entry is then the address of your document in her space. declarative is reserved for a later wave and is refused today.
  • publisher carries the identity of whoever published; there is no signature scheme yet.
  • pitch, when present, is never blank. Your own voice, one line.
  • The record's version is the version in its address. It never changes after it is written.

The grants

access is a list of entries, and an entry is one of two shapes:

  • { "region": "private", "verbs": [...] } — the app's own region, and nothing else.
  • { "region": "shared", "structure": "<name>", "verbs": [...] } — a registered shared structure.

The verbs are read, receive, observe and delete. receive writes. delete is its own verb and is never implied by receive. An empty ask, an entry with no verbs, a verb outside this list and a region outside private and shared are each refused. The scope the app runs with is exactly the verbs the ask names per region, and she reads each one as a sentence before it installs.

The design-system components

The shared stylesheet is src/frontend/ds.css, in three cascade layers: foundation, vocabulary and app. The components live in vocabulary and every class begins ds-:

  • Type: ds-head, ds-eyebrow, ds-title, ds-lede, ds-section, ds-section-title, ds-muted, ds-rule and ds-link.
  • Buttons: ds-btn with ds-btn--solid or ds-btn--ghost.
  • Lists: ds-list, ds-list-item, ds-list-item-title and ds-list-item-meta.
  • Cards: ds-card, ds-card-title and ds-card-text.
  • Forms: ds-form, ds-field, ds-field-label and ds-input.
  • Feedback: ds-error.

Colour comes from the custom properties --bg, --surface, --text, --muted, --accent, --accent-contrast, --border and --danger; spacing is calc(var(--step) * n), and nothing tappable is smaller than --tap. A bundle is one self-contained document and the frame loads no stylesheet, so it carries the rules it wants inline and uses these names and tokens as its vocabulary; an app built inside the repo imports ds.css itself.

How a scenario is written

A scenario is one Gherkin file under capabilities/<task>/scenarios/<name>/SCENARIO.md, with a validation.ts beside it whose run is the scenario's status. It opens from a user type under users/: Given I am is followed by that type, and a more specific subject rides on the And I am line under it. When I is followed by the trigger. Add one with deno task new-scenario, which writes the file already conforming, and do not copy the template by hand.

How what you emit lands

You do not write into her space and you do not install. The person runs one command, or has you run it, from the platform's repo, with HOJE_DATA_URL naming her data edge:

  • deno task one-ask is the door for a sentence: the platform asks you once, refuses an answer that breaks the rules above before any write, and shows her the sentences for your access. It takes --org <org> --installation <installation> --account <account> --say "<her sentence>".
  • deno task new-home --shape agenda|tally|notes … is the door only when her sentence is one of those three shapes; its bundle is a fixed template and it takes no sentence.

When you are driving one-ask with no terminal, it runs in two steps and never installs on its own. The first (one-ask … --plan, implied without a terminal) writes nothing: it prints the sentences for your access and an ask digest. Show her those sentences as printed — do not paraphrase them — and wait for her yes in the conversation. Only then run the same command with --agree <digest>, passing the digest you showed her and the same installation and account; the install refuses unless the ask and the space it would write to hash to it. There is no flag that says yes for her, and a different ask is a new plan and a new digest.

What you do not do

  • You do not change the platform's source or deploy anything; you emit the object above.
  • You do not install: the platform renders the ask as sentences, and she agrees or declines.
  • You do not widen access after she has read it. A change of ask is a new version.