# Hay documentation SDK 0.1; package and registry format v1. Source pages: https://hay.so/docs/ --- Source: https://hay.so/docs/index.md # Hay documentation Hay is a desktop workspace with chat, a built-in browser, native coding tools, and optional plugins. A [Hay account](accounts/) is required to use the app; one account connection is shared by the workspaces on your Mac. A plugin adds a focused interface. Hay provides the model connection, local records, and reviewed access to capabilities. ## Find your starting point - [Sign in or create an account](accounts/): verify your email, optionally choose a public handle and approve app sign-in. - [Find and install plugins](marketplace/): browse public releases and review installation in Hay. - [Build your first plugin](quickstart/): make a small tool with local storage. - [Understand local plugins](local-plugins/): find saved code, export source, and share a plugin file. - [Use the SDK](sdk/): read data, generate outputs, hand off work, and use host tools. - [Publish to Hay](publishing/): release a code-only copy under your personal publisher identity. - [Keep private releases](personal-registries/): publish code for your account only. - [Host a registry](registry/): prepare immutable releases for a custom catalog. ## A plugin has three places | Place | What belongs there | | --- | --- | | Source project | The editable manifest and self-contained HTML you keep in a folder or Git repository. | | Installed copy | The code Hay saves inside one workspace, with that workspace’s records and permissions. | | Shared release | A versioned plugin bundle or registry artifact that other people can install. | Editing a source folder does not silently update the installed copy. Sharing a plugin does not share its workspace records. ## What is available Local creation, imports, exports, custom registries, capability review, browser tools, and native coding are implemented in the desktop app. The package and registry formats are version 1; the SDK reference is version 0.1. The account and direct-publishing implementation uses email-code sign-in, personal publisher ownership and immutable releases. New clients use the account/API origin `https://app.hay.so`; the registry stays at `https://plugins.hay.so`. Existing live inbox-code, publication, history and revocation checks used the legacy account host and shared D1 database. Read [account migration](accounts/) for preserved logins and the operator record for new-origin rollout evidence. The first public release is **Documents 0.1.1** (`halil.documents`); install it with empty records and reviewed permissions. The [marketplace guide](marketplace/) explains web discovery and exact install links. The [operator record](account-service/) tracks marketplace and personal-registry rollout separately from the earlier live catalog checks. Custom static registries remain supported. ## For agents Start with the [agent guide](agents/), [SDK reference](sdk/), and [registry contract](registry-reference/). Every page has a plain Markdown version. [llms.txt](../llms.txt) indexes the docs; [llms-full.txt](../llms-full.txt) contains them in one text file. --- Source: https://hay.so/docs/accounts.md # Hay accounts Hay requires an account to use the desktop app. [app.hay.so](https://app.hay.so/account) is the account home for both Hay and the marketplace. Your app account is shared by local workspaces on this Mac. Model-provider connections and sign-ins to other websites remain separate. ## Sign in or create an account The same email-verification flow handles both. Enter your email and verify its inbox code: an existing account signs in, or a new account is created after successful verification. Asking for a code alone does not create an account. There is no separate password signup or handle-based login. ## Sign in through your browser 1. On Hay’s **Welcome to Hay** screen, choose **Continue in browser**. The screen explains: **Sign in or create your Hay account.** 2. Your default browser opens **Sign in or create an account**. Enter your email, then the inbox code and choose **Verify and continue**, if verification is needed. 3. If **Choose your handle** appears, save your optional profile or choose **Do this later**. The original app sign-in request is preserved. 4. Check the identity shown and explicitly choose **Continue to Hay**, even if the browser was already signed in. 5. Choose **Open Hay**, allowing the browser to open the app if it asks. The app finishes only the sign-in request it started. Merely visiting the approval page, returning to the app window or knowing the request URL does not sign it in. Requests expire after five minutes. **Open browser again** returns to the current request; **Cancel sign-in** cancels it. If it expires, start again in Hay. Keep approval links and callback codes private. Do not forward them, paste them into chat or ask an agent to approve your account. Reloading an approved page requires another explicit **Continue to Hay** to create a fresh app callback. ## Email-code fallback If your browser cannot open or you prefer to finish in the app, choose **Use an email code instead**. Enter your email address, choose **Send sign-in code**, then enter the six-digit inbox value in **Verification code** and choose **Verify and continue**. No Hay password is needed. The web sign-in page uses the same email-code service. Codes expire after five minutes, allow three wrong attempts and cannot be replayed after use. Resend is enabled after 60 seconds; server limits per address and IP still apply. Check delivery errors instead of assuming a code was sent. Enter codes directly into the trusted Hay sign-in surface, never a plugin or conversation. ## Choose a public handle, or do it later After email verification, an account without a profile is offered **Choose your handle**: - **Display name** is the public name used for your publishing identity. - **Handle** is public and prefixes published plugin IDs, such as `your-handle.little-notes`. It cannot be changed yet. - **Save profile** saves that identity after checking the handle. - **Do this later** skips setup and continues the original app request or web return page. The whole profile step is optional. Enter both fields if you choose to save; you can use Hay without a handle and finish setup before publishing. Availability is checked while you type, but an available result does not reserve a handle. Saving is the final claim, so another person may take it first. Handles have 3–32 lower-case letters or digits with single internal hyphens and start with a letter. Reserved and taken handles cannot be used. Display names have 1–80 characters. Review the public/permanent-handle notice before saving. If you already have this profile, Hay reuses it. Existing signed-in sessions without a profile remain usable. Browser verification offers setup there; it does not immediately ask you to repeat it in the app. The app’s direct email-code fallback can offer setup once after a fresh sign-in. To finish later, open **Account settings / Hay account** in the app. On the web account page, **Finish your profile / Choose a handle** opens the same setup. **Share & publish** also reuses this profile form. You do not need to create a second publisher identity. Saving a profile does not publish a plugin, upload workspace records or grant capabilities. Public/private publication still requires its own code review and explicit approval. ## Your account control in Hay The small profile button in the app opens your identity and these actions: - **Account settings** opens **Settings / Hay account**. - **Manage on web** opens an account handoff in your default browser. Check the identity and choose **Continue as …** to open the web account page. - **Sign out of Hay** signs out this Mac’s app session. An account handoff expires after two minutes and belongs to the first browser that opens it. If that browser is signed in as someone else, the page explains the switch before you confirm it. Opening the handoff alone does not switch accounts. A used, expired or other-browser link requires a new **Manage on web** action in Hay. ## Marketplace sign-in The marketplace header shows **Sign in** or a small profile button. Its menu shows your web identity, **Manage account**, **Open Hay** and **Sign out**. Signing in returns you to the page you were viewing. An already signed-in browser can reuse its account identity when you explicitly approve app sign-in. The app and browser use the same Hay identity with separate sessions. Signing in on the web does not silently sign in the app, and app sign-in does not copy a credential into browser storage. Different browsers, profiles and Hay’s built-in browser can have separate web sessions. Use **Manage on web** for an explicit app-to-browser handoff. ## What stays local Signing in does not upload existing plugin records, documents, conversations, source repositories or workspaces. A public or private plugin publication is a separate, reviewed code upload. A Hay account also does not automatically connect a model provider or another website. The desktop keeps its account session encrypted outside workspace snapshots. Plugin frames and model providers do not receive it. The host sends it only to the account/API origin and its own exact personal-registry base; public and custom registries receive no account credential. The browser session is an HTTP-only cookie on the account host. Marketplace pages receive limited profile status through a restricted account frame, not the cookie or a bearer token. No session credential is put in browser JavaScript storage or a sharing URL. ## Offline work and sign-out An already signed-in Mac can continue local work during a connection failure while its saved session remains unexpired. Online account management, publication and private-registry reads still require session validation. Expiry or server rejection returns the app to sign-in. | Action | Session affected | | --- | --- | | **Sign out of Hay** in the app | This Mac’s app session, across its local workspaces. Running tools stop. | | **Sign out** on the marketplace or **Sign out of Hay on the web** on the account page | The account session in that browser. | These actions do not sign out every other app or browser session. Neither deletes local workspaces or published release history. App sign-out attempts server revocation and reports when it could not confirm it. An approving session must still be valid while a pending sign-in or handoff is completed. ## Personal publishing A handle chosen during optional account setup is the same personal publisher profile used for releases. If you skipped it, finish setup before your first publication. Releases use its immutable handle, such as `your-handle.little-notes`. Ownership belongs to the account, not a workspace name or a plugin’s author text. Choose a [public marketplace release](publishing/) or [personal registry · Only you](personal-registries/). Organization accounts, shared publisher roles and organization controls are not exposed yet. ## Moving an existing login The default account home and API move from the legacy `accounts.hay.so` host to `app.hay.so`. Existing valid logins are preserved; you do not need another email code just for this move. Updated Hay recognizes an encrypted session saved for exactly `https://accounts.hay.so` when its configured origin is exactly `https://app.hay.so`. It tries the new origin first. If the new hostname temporarily fails DNS lookup, only that migrated login can retry its original trusted HTTPS account server. The app keeps the encrypted saved origin unchanged during that retry and updates it after successful validation at the new origin. Once the canonical origin responds, fallback stops. New sign-ins and custom account servers do not use this exception, and certificate or HTTP failures never trigger it. Workspace records are unchanged. A browser’s host-only cookie cannot simply be read by another host. The legacy account site carries a valid web login through a short-lived, single-use handoff bound to the receiving browser. Check the identity and confirm **Continue as …** before the new host sets its own HTTP-only session cookie. A page visit alone does not approve that transfer. Pending app sign-in requests keep their original destination through the carry, so you can finish **Continue to Hay** afterward. The old and new host cookies refer to the same migrated web session: signing it out revokes that web session on both hosts, while an independent native session remains separate. A used or expired handoff needs a new link; normal sign-in remains available if the original session has ended. While this narrow fallback is active, **Manage on web** can use a validated legacy account URL for the migrated login. It still requires explicit identity confirmation and never places a session token in the URL. The legacy account API remains served for older clients. Bearer requests receive API responses there rather than redirects to another host. App and browser sessions keep their existing storage and sign-out boundaries. ## Verification scope Rebuilt installed Hay retained the existing signed-in account and workspace after Keychain approval. Its **Manage on web** used the trusted legacy handoff, verifying the scoped native DNS fallback. Chrome then carried the existing browser login to `app.hay.so` through explicit identity confirmation, with no new email code or sign-out. The new origin’s HTTPS checks passed against its published address with normal TLS hostname validation. Canonical native fallback retirement is covered by regression tests but has not yet been observed live, and normal Node/macOS DNS lookup recovery remains pending. Read the [operator record](account-service/) for those separate checks. The clarified sign-in/create copy and optional profile offer are implemented with isolated PostgreSQL/D1 regression coverage. That coverage does not by itself verify the new onboarding UI in production. --- Source: https://hay.so/docs/marketplace.md # Find and install plugins The public marketplace at [plugins.hay.so](https://plugins.hay.so/) lets you search published plugins and inspect a release before opening Hay. Browsing needs no account or browser sign-in; using the desktop app still requires a [Hay account](accounts/). ## Browse what a plugin does Search by name, ID, author or description. Results show the latest numeric version of each public plugin. Open a plugin to read its description, included views, requested access, package size and release history. Choose a version to inspect that release’s permissions and checksum. The author name is display metadata, not a code-safety guarantee. Installation leaves permissions off until you review them in Hay. ## Install in Hay 1. Open a plugin’s marketplace page and choose the release version. 2. Select **Install in Hay**. Your browser may ask to open Hay. 3. In Hay, check the overview’s plugin ID, version, registry and destination workspace. 4. Choose **Review installation**. Hay downloads the exact release and checks its manifest, size and SHA-256 against the link and registry. 5. Review the inline installation request and approve it if you want the plugin. Open its task and review any permissions it needs. Opening a link does not install a plugin, grant access or change your saved registry selection. Existing source, version and view-preservation checks still apply to updates. If Hay refuses a changed checksum, unavailable version or conflicting source, fix the release or select the correct source rather than bypassing review. **Copy install link** copies the same exact-version link for a deliberate handoff. It contains no credentials. A private registry address is not a shareable access grant; see [personal registries](personal-registries/). ## Install-link contract An install link has exactly four query parameters: ```text hay://install?registry=&id=&version=&sha256= ``` Use a complete registry base address and URL-encode values. For a validated registry entry, a website can construct the link without handling Hay credentials: ```js function installLink(entry, registryBase) { const url = new URL('hay://install'); url.search = new URLSearchParams({ registry: registryBase, id: entry.manifest.id, version: entry.manifest.version, sha256: entry.bundle.sha256 }); return url.href; } ``` Offer it as an explicit link or copy action. Do not launch it automatically on page load, after a search, or from plugin content. Duplicate or unknown parameters, embedded credentials, fragments, malformed identities and invalid digests are refused. Versions use canonical three-part numeric values; digests use 64 lower-case hexadecimal characters. ## Public and personal plugins The marketplace profile button uses the [central Hay account](accounts/) for web identity and account actions. Its **Sign out** affects that browser session, not the native app. Browsing public releases still needs no sign-in. The web marketplace lists public releases only. Private releases live in your account’s **Personal registry · Only you** in Hay; the public marketplace does not browse private artifacts or ask for a token. To publish, choose the audience in [Share & publish](publishing/). Local creation, source folders and installed records remain local unless you deliberately export or publish code. ## Rollout status The marketplace and [documentation](https://plugins.hay.so/docs/) are live. Public search, details and exact-release checks passed. Packaged Hay’s built-in browser also reached the native overview and inline installation request from a live marketplace link; that demonstration was declined to preserve the existing workspace. Approved installations were tested separately in an isolated end-to-end environment. Read the [operator verification record](account-service/) for the exact production and test scope. --- Source: https://hay.so/docs/quickstart.md # Build your first plugin You need Hay, a [Hay account](accounts/), and a text editor. Building the optional bundle with the SDK also needs Node.js. The source-folder route does not require an npm project. ## Create a source folder Make a folder named `little-notes`. Add `manifest.json`: ```json { "schemaVersion": "hay.app/1", "id": "example.little-notes", "name": "Little notes", "version": "0.1.0", "description": "A small place for one useful note.", "author": "Your name", "entrypoint": "index.html", "icon": "FileText", "color": "#20242c", "capabilities": ["storage"], "views": [{"id": "main", "title": "Notes", "description": "Your note"}], "intents": [] } ``` Use an ID you control before sharing a real release. The `example` prefix is for this tutorial. ## Add the interface Add `index.html`. Keep scripts and styles inline; Hay injects the SDK before your script. ```html Little notes

Little notes

Opening your note…

``` ## Install and grant access In Hay, open **Plugins**, choose **Import local plugin**, and select the source folder. Review the installation request. Then enable **Local plugin storage** in its installed card and choose **Open a new task**. Installation itself grants no capabilities. If storage is off, the example reports the host error instead of pretending to save. ## Test the saved copy Save a note, leave the task, return, and check that it is still there. Revoke storage permission and check the error state. Try keyboard navigation, an empty note, and a narrow window. A regular browser does not provide `window.Hay`. Test host integration inside Hay. Local previews should use explicitly labelled fixtures rather than pretending to provide the host. ## Share it Open **Plugins / Share & publish**, choose Little notes, and select **Export plugin file**. Send the `.hay-plugin.json` file to someone who uses Hay. Their records and permissions are their own. For a reproducible source build, use the packager from the [Hay SDK source](https://github.com/use-hay/app/tree/main/packages/sdk): ```sh node /path/to/hay/packages/sdk/build-plugin.mjs ./little-notes ``` The result is `little-notes/dist/example.little-notes-0.1.0.hay-plugin.json`. Read [package formats](manifest/) before adding capabilities or sample data. --- Source: https://hay.so/docs/local-plugins.md # Local plugins and saved work A local plugin can begin as a source folder, an imported bundle, or a personal interface created inside Hay. Installing it saves a copy in the current workspace. ## Where Hay saves plugins Open **Plugins / Where plugins are saved / Show in Finder** to reveal the actual workspace file. On macOS the default user-data directory is: ```text ~/Library/Application Support/Hay/ hay-account.enc workspace.json workspaces/.json ``` The Hay account connection is device-wide. Its host-owned session is encrypted in `hay-account.enc`, outside the workspace snapshots. Sign-in does not upload this local content. The original workspace uses `workspace.json`; additional workspaces use their own files. Installed HTML, manifests, personal drafts, plugin records, tasks, and granted capabilities are stored in the corresponding workspace snapshot. `HAY_DATA_DIR` can change the directory for development. Keep workspace files private: they include work and conversations, not just distributable plugin code. Model credentials are handled separately by the trusted host. Browser sign-ins live in Chromium’s workspace-scoped profile, outside plugin records. ## Source and installed copy An imported source project stays where you put it. Hay runs its installed copy. Editing the original `index.html` does not hot-reload the installed plugin. Use **Export source folder** under **Share & publish** to get editable `manifest.json` and `index.html` from an installed optional plugin. Keep that folder in a separate repository if you want a source history. Generated personal interfaces can also be changed through **Edit interface**. For an imported local plugin, increase its version and import the new file or source folder to request an update. Hay preserves records and tasks, retains old view IDs, and resets grants. A local import cannot take over a registry installation or replace a Hay-created personal interface; edit a personal interface through Create. ## Share code, keep records **Export plugin file** produces a portable `.hay-plugin.json` bundle. **Export source folder** produces editable files. These app exports include the installed code and manifest only. They exclude saved records, documents, conversations, sources, grants, credentials, and recovery snapshots. Recipients start with empty records. Exporting is a local save; it does not upload or publish anything. You should still review the source before sharing: content deliberately written into HTML or JavaScript is part of the code export. ## Remove and restore Uninstalling an optional plugin removes its executable copy and revokes its capabilities. Hay preserves the workspace records and task history. Reinstallation does not seed over existing records. A registry-installed plugin must return from its original registry, version, and exact package digest to restore its archived namespace. Updates of an installed registry plugin require a newer release from the same recorded registry, retain existing view IDs and records, and reset grants for review. Unsupported migrations and downgrades are refused. Personal interfaces also have a previous-version recovery path. Restoring an interface and recovering a saved record snapshot are separate actions; read the recovery controls before choosing one. --- Source: https://hay.so/docs/sdk.md # SDK overview Hay injects `window.Hay` before plugin scripts. Do not load a remote SDK script and do not import the desktop preload API. Hay account sign-in and publishing authority stay in the trusted host; there is no account-management SDK for plugin frames. The public TypeScript contract is in the [SDK source](https://github.com/use-hay/app/blob/main/packages/sdk/index.ts). ## Connect to the current task ```js const context = await Hay.ready; console.log(context.taskId, context.viewId, context.pinned); const unsubscribe = Hay.onContext(next => { // Preserve in-progress edits when context is delivered again. }); // Call unsubscribe() when disposing the view. ``` `Hay.context` holds the latest context after readiness. Views must be declared in the manifest. Context events can repeat as workspace state changes. ## Services at a glance | Service | What it does | Required capability | | --- | --- | --- | | `storage.get`, `storage.set` | Read and save JSON in this plugin’s workspace namespace. | `storage` | | `documents.list` | Read explicitly imported text documents. | `files.read` | | `sources.list` | Read sources captured in this workspace for the plugin. | `storage` | | `agent.run` | Start an assistant response in the current task. | `models.invoke` | | `models.generate` | Generate a text or JSON result without a chat tool loop. | `models.invoke` | | `outputs.*` | Save, read, and explicitly hand off task-owned outputs. | `storage` | | `coding.*` | Create a scoped spec, request coding, inspect checks, review and request a commit. | Method-specific coding grants | | `browser.*` | Open pages, observe task tabs, and request reviewed actions. | `browser.read` or `browser.control` | | `github.readPullRequest` | Read an approved GitHub.com PR snapshot. | `github.read`; private reads also need `github.private.read` | The [capability reference](capabilities/) maps each permission to its methods. APIs reject unavailable, revoked, or invalid requests; show those errors in the interface. ## Read and save local state ```js await Hay.ready; const notes = await Hay.storage.get('notes') ?? []; await Hay.storage.set('notes', [...notes, {text: 'A useful observation'}]); ``` Keys belong to the plugin identity and workspace. The plugin chooses keys, never filesystem paths. Records survive a task switch. Use task-owned outputs for deliverables that should belong to one task rather than to the plugin as a whole. ## Render returned data safely ```js const documents = await Hay.documents.list(); const list = document.querySelector('#documents'); for (const record of documents) { const item = document.createElement('li'); item.textContent = record.name; list.append(item); } ``` Treat document text, web pages, model output, and tool results as untrusted data. Render text with `textContent`, not by interpolating it into HTML. Data cannot grant a capability or instruct the host to bypass review. ## What the SDK does not expose There is no arbitrary filesystem, shell, raw network, credential, cookie, permission-management, or native desktop-control API. Plugins run in an opaque-origin frame with restrictive content policies. Services are mediated by Hay’s capability broker. `Hay.call` is the lower-level typed transport entry point, not a route around capabilities. Prefer the documented service methods. A method name sent by a plugin is still validated by the host. ## Contract files The [SDK TypeScript declaration](reference/index.ts), [injected client source](reference/client.js), and [standalone packager](reference/build-plugin.mjs) are published as readable source files. [Reference notes](reference/README.md) explain how they are generated from the app. Structural JSON schemas are available for [manifests](reference/manifest.schema.json), [bundles](reference/bundle.schema.json), and [legacy registry indexes](reference/registry.schema.json). Schemas also cover [discovery](reference/registry-discovery.schema.json), [pages](reference/registry-page.schema.json) and [direct releases](reference/registry-release.schema.json). The [registry contract](registry-reference/) describes base-address discovery, paged catalogs and direct release lookup. Those network operations belong to the trusted host, not the plugin SDK. A structurally valid object must still pass runtime checks for cross-field references, grants, ownership, source provenance, exact bytes, review and size limits. --- Source: https://hay.so/docs/capabilities.md # Capabilities and review The manifest lists requested capabilities. Installing a package grants none. Users enable access in **Plugins / Installed**, and can revoke it later. Hay checks the current plugin identity, task ownership, and grants at each operation. Hay account sign-in is a host-level prerequisite for app access, not a plugin capability. It does not grant model, storage, browser or coding access. No capability exposes the account’s verification code or session token. ## Capability reference | Capability | Supported access | | --- | --- | | `storage` | `storage.get`, `storage.set`, `sources.list`, and `outputs.*`. | | `files.read` | `documents.list`; only documents explicitly imported for the plugin. | | `models.invoke` | `agent.run`, `models.generate`; also required to execute a coding run. Permitted context goes to the selected provider. | | `browser.read` | `browser.open`; request approval to open a URL and return visible text. | | `browser.control` | `browser.create`, `sessions`, `observe`, `act`, `screenshot`, `close`, and browser change events. | | `github.read` | Approved public GitHub.com PR snapshots through `github.readPullRequest`. | | `github.private.read` | Private PR reads, in addition to `github.read`, a host-managed account connection, an allowed repository, and approval. | | `coding.read` | `coding.project`, `readFile`, `list`, `get`, `review`; preview also needs `browser.control`. | | `coding.run` | `coding.createSpec`, `run`, `wait`, `stop`; coding-spec output handoff. A run also needs `models.invoke`. | | `coding.commit` | Request `coding.commit` and `coding.integrate` after user review. | ## A grant is not blanket approval A granted browser or coding capability makes the corresponding service available. Navigations, browser actions, coding execution, output handoffs, commits, and integration can still need a concrete host review. Plugins cannot dismiss or approve these requests themselves. Hay presents requests inline in the originating conversation. A pending request does not block other replies. A user can stop a task or decline the request; the plugin must handle rejection. ## Model access Model access sends the plugin’s prompts and explicitly permitted context to the workspace’s selected model provider. That provider’s data policy applies. A browser tab or local document is not automatically model context for every request. No verified zero-data-retention route is configured in the current desktop implementation. A workspace requiring such a route blocks inference rather than silently choosing another provider. ## Revocation and ownership Pausing, uninstalling, or revoking access stops owned work where appropriate. A plugin cannot read another plugin’s namespace, claim another task’s output by guessing an ID, or choose a new filesystem root. Host-managed credentials never appear in plugin messages or results. --- Source: https://hay.so/docs/outputs.md # Generation and outputs Use direct generation when a plugin needs a result rather than an assistant tool loop. Generation and durable output storage are separate operations. ## Generate text or JSON ```js const generated = await Hay.models.generate({ prompt: 'Turn these notes into a concise implementation brief: ...', format: 'text' }); ``` `models.generate` requires `models.invoke`. It returns `{text, json?}`. Formats are `text` and `json`; malformed JSON rejects. Prompts are limited to 60,000 characters and a generation request has a three-minute timeout. The selected provider and retention policy apply. ## Save and read an output ```js const saved = await Hay.outputs.create({ title: 'Implementation brief', format: 'text', text: generated.text }); const summaries = await Hay.outputs.list(); const full = await Hay.outputs.read(saved.id); ``` Outputs require `storage` and belong to the current plugin and task. Supported formats are `text`, `json`, and `coding-spec`. Text outputs use `text`; structured outputs use `data`. Records are immutable. Hay assigns identity, ownership and creation time. A task can retain 100 outputs of up to 180 KB each. Outputs persist independently of the conversation. ## Hand off explicitly ```js const targets = await Hay.outputs.targets(); // Let the user choose one of these destinations. const selected = targets[0]; if (selected) { const copy = await Hay.outputs.handoff(saved.id, { kind: 'task', taskId: selected.taskId }); } ``` `targets()` lists other enabled tasks whose plugins have storage permission. `handoff` asks for review naming the output and destination, then creates an independent copy. The copy records `sharedFrom`. It does not start a model call or execute the receiving plugin. Use `outputs.onChange(listener)` to refresh a task’s output library; it returns an unsubscribe function. ## Hand a spec to coding ```js const output = await Hay.outputs.create({ title: spec.title, format: 'coding-spec', data: spec }); const job = await Hay.outputs.handoff(output.id, {kind: 'coding'}); ``` A coding handoff also needs `coding.run`. It creates a native draft, without running code or granting repository access. Read [native coding](coding/) for execution, verification, and exact-diff review. --- Source: https://hay.so/docs/coding.md # Native coding The selected model supplies inference. Hay owns source reads, the isolated checkout, file edits, command execution, verification, and Git operations. Connect a repository from Hay’s chat composer; plugins cannot select arbitrary filesystem roots. ## Propose a scoped change ```js const draft = await Hay.coding.createSpec({ title: 'Improve the empty state', goal: 'Explain how to start when the list is empty.', acceptance: 'The empty list shows useful next steps.', decisions: 'Use existing components and styles.', paths: ['src/EmptyState.tsx', 'tests/empty-state.test.tsx'], commands: ['npm test', 'npm run build'] }); await Hay.coding.run(draft.id, draft.specRevision); ``` A spec has six required fields: `title`, `goal`, `acceptance`, `decisions`, `paths`, and `commands`. Paths and commands are immutable after creation. Commands are direct argument vectors parsed by Hay, not arbitrary shell scripts. Execution needs `coding.run`, `models.invoke`, and review of the exact scope. ## Watch real progress ```js const off = Hay.coding.onChange(async change => { if (change.jobId !== draft.id) return; const job = await Hay.coding.get(draft.id); renderStatus(job.status, job.events, job.checks); }); const finished = await Hay.coding.wait(draft.id); ``` `coding.get`, `list`, and `review` need `coding.read`. `coding.wait` needs `coding.run`; it waits asynchronously while chat remains available. Stop cancels the run and preserves the checkout. Long healthy jobs do not share a whole-run ten- or fifteen-minute deadline; model calls and tool operations retain their own limits. `coding.project()` returns the connected project and a bounded file inventory. `coding.readFile(path)` reads bounded planning context, refusing ignored, private, and symlinked files. ## Test built output in the browser After all approved checks pass on the final revision: ```js const page = await Hay.coding.preview(draft.id); const observed = await Hay.browser.observe(page.sessionId); ``` Preview needs `coding.read` and `browser.control`. It serves approved built web output on loopback in a temporary browser session. It has no native bridge or saved account sign-ins. Hay renderer previews use labelled sample data; they cannot establish native backend behavior. Use fresh observations and reviewed browser actions to test the actual page. Source maps, private files, non-web content and symlinks are not served. ## Review, commit, and apply ```js const reviewed = await Hay.coding.review(draft.id); // Only after the user reviews the complete current diff: await Hay.coding.commit(draft.id, reviewed.diff.revision, 'feat: explain how to start an empty list'); await Hay.coding.integrate(draft.id); ``` Commit and integrate need `coding.commit` and separate trusted reviews. Every approved check must pass on the exact diff revision. Truncated, stale, and out-of-scope diffs are refused. The user sees a concise change card in chat and opens the full diff in the side panel. A commit is made on the task branch. Integration fast-forwards a clean original repository still at the recorded base. It does not push, overwrite dirty work, or silently resolve conflicts. ## Runtime boundaries Native execution currently needs the macOS sandbox. The worker has scoped writes, loopback-only networking, read-only existing dependencies, and no provider credentials or direct Git writes. Dependency downloads and unsandboxed fallback execution are unavailable. Use `coding.stop(id)` to stop owned work and unsubscribe listeners when disposing the view. Interrupted checkouts are retained; closing and reopening Hay does not automatically replay a coding run. --- Source: https://hay.so/docs/browser.md # Browser and source tools A user can browse manually without giving a plugin browser access. Plugin actions use task-owned sessions and host review. ## Open and read a page ```js const observed = await Hay.browser.open('https://example.com'); ``` This needs `browser.read` and approval for the URL. It opens the page visibly and returns its visible text. Source capture and saved sources belong to the host; plugins do not get a raw HTTP client. ## Observe and act ```js let page = await Hay.browser.create('https://example.com'); page = await Hay.browser.observe(page.sessionId); const button = page.elements.find(item => item.label === 'Continue'); if (button?.actions.includes('click')) { await Hay.browser.act(page.sessionId, { snapshotId: page.snapshotId, kind: 'click', target: button.id }); page = await Hay.browser.observe(page.sessionId); } ``` Interactive methods need `browser.control`. `create` asks to open a task tab. Each action is reviewed against fresh observed state. Never invent element references or reuse a snapshot after an action. `act` supports `click`, `fill`, `key`, and `scroll`. Keys are limited to Enter, Tab, Escape, the four arrow keys, Backspace and Space. Fill accepts plain text up to 4,000 characters. Private fields, credentials, and consequential account actions require the user to take over. ## Sessions and images `browser.sessions()` lists only the current workspace/plugin/task’s agent tabs. A task can open up to three. `browser.screenshot(id)` returns a bounded PNG data URL with width, height and capture time. `browser.close(id)` closes the agent tab. Subscribe with `browser.onChange(listener)` and dispose the returned unsubscribe function. A slow operation in one tab does not queue unrelated chat or another tab. ## Sign-ins and isolation Cookies and site storage persist in a browser profile for each workspace. Closing an agent tab or stopping a task does not sign out. Tabs themselves are not restored after restart. The profile is separate from the user’s everyday browser, and some sites may restrict embedded sign-in. Plugins do not receive cookies, passwords, browser storage, or the native bridge. Downloads, device permissions, invalid certificates, non-web protocols, and native desktop-control APIs are not exposed to plugins. ## GitHub PR snapshots `Hay.github.readPullRequest(url)` reads bounded GitHub.com PR metadata and patches with `github.read` plus approval. `{authenticated:true}` also needs `github.private.read`, an account connection managed in Hay Settings, and an allowed repository. Credentials remain in the host. The result includes source URL, head/base commit IDs, capture time, changed files and available patches. Missing or truncated patches are incomplete evidence. This API has no comment, merge, push or generic GitHub-write method. --- Source: https://hay.so/docs/sharing.md # Share a plugin For a direct handoff, send a plugin file. For editable work, share a source folder or source repository. For public discovery and updates, [publish to Hay](publishing/) under your personal publisher identity or put versioned bundles in a custom registry. For releases only your account can read, choose a [personal registry](personal-registries/). An [install link](marketplace/) proposes an exact release for review; it does not carry credentials or install automatically. ## Export from Hay 1. Open **Plugins / Share & publish**. 2. Choose an installed optional plugin. 3. Select **Export plugin file** for a portable `.hay-plugin.json` release, or **Export source folder** for editable files. 4. Choose where to save it, then share that file or folder using your preferred service. Hay saves these exports locally. It does not upload them, create a remote repository, or publish a catalog entry. Chat is bundled with Hay and is not a shareable optional plugin. ## What recipients do They open **Plugins / Import local plugin**, choose the file or source folder, and review the installation. They then grant the access they want and open a task. The code and manifest travel with the export. Saved records, documents, conversations, sources, credentials, permissions and previous-version snapshots do not. Each recipient gets their own workspace records. ## Keep the source and release distinct Store source in Git when you want reviewable history. A release is a built artifact with a stable ID and version. Increase the numeric version before distributing changed code. Do not silently replace an existing registry release with different bytes. An imported local plugin can be updated by deliberately importing a strictly newer local release. Old view IDs must remain, saved records and tasks are preserved, and grants reset. Local import cannot replace a registry-sourced plugin or a Hay-created personal interface. Archived namespace restoration is also constrained by source and version. Read [local plugins](local-plugins/) before replacing an existing installation. ## Publish through your Hay account Direct publishing uses the Hay account signed into the app and an owned personal publisher handle. It creates a remote release with a stable ID, version and digest while preserving the local plugin and records. Read [publish to Hay](publishing/) for the concrete review and service requirements. The local export buttons remain useful for direct sharing and self-hosting. Saving an export is not the same as confirming an authenticated publication. ## Prepare a registry **Prepare registry files** lets you choose installed plugins and a catalog name. Hay saves a new folder containing `.well-known/hay-registry.json`, `index.json`, digest-addressed artifacts, and a README explaining hosting. This is publish-ready output, not a live publication. Read [host a registry](registry/) for the upload and validation steps, or the [registry reference](registry-reference/) for the exact JSON contract. --- Source: https://hay.so/docs/publishing.md # Publish to Hay Direct publishing sends a code-only release to the Hay publishing service using the account signed into the desktop app. Choose **Public marketplace · Everyone** for public discovery or **Personal registry · Only you** to keep the release private to your account. It is different from exporting a local file or preparing files for a custom registry. ## Before your first release Sign in or create your Hay account and add the optional plugin you want to publish. If you chose a handle after email verification, that same profile is ready for publication. If you skipped it, open **Account settings / Hay account** in the app, **Choose a handle** on your web account page, or the profile form in **Plugins / Share & publish**. Enter **Display name** and **Handle**, review their public identity and permanent-handle notice, then choose **Save profile**. Setup is optional for using Hay, but required before public or private publication. The handle is permanent in version 1: 3–32 lower-case letters or digits with single internal hyphens, starting with a letter. Choose it carefully. The account service owns publisher identity. Changing a manifest’s `author` text does not grant ownership of another publisher’s namespace. Current publishing is personal; there is no organization or team selector. ## Review the release 1. Open **Plugins / Share & publish** and choose the installed plugin. 2. In **Publish to Hay**, choose **Public marketplace · Everyone** or **Personal registry · Only you**, then a stable plugin name and release version. 3. Select **Review publication**. Check the destination and audience, plugin ID, author, version, requested capabilities, byte count and SHA-256 digest. Expand the code and manifest to review what will leave the Mac. 4. If the release is ready, select the confirmation for that destination: **I reviewed this code and want to make this release public.** or **I reviewed this code and want to publish it to my private registry.** 5. Choose **Publish public release** or **Publish private release** and wait for the matching receipt. Plugin names are 1–48 lower-case letters or digits with single internal hyphens, starting with a letter. The full published ID must fit within 80 characters. Release versions use canonical `major.minor.patch` numbers without leading zeroes, with each component at most nine digits. Only the installed code and manifest are included. Live records, documents, conversations, sources, credentials, granted capabilities, and previous-version snapshots are excluded. Content deliberately written into HTML or JavaScript remains part of the code, so review the source before publishing. Publishing is a network action. A local export or a prepared registry folder has not been uploaded. Treat a release as published only after the service returns a successful release result. ## Identity and ownership The published ID combines your publisher handle and plugin slug, for example `your-handle.little-notes`. Publishing creates that release identity without renaming the local plugin or moving its records. Hay keeps the publication receipt alongside the installed plugin. The server checks the authenticated account’s ownership before accepting a release. An account cannot publish into another account’s handle by changing a plugin ID or request body. A published release has a concrete version, exact byte count, and SHA-256 digest. Namespace ownership does not verify a publisher’s legal identity or guarantee code safety. ## Release updates The accepted release appears in **Your published releases** for the selected destination. Public and private histories are separate; switching the destination does not move a release or change its visibility. If a response is interrupted, refresh that list before retrying: the server may already have accepted the release. Keep an existing release immutable. Publish changed code with a newer version rather than replacing a released ID/version with different bytes. Retain older releases so catalog consumers can verify or restore the original artifact. Installation remains a separate action. Publishing a release does not grant capabilities to its users or change another workspace’s records. Registry updates retain their source, version, view, and review checks. Read [personal registries](personal-registries/) for private access boundaries and [the marketplace guide](marketplace/) for public discovery and reviewed install links. Neither path automatically updates a workspace or grants capabilities. ## Custom registries and local sharing You can still use **Export plugin file**, **Export source folder**, and **Prepare registry files** for direct handoffs and self-hosted catalogs. A custom static registry does not need the Hay publishing service to serve its catalog and artifacts, although the desktop app itself requires Hay sign-in. The [registry hosting guide](registry/) covers self-hosted release files. The [registry contract](registry-reference/) describes the read-only catalog format. ## API and hosting The [publisher API](publisher-api/) documents authenticated requests and release receipts for integrations. The [account service guide](account-service/) covers deployment, durable storage and email delivery. These account operations are not plugin SDK methods. ## Service availability The default account/API origin is `https://app.hay.so`, and the public catalog remains at `https://plugins.hay.so`. Legacy account routes stay compatible. Earlier live inbox-code, publication, artifact, history and sign-out checks used the legacy account host with shared D1 storage; the default-origin move has a separate verification record. Hay’s production downloader and installer also installed the live artifact into a disposable fresh workspace with empty records and no grants. The first public release is `halil.documents` 0.1.1. Read the [operator verification record](account-service/) for the exact scope. --- Source: https://hay.so/docs/personal-registries.md # Keep releases in your personal registry A personal registry holds private plugin releases for the Hay account you are signed into. Only that account can read their discovery, catalog, release metadata and artifact bytes. A private release is uploaded to the Hay service; it is different from an installed copy or source folder kept only on your Mac. ## Publish only for yourself 1. Open **Plugins / Share & publish** and select the installed plugin. 2. If you skipped [optional handle setup](accounts/), finish that same profile first; there is no separate private publisher identity. Then choose **Personal registry · Only you** under **Publish to**. 3. Choose the plugin name and version, then select **Review publication**. 4. Check the private destination, identity, version, permissions, code and digest. 5. Select **I reviewed this code and want to publish it to my private registry.**, then choose **Publish private release**. 6. Wait for the matching receipt. **Your published releases** shows the history for the selected destination. The release contains code and manifest only. Workspace records, documents, conversations, credentials and grants are excluded. Text deliberately embedded in HTML or JavaScript remains part of that code, so inspect it before publishing. ## Browse and install In **Plugins / Registry**, choose **Personal registry · Only you** from **Saved registries**. Hay validates the current account online and reads that account’s private registry. Review installation and grant capabilities separately, just as for a public release. The host receives the exact `personalRegistryURL` from the authenticated account service. Its form is `https://plugins.hay.so/private//`. The path identifies the owner; knowing it does not grant access. Copying it into another person’s Hay or a normal browser does not share the releases. ## Credentials and isolation Hay keeps its account credential encrypted outside workspace snapshots. The trusted host adds it only to the account/API origin or the signed-in account’s exact personal registry base. Private requests cannot redirect or escape that base path. Public and custom registries receive no credential; the renderer, plugins, model providers and install links never receive the token. Private responses use `Cache-Control: private, no-store` and `Vary: Authorization`. Missing or revoked sessions receive HTTP 401. A different signed-in account receives HTTP 404. Private metadata and artifact bytes are stored separately from public release tables and are absent from public marketplace search, catalog and artifact routes. Signing out of the app removes this Mac’s credential and attempts server-session revocation. Signing out on the web revokes only that browser session; it does not revoke the app’s separate private-registry credential. It does not delete local installed copies, workspace records or private release history. Reading or publishing private releases requires a valid online account session. ## Versions and scope Public and private releases have independent immutable histories. The same ID/version cannot be overwritten with different bytes within its destination. Changing the publication selector does not change an existing release’s visibility or move it between registries. There is no sharing, organization access, anonymous private download, promotion or deletion API in this version. Use a separate, deliberate public publication review when deciding what should be public; do not treat a private registry path as a sharing link. The [publisher API](publisher-api/) documents private publication routes. The [operator record](account-service/) distinguishes implemented behavior from verified deployment. ## Verification scope Packaged Hay successfully loaded the signed-in account’s live personal registry; it was empty. Private publication, owner isolation, approved installation with empty records and no grants, session restart and revocation passed separate isolated checks. No private release was published in production. The [operator record](account-service/) preserves that distinction. --- Source: https://hay.so/docs/registry.md # Host a registry A registry lets people discover releases and install or update them from a known source. A static HTTPS host can serve discovery, a catalog and artifacts without a server-side application. Larger registries can expose paged catalogs and direct release lookup behind the same base address. ## Prepare files in Hay Open **Plugins / Share & publish**, name the registry, choose the installed plugins to include, and select **Prepare registry files**. Choose a local parent folder. Hay writes a new `hay-registry-*` directory containing: ```text hay-registry-…/ .well-known/ hay-registry.json index.json artifacts/ .hay-plugin.json README.md ``` The export includes installed code and manifests only. Workspace records and grants remain on the Mac. Nothing has been uploaded. ## Build from source projects For repeatable releases, use the generic tooling from the Hay app repository. Build each plugin first with the SDK, then create a `catalog.json` in your registry project: ```json { "name": "Team tools", "plugins": [{ "project": "../little-notes", "artifact": "../little-notes/dist/example.little-notes-0.1.0.hay-plugin.json" }] } ``` Paths are relative to the registry directory. Each artifact must be inside its source project’s `dist` directory. ```sh node /path/to/hay/packages/sdk/build-plugin.mjs /path/to/little-notes node /path/to/hay/registry/build.mjs /path/to/registry node /path/to/hay/registry/serve.mjs /path/to/registry/dist ``` Connect the local server at `http://127.0.0.1:4716/`. Set `HAY_REGISTRY_PORT` for another port. It binds only to loopback; it is a development server, not a public host. ## Put it online Upload `.well-known/hay-registry.json`, `index.json` and `artifacts/` together, including the dot directory. The descriptor points to `index.json`. Preserve their relative paths; hosting under a nested prefix such as `/team/` is supported. Configure the host to return JSON files directly, without redirects, login pages, or HTML fallbacks. In Hay, connect a base address under **Plugins / Registry**, for example `plugins.example.com` or `https://plugins.example.com/team/`. Use your actual domain; these examples do not host a registry. Hay discovers the catalog and remembers saved sources for switching. Existing saved `/index.json` addresses normalize to the parent base, with equivalent historical provenance recognized for updates and restoration. For older static hosts without discovery, only an HTTP 404 or 410 for the descriptor triggers a fallback to `index.json`. A login page, redirect or invalid JSON is an error. Static indexes retain a 100-release / 1,000,000-byte bound. For larger catalogs, implement [paged discovery and direct release lookup](registry-reference/); current client browsing remains bounded. Test the registry in a fresh workspace: browse, install one release, approve installation, grant access, and open a task. Verify that a changed byte or wrong digest is refused. ## Release updates Increase the plugin’s version, build a new artifact, and add a new ID/version entry. Retain previously released entries and artifacts. Upload artifacts before changing the index, so the catalog never points at a missing file. A newly prepared folder includes only the installed versions selected for that export. Before updating an existing hosted registry, merge those new entries and artifacts with its retained release history. Do not replace the old index with a fresh export and accidentally drop earlier releases. A newer version from an installed plugin’s recorded registry can update after review if its old view IDs remain available. Records are preserved and all capabilities reset. Downgrades, foreign-source replacements, changed bytes at the same version, and migrations that remove views are not silent updates. ## Hay’s catalog and publisher accounts The default Hay registry address is `plugins.hay.so`. Its public discovery, catalog, release metadata and artifacts can be read without an account token. The desktop app itself requires a [Hay account](accounts/). For direct publication, Hay’s account/API service authenticates the user and checks ownership of a personal publisher handle. Read [publish to Hay](publishing/) for the release workflow and [publisher API](publisher-api/) for its server contract. Account credentials go only to the account/API origin and the signed-in account’s exact [personal registry](personal-registries/) base. Public and custom registry downloads remain unauthenticated reads. The [marketplace](marketplace/) adds human browsing, search and explicit install links without handling account tokens. A static custom registry needs only its discovery file, version 1 catalog and artifacts. It does not need to implement Hay account login. Account-backed publishing adds namespace authorization; it does not turn a digest into a code-safety guarantee. New account/API requests use `https://app.hay.so`; the public registry origin is unchanged. Legacy account APIs remain served directly for older clients. Earlier live inbox-code, publication and revocation checks used the legacy account origin and shared D1 storage; they do not by themselves verify the new origin. The first public entry is **Documents 0.1.1**, ID `halil.documents`. Hay’s production downloader and package installer verified its artifact and installed it into a disposable fresh workspace with empty records and zero grants. The [operator record](account-service/) describes the exact evidence and UI-test boundary. --- Source: https://hay.so/docs/manifest.md # Package and manifest A source project contains `manifest.json`, `index.html`, and optionally `initial-data.json`. The SDK builds these into a `.hay-plugin.json` bundle. Project metadata and build output are not installed. ## Manifest fields | Field | Contract | | --- | --- | | `schemaVersion` | Exactly `hay.app/1`. | | `id` | Stable lower-case identity with dot or hyphen-separated segments, up to 80 characters. | | `name`, `description`, `author` | Display metadata. Author names are self-reported, not verified. | | `version` | Three non-negative numeric components, such as `0.1.0`. No prerelease labels in this format. | | `entrypoint` | Exactly `index.html`. | | `icon`, `color` | Icon name and six-digit hex color. | | `capabilities` | Requested host capabilities. Duplicates and unknown values are refused. | | `views` | 1–12 named views with unique IDs, titles and descriptions. | | `intents` | Up to 16 prompt suggestions, each pointing to a declared view. | | `defaultTask` | Optional initial task title and declared view ID. | | `agent.instructions` | Optional domain guidance, up to 6,000 characters; cannot grant permissions. | See the [quickstart manifest](quickstart/) and the public [TypeScript contract](https://github.com/use-hay/app/blob/main/packages/sdk/index.ts) for complete field types. ## Bundle format ```json { "schemaVersion": "hay.bundle/1", "manifest": {"...": "complete hay.app/1 manifest"}, "html": "...", "initialData": { "state": {"welcome": "Example record"}, "documents": [{"id": "example", "name": "Example.txt", "text": "Fictional content"}] } } ``` This is a shape illustration; replace `manifest` with a complete valid manifest. `initialData` is optional. Hay’s installed-plugin exports intentionally omit it and omit live workspace data. ## Resources and limits HTML, CSS, and JavaScript must be self-contained. External scripts, resource directories, remote styles, CSS imports, and referenced assets are not supported by version 1. No plugin Node.js process runs in the desktop host. Source files must be plain files without symlinks and fit within the packager’s 1 MB file limit. HTML is bounded at 1,000,000 characters; the final serialized bundle must fit in 2,000,000 bytes. Registries bound each catalog page or legacy index at 1,000,000 bytes and each bundle at 2,000,000 bytes. Paged browsing has additional [aggregate limits](registry-reference/). Unknown package fields, duplicate view or intent IDs, and references to missing views are refused. Validation of structure does not prove that plugin code is safe or useful; review the code you distribute. ## Sample data `initial-data.json` can seed JSON state and up to 100 sample documents. Document IDs must be unique and stable. Hay labels imported sample documents as samples. Initial data applies only to a fresh namespace; reinstall and update never replace saved user records with samples. Keep secrets, credentials, installation metadata, workspace paths, and real user records out of distributable bundles. --- Source: https://hay.so/docs/registry-reference.md # Registry contract Connect a registry base address, such as `plugins.hay.so` or `https://plugins.example.com/team/`. Hay adds HTTPS to a bare hostname and preserves nested path prefixes. The trusted host discovers catalog endpoints and downloads artifact bytes; plugin frames do neither. ## Discovery Hay requests `/.well-known/hay-registry.json`: ```json { "schemaVersion": "hay.registry.discovery/1", "name": "Team tools", "catalog": "v1/registry/plugins", "release": "v1/registry/releases/{id}/{version}" } ``` `name` is nonempty and up to 100 characters. `catalog` is required; `release` is optional. The release template must contain exactly one `{id}` and one `{version}`; Hay URL-encodes each value before substituting it. Paths resolve against the registry base, including `catalog`, `release`, pagination `next` and artifact paths. For a base of `https://plugins.example.com/team/`, `artifacts/file.hay-plugin.json` resolves under `/team/`, not relative to a catalog page. Keep paths relative to preserve a nested prefix. Absolute URLs and protocol-relative origins are refused; every resolved URL must remain on the registry origin. Only a discovery response of HTTP 404 or 410 falls back to `/index.json`. Invalid discovery JSON, redirects and other failures are errors, not a reason to bypass discovery. ## Paged catalog A dynamic catalog returns: ```json { "schemaVersion": "hay.registry.page/1", "plugins": [{ "manifest": {"...": "complete hay.app/1 manifest"}, "bundle": { "path": "artifacts/.hay-plugin.json", "sha256": "<64 lower-case hexadecimal characters>", "size": 1234 } }], "next": "v1/registry/plugins?after=" } ``` This illustrates the shape; supply a complete manifest, real digest and exact size. Omit `next` on the final page. Each ID/version pair must be unique across the catalog. A page has at most 100 entries and 1,000,000 bytes. Hay’s hosted service currently returns up to 50 entries per page, within the byte limit. The current client assembles pages automatically, with limits of 5,000 releases, 100 pages, 20,000,000 cumulative catalog bytes and a 30-second overall operation deadline. Cycles, duplicate releases and exceeded bounds fail explicitly. This is bounded browsing, not an unlimited catalog client. ## Direct release lookup When discovery supplies `release`, installation looks up the exact ID/version without walking catalog pages: ```json { "schemaVersion": "hay.registry.release/1", "manifest": {"...": "complete hay.app/1 manifest"}, "bundle": { "path": "artifacts/.hay-plugin.json", "sha256": "<64 lower-case hexadecimal characters>", "size": 1234 } } ``` The returned manifest must match the requested ID and version. Without a direct endpoint, Hay finds the release through the bounded catalog read. Direct lookup does not bypass integrity checks, installation review or capability grants. ## Static compatibility Static tooling exports a discovery file whose `catalog` is `index.json`; no release endpoint is required. That catalog keeps the original format: ```json { "schemaVersion": "hay.registry/1", "name": "Team tools", "plugins": [{ "manifest": {"...": "complete hay.app/1 manifest"}, "bundle": { "path": "artifacts/.hay-plugin.json", "sha256": "<64 lower-case hexadecimal characters>", "size": 1234 } }] } ``` A legacy index is limited to 100 entries and 1,000,000 bytes. Hosted publishing is not capped at 100 releases: the service uses paged catalogs and direct lookup. Its legacy index returns an explicit upgrade error once it exceeds the compatibility bounds, rather than silently omitting older releases. ## Artifact integrity and transport `sha256` hashes the exact downloaded artifact bytes, including whitespace and trailing newlines. `size` is the exact byte count, from 1 to 2,000,000. The artifact is a complete `hay.bundle/1` bundle; its manifest must exactly equal the release metadata. A digest from another serialization fails verification. Public registries require HTTPS. Numeric loopback HTTP is allowed for local development. Plain HTTP on public hosts, embedded credentials, fragments, redirects and cross-origin paths are refused. Public and custom registry requests omit credentials. The host’s built-in [personal-registry](personal-registries/) access is the narrow exception: an online-validated bearer session goes only to the current account’s exact private base, with every endpoint and artifact confined to that path. Cookies remain omitted and redirects are refused. Each request has at most ten seconds, within the operation’s 30-second deadline. Streamed byte limits apply even without a trustworthy Content-Length. Discovery is bounded at 16,000 bytes; catalog pages and direct release metadata at 1,000,000 bytes; bundles at 2,000,000 bytes. ## Source identity and trust A registry’s base address identifies the source independently of its catalog transport. Saved `/index.json` addresses normalize to their parent base. Historical installation provenance remains intact; source comparisons recognize the equivalent old and new address for updates and restoration. Changing a descriptor endpoint does not create a new publisher or permit a foreign-source replacement. Provenance records the registry source, download URL, digest, size and installation time. A plugin cannot change its provenance or grants through SDK calls. The read-only registry protocol is a host responsibility, not a `window.Hay` plugin API. The format is unsigned. A digest verifies bytes against the selected registry, not legal identity or code safety. The [publisher API](publisher-api/) separately authorizes publication under an account-owned namespace. Static custom registries need no Hay account service; author names there remain self-reported. ## Marketplace and install links Marketplace search returns latest public plugins for human browsing, independently of the full release catalog. Its APIs and numeric release ordering are documented in the [publisher API](publisher-api/). An exact `hay://install` link pins the registry base, plugin ID, version and SHA-256. It opens an overview in Hay; **Review installation** and inline approval remain separate. It never changes the saved registry selection or grants capabilities. See the [install-link contract](marketplace/). ## Downloadable schemas Structural schemas are available for [discovery](reference/registry-discovery.schema.json), [catalog pages](reference/registry-page.schema.json), [direct releases](reference/registry-release.schema.json) and [legacy indexes](reference/registry.schema.json). Passing a schema does not prove same-origin confinement, template validity, exact bytes or installation approval; runtime checks still apply. ## Errors Invalid JSON, unsupported fields, duplicate releases, unavailable versions, pagination cycles, oversized responses, wrong digests, manifest mismatches and redirects fail closed. Fix the registry or publish a correctly versioned release; do not bypass review or replace user records to work around an error. --- Source: https://hay.so/docs/publisher-api.md # Publisher API This reference describes the account and publishing server contract. It is not part of `window.Hay`: plugins cannot manage accounts or obtain the desktop host’s bearer session. The default authenticated API origin is `https://app.hay.so`; public catalog reads use `https://plugins.hay.so`. The legacy account origin remains compatible as described below. Earlier live inbox-code, publication, history and revocation checks used the legacy host and existing D1 storage. The new origin’s deployed routing/HTTPS checks passed using its published address and normal TLS validation. Live installed-app credential retention/scoped fallback and Chrome’s authenticated legacy-to-new browser carry are also verified; canonical native fallback retirement remains regression-covered only. See the [operator verification record](account-service/) for the release and integration scope. ## Legacy account compatibility `https://accounts.hay.so` remains a served account/API origin for older clients. Bearer API requests are handled directly, without redirecting credentials to `app.hay.so`. New clients use the new origin; public/custom registry credential rules are unchanged. Native encrypted-session migration accepts only the exact old production origin to the exact new production origin. The host tries the canonical account API first. Before its first canonical response, only requests carrying the retained legacy credential may retry the original HTTPS server on `ENOTFOUND` or `EAI_AGAIN`. Fresh email-code requests, custom account servers, HTTP errors, certificate failures and other transport failures never qualify. The encrypted saved origin remains legacy while fallback is used and is rewritten after successful canonical validation; a canonical response retires fallback for the running client. **Manage on web** may accept a validated legacy URL for these migrated logins. This is not a general cross-origin credential migration. Browser cookies stay host-only, with no Domain attribute. A valid legacy web session moves through a short-lived one-use handoff bound to the receiving browser and explicit identity confirmation. The new host sets its own HTTP-only cookie without exposing either session to JavaScript or asking for another email code solely for the move. Handoff IDs are not bearer sessions; expiry, replay and approving-session revocation still apply. The migrated cookies share the legacy browser session’s revocation boundary rather than minting an independently surviving login. A validated canonical `/sign-in` destination can preserve a pending desktop request through migration. See [Hay accounts](accounts/) for the user flow and the [operator record](account-service/) for deployment evidence. ## Native email-code fallback These routes support the trusted native host’s fallback. Web pages use the cookie-backed `/web/` routes below rather than obtaining a bearer session in JavaScript. Email-and-password signup and password login are not enabled. | Method and path | Request or result | | --- | --- | | `POST /api/auth/email-otp/send-verification-otp` | JSON `{email, type: 'sign-in'}` requests a code. | | `POST /api/auth/sign-in/email-otp` | JSON `{email, otp}` verifies the code and returns the signed-in user and session token. | | `GET /api/auth/get-session` | Validate the current account session. | | `POST /api/auth/sign-out` | Revoke the presented account session. | Email verification signs in an existing account or creates a new one only after a correct code. Sending a code alone does not create an account. Codes have six digits, expire after five minutes, and allow three wrong attempts. A used code cannot be replayed. The server also limits requests per address and IP; a throttled caller must wait rather than retry rapidly. Keep codes and session tokens out of agent context, plugin data, logs and distributed code. A user should enter a verification code directly in the trusted sign-in surface. ## Browser sessions All web account actions live on the configured account origin, normally `https://app.hay.so`. On HTTPS deployments, the browser uses a host-only `__Host-hay_web_session` cookie with Secure, HttpOnly, SameSite=Lax and Path=/; there is no Domain attribute. Account pages and JSON session responses are private/no-store and vary by Cookie. Account HTML uses `Referrer-Policy: same-origin`; account API JSON uses `Referrer-Policy: no-referrer`. | Method and path | Request or result | | --- | --- | | `GET /web/session` | `{connected:false}` or a sanitized connected profile containing user, publisher if present, expiry and registry addresses. No bearer token. | | `POST /web/send-code` | `{email}` requests the inbox code; returns `{sent:true}`. | | `POST /web/verify-code` | `{email,code}` signs in or creates the verified account, sets its HTTP-only cookie and returns sanitized profile status. | | `GET /web/handles?handle=` | Authenticated, read-only handle availability; `{handle,available}`. | | `POST /web/profile` | `{handle,name,expectedUserId}` saves the current account’s profile and returns sanitized session status with its publisher, HTTP 201. | | `POST /web/sign-out` | Form or JSON `returnTo`; revokes this browser session, clears its cookie and returns a validated HTTP 303 redirect. | Browser POSTs require the exact account origin, except sign-out also accepts the configured marketplace origin. Return destinations are confined to the configured registry origin’s root, plugin or docs pages, or the account origin’s `/account`. A narrowly validated, live `/sign-in?request=` return is allowed for explicit account switching. There is no arbitrary-origin CORS or cookie-backed publication API. ## Optional profile setup Web **Choose your handle** and native profile setup use the same personal publisher principal and namespace as publication. They do not create a second identity, release or capability grant. The offer appears after verification when a profile is missing; **Do this later** continues the original request or validated return destination. Complete profiles skip it, existing profileless app sessions stay usable, and a profile already offered on the browser flow is not immediately offered again natively. Both availability routes require an authenticated verified session: `/web/handles` uses the host cookie and `/v1/handles` the native bearer. They return `{handle,available}`. Invalid or reserved handles return 400; a taken valid handle returns `available:false`. Lookup is read-only and advisory, not a reservation. Final profile creation can still return 409 if another account claimed the handle. Handle lookup is limited to 60 requests per account per minute; profile saves to 30 per hour. `POST /web/profile` requires the exact account Origin, a valid verified cookie session and an `expectedUserId` equal to that current session’s user. An account switch or ownership mismatch returns 409. The response is sanitized account status, never a bearer token. Native setup retains `POST /v1/publisher` with `{handle,name}` and checks the expected user in the host before saving; this expected identity is not delegated to a plugin. Both fields are required when saving: a 1–80-character display name and the validated public handle below. The handle cannot be renamed yet. An already-profiled account can return 409 on another creation attempt; the client refreshes and reuses its existing publisher instead of creating or renaming one. Saved plugin IDs, local records and publication histories keep their identities. Profile setup never publishes code or bypasses release review. Account operations remain outside the plugin SDK. ## Browser approval for native sign-in | Method and path | Request or result | | --- | --- | | `POST /v1/login-requests` | Native host sends `{challenge}`; returns 201 with `{id,url,expiresAt}`. | | `GET /web/login-requests/` | `{kind:'desktop',expiresAt,state}` where state is pending or approved; never returns a callback code. | | `POST /web/login-requests//approve` | Explicit browser action with its valid cookie and `{}`; returns `{approved:true,appURL}`. | | `POST /v1/login-requests//exchange` | Native host sends `{verifier,code}` to exchange once for its own session. No-code requests return 202 pending without a session. | | `POST /v1/login-requests//cancel` | Native host sends `{verifier}` to consume and cancel the pending request. | The native host generates the PKCE verifier and its SHA-256 base64url challenge. The verifier remains in the host process. Requests expire after five minutes; their 43-character base64url IDs are not login grants. Approval creates a separate, single-use 43-character callback code. Only the explicit approval POST returns `hay://sign-in?request=&code=`; a page visit or status GET never supplies it. The user then chooses **Open Hay**. Native completion requires both the matching verifier and callback code, the current pending request and a still-valid approving session. Polling with the verifier alone cannot sign in. The exchange consumes the request atomically before issuing a native session; cancellation, expiry and replay fail closed. Reapproving explicitly from the same browser session can rotate the callback code while the request remains pending. Keep callback values only for that user-initiated handoff; do not persist, log, forward or put them into agent context. Callback links accept exactly one request and one code, with no extra parameters, credentials or fragments. ## App-to-web account handoff `POST /v1/web-handoff` requires the native bearer session and accepts `{returnTo?}`. It returns a single-use account-origin `/continue?handoff=` URL, expiring after two minutes. It contains no session token. The initial `/continue` visit binds the handoff to that browser using a separate host-only HTTP-only flow cookie. `GET /web/handoffs/` requires that binding and returns only the identity and expiry. Explicit `POST /web/handoffs//accept` with `{}` consumes the handoff, validates the approving native session and sets or reuses the correct browser session. A normal app-to-web handoff returns a validated account-origin `/account?returnTo=...` destination. Legacy browser carry can instead return a narrowly validated canonical `/sign-in` request destination. The client replaces the URL to remove the handoff ID. Opening the page does not accept it or switch an existing browser identity. Another browser, an expired link, replay or a revoked approving session cannot complete it. Web sign-out and native sign-out revoke separate sessions; there is no sign-out-everywhere control in this version. ## Marketplace profile frame `GET /session-frame?parent=` is served only for the configured registry parent, with matching `frame-ancestors`. Other account pages cannot be framed. The frame fetches `/web/session` using same-origin cookies and posts `{type:'hay.session/1',session}` with sanitized profile status to that exact parent. The marketplace validates both `event.origin` and `event.source === iframe.contentWindow`. The frame accepts refresh messages only from its exact parent and origin. Public browsing requests still omit credentials. Neither the frame message nor web profile APIs expose a bearer session, and JavaScript never reads the HTTP-only cookie or uses local/session storage for credentials. ## Authenticated account routes The account and publication routes listed below require `Authorization: Bearer ` for an email-verified account. Public `/v1/registry/` reads do not require it. Browser cookies alone cannot mutate publications. The host sends tokens only to the configured account/API origin or its own authenticated personal-registry base, never to a public or custom registry. | Method and path | Result | | --- | --- | | `GET /v1/account` | `{user, expiresAt, publisher?, registryURL, personalRegistryURL}`. User fields are `id`, `email`, and `name`. | | `POST /v1/web-handoff` | Create the explicit app-to-web handoff described above; requires the native session. | | `GET /v1/handles?handle=` | Authenticated native availability lookup; `{handle,available}`, with no reservation. | | `POST /v1/publisher` | Create the account’s personal profile with JSON `{handle, name}`. Returns `{id, handle, name}` with HTTP 201. | | `GET /v1/publications` | `{publications}` for the account’s publisher, including plugin ID, version, digest and publication time. | | `POST /v1/releases` | Publish JSON `{bundle, sha256}` to the public marketplace; returns a release receipt. | | `POST /v1/personal/releases` | Publish the same reviewed format to the caller’s private registry. | | `GET /v1/personal/publications` | `{publications}` for the caller’s private release history. | A handle is 3–32 lower-case letters or digits with single internal hyphens, starting with a letter. Repeated or trailing hyphens are refused. Reserved handles are unavailable. The display name is 1–80 characters after validation. Version 1 allows one personal publisher per account and no handle rename, organization creation or membership-management endpoint. ## Release body Send only `bundle` and `sha256`. The bundle must pass the canonical SDK validator and contain `schemaVersion`, a complete manifest, and self-contained `html`. `initialData` is refused for direct publishing. The manifest ID must be `.`. Slugs are 1–48 lower-case letters or digits with single internal hyphens and start with a letter. The complete ID must fit within 80 characters. `manifest.author` must equal the publisher profile’s display name. Release versions use canonical `major.minor.patch` numeric components without leading zeroes, with each component at most nine digits. This publishing rule is narrower than the general package format. Use a newer version for changed code. The digest hashes the exact service serialization: ```js const bytes = Buffer.from(JSON.stringify(bundle) + '\n'); const sha256 = createHash('sha256').update(bytes).digest('hex'); ``` `createHash` is the Node.js crypto hash function. Property order and the trailing newline affect the hash; do not substitute the bytes of a separately prettified file. The complete serialized bundle must fit within 2,000,000 bytes. ## Ownership and immutability The service checks the authenticated account’s publisher membership and owned handle. A caller cannot claim another publisher’s plugin by submitting a different author or ID. Ownership is enforced server-side rather than trusted from the request. A new ID/version is stored atomically with its manifest, digest and artifact. Repeating an identical publication returns the same receipt. Reusing an ID/version with different bytes returns a conflict. Successful release requests return status 201. The receipt contains `pluginId`, `version`, `sha256`, `size`, `registryURL`, and `artifactURL`. It identifies the accepted release. It does not attest to code safety or the publisher’s legal identity. ## Public reads Connect `plugins.hay.so` as the registry base. These read-only routes need no account credentials: | Method and path | Result | | --- | --- | | `GET /.well-known/hay-registry.json` | `hay.registry.discovery/1` descriptor with catalog and release paths. | | `GET /v1/registry/plugins` | `hay.registry.page/1` catalog page, up to 50 entries within 1,000,000 bytes. | | `GET /v1/registry/plugins?after=` | Next page; follow the response’s base-relative `next` path. | | `GET /v1/registry/releases//` | Exact `hay.registry.release/1` metadata; no catalog walk required. | | `GET /artifacts/.hay-plugin.json` | Immutable artifact bytes. | | `GET /index.json` | Legacy `hay.registry/1` compatibility index. | Hosted publishing retains releases without the old 100-release catalog ceiling. The legacy index alone remains bounded at 100 entries / 1,000,000 bytes and returns an upgrade error beyond that bound; it never silently drops old releases. Artifact responses use immutable cache headers; discovery and catalog metadata can be refreshed. Read the [registry contract](registry-reference/) for bounded client browsing and same-origin verification. ## Marketplace reads The registry host serves the marketplace shell at `/` and `/plugins/`. It keeps `/.well-known/hay-registry.json` for protocol discovery. Public pages need no token or browser sign-in. | Method and path | Result | | --- | --- | | `GET /v1/marketplace/plugins?q=&after=` | `{plugins, next?}`; up to 24 latest releases, one per public plugin. | | `GET /v1/marketplace/plugins/?after=` | `{plugin, releases, next?}`; latest plugin plus up to 50 releases in numeric-newest order. | Entries contain `{manifest, bundle}` with the same manifest, path, exact size and digest as registry release metadata. Search matches the current ID, name, description and author literally and case-insensitively, up to 120 characters. It is not a regex or fuzzy-search API. Follow `next` relative to the registry base rather than constructing cursors. Latest metadata is updated atomically, including when an older version is published later. Use an explicit [install link](marketplace/) to hand an exact release to Hay. It carries only registry, ID, version and digest, never an account session. ## Private registry reads `GET /v1/account` returns the caller’s `personalRegistryURL`, shaped as `https://plugins.hay.so/private//`. The path identifies an owner; it is not a secret or access token. Under that base, discovery, `v1/registry/plugins`, `v1/registry/releases//` and `artifacts/.hay-plugin.json` require the same owner’s valid bearer session. Responses use `Cache-Control: private, no-store` and `Vary: Authorization`, including errors. Missing/revoked sessions receive 401; a different authenticated owner receives 404. Private bytes and metadata use separate storage and never appear in public catalog, marketplace or artifact reads. Private publication uses the same code-only validation, owned handle and digest rules. Its receipt additionally identifies private visibility and the private registry/artifact addresses. Public and private immutable histories are independent. There is no sharing, organization access, anonymous private download, promotion or deletion API. The desktop confines authenticated requests to its own exact private base and validates the session online. It refuses redirects and escaping endpoints; it never delegates the credential to a renderer, plugin, model or custom registry. ## Failures and limits API errors use appropriate HTTP status codes: 400 for invalid data or a digest mismatch, 401 for a missing or invalid session, 403 for missing publisher ownership, 409 for handle or immutable-release conflicts and legacy-index capacity errors, and 429 for throttling. Authentication routes also return their authentication-provider error structure. Failed verification-email dispatch returns HTTP 503 rather than a false success. A provider acceptance receipt is not proof of inbox delivery; account verification still requires the actual code received by the user. The service allows at most three code requests per email address in a 15-minute server bucket, with additional IP-based authentication limits. Each publication destination allows up to 30 requests per account per hour. An unavailable email transport or database is an error, never a successful login or publication. --- Source: https://hay.so/docs/account-service.md # Deploy the account service The recommended free-plan setup is Cloudflare Workers Free with D1 and Resend transactional email over HTTPS. Native Cloudflare `EMAIL` sending is an optional paid-plan path. PostgreSQL remains available through Hyperdrive or a Node.js 24.3 or later server. The account and publishing service is separate from the static Hay website and local workspaces. The Worker, D1 database and HTTPS routes are deployed. A real inbox code has now verified account ownership, followed by personal publisher creation, public release, authenticated release history and session revocation. ## Verified live flow | Check | Status | | --- | --- | | Historical account health check on legacy `accounts.hay.so` | Verified HTTP 200 before the default-origin move. | | New `app.hay.so` routing and HTTPS | Verified against its published address with normal TLS hostname validation; authenticated carry is a separate check. | | Registry discovery at `plugins.hay.so` | Verified HTTP 200 and `hay.registry.discovery/1`. | | Paged catalog and exact release lookup | Verified HTTP 200, response schemas and matching release metadata. | | Legacy `/index.json` | Verified HTTP 200 for the current catalog. | | Unauthenticated `GET /v1/account` | Verified HTTP 401. | | Real inbox code and account sign-in | Verified against the deployed service, including the rebuilt packaged Hay app. | | Native global account and publisher history | Verified in Settings and Share & publish using live native credentials; existing workspace and model settings were preserved. | | Owned public release and authenticated release history | Verified for the reviewed code-only release below. | | Sign-out and session revocation | Verified; the revoked session is rejected with HTTP 401. | | Production download and installation code | Verified in a disposable fresh workspace, with empty records/documents/sources and zero grants. | `npm run check:live` checks health, discovery, the paged catalog, an exact release when available, the legacy index and unauthenticated account rejection. Authenticated account, publication and installation behavior were verified separately through the packaged app, deployed APIs and Hay’s production registry downloader and package installer. On 12 November 2026, migration `0002_paginated_registry.sql` was applied and the discovery/pagination Worker deployed. The updated Hay client connected using `plugins.hay.so`, directly downloaded `halil.documents` 0.1.1 and installed it into a disposable fresh workspace with empty records and no grants. Its digest remained unchanged. The first public release is **Documents 0.1.1**, with ID `halil.documents` and SHA-256 `01793512eb4433f47665eeee955fb8f1c8111256ac6267129aafecf1439f89e3`. Its [release metadata](https://plugins.hay.so/v1/registry/releases/halil.documents/0.1.1) and [immutable artifact](https://plugins.hay.so/artifacts/01793512eb4433f47665eeee955fb8f1c8111256ac6267129aafecf1439f89e3.hay-plugin.json) were checked for exact bytes and integrity. Live packaged sign-in, the global publisher display and authenticated release history are verified. Publication and fresh-workspace installation were verified separately through the deployed API and production host code. The full native publish click-through previously ran against a local authentication service; that complete journey has not been repeated against production. ## Default: Workers and D1 The free-plan D1 deployment uses a `DB` database binding and the Resend REST sender. Accounts, verified sessions, publisher ownership, rate limits, release metadata and artifact bytes are durable database records, not process memory or an ephemeral filesystem. 1. For a new deployment, create D1 with `npx wrangler d1 create hay-accounts` and put its ID in `wrangler.d1.jsonc`. The current Hay configuration records the deployed account/database identifiers, not credentials. Keep the `DB` binding, database name and `migrations-d1` directory aligned; use your own resources for a separate deployment. 2. Store `HAY_AUTH_SECRET` with `npx wrangler secret put HAY_AUTH_SECRET --config wrangler.d1.jsonc`. Keep it stable across deployments; do not commit or print it. 3. Verify a sending-only domain in Resend, store its sending API key as the `RESEND_API_KEY` Worker secret, and set `HAY_EMAIL_MODE=resend` plus `EMAIL_FROM=sign-in@hay.so`. Complete the key and sender setup before switching the deployed Worker to this mode. It does not need a native `EMAIL` binding. 4. Run `npm run migrate:d1:local` against disposable local storage, then explicitly apply `npm run migrate:d1:remote` to the intended production D1 database before serving requests. 5. Run `npm ci`, `npm test` and `npm run build:worker`, review the production configuration, then use `npm run deploy:worker` when ready. Both Worker commands select `wrangler.d1.jsonc`. 6. For a new deployment, attach the account and catalog hostnames as Worker Custom Domains after reviewing DNS. The Hay hostnames already route to the deployed Worker. Verify real email delivery, sign-in, revocation and publication before treating the service as ready for users. The adapter fails with a service-unavailable response if required database, authentication or selected-sender configuration is missing. A dry-run build does not provision those resources. Changing to Resend mode before adding its secret can therefore also break the previously working readiness endpoints. ## D1 migrations and atomic releases The initial checked-in migration is `migrations-d1/0001_accounts_and_registry.sql`. It includes the pinned authentication schema and Hay’s publisher, release and catalog constraints. The additive `0002_paginated_registry.sql` removes the monolithic publication-cap trigger for paged registry reads, while retaining immutable-release enforcement and counters. Apply pending migrations before rolling out the new Worker. Future schema changes must add another numbered migration; do not regenerate and overwrite a migration already applied in production. The migration commands use the same D1 profile as the default build and deployment: ```sh npm run migrate:d1:local npm run migrate:d1:remote ``` The remote command changes the configured production database. Check the target database ID and back up before applying production migrations. Worker requests never run migrations. Local D1 tests directly apply the checked-in SQL before exercising authentication and publication. Publisher creation and publication use D1 batches. Ownership and immutable-version constraints keep competing requests atomic: a conflicting publication rolls back instead of leaving a visible partial release or orphaned artifact. The legacy index’s read limit is not a publication cap; releases remain available through paged catalogs and direct lookup. The public contract stays `hay.registry/1`. A release still has an exact byte count and SHA-256 digest. Storage choice does not change the account, namespace or installation rules. ## Worker bindings and variables | Setting | Purpose | | --- | --- | | `DB` | D1 database binding for the default hosted profile. | | `HAY_EMAIL_MODE` | Set `resend` for the Workers Free setup. | | `RESEND_API_KEY` | Resend sending key, stored as a Worker secret. | | `EMAIL_FROM` | Sender permitted by the verified domain; intended value `sign-in@hay.so`. | | `HAY_AUTH_SECRET` | At least 32 random characters, stored as a Worker secret. | | `HAY_BASE_URL` | Default account/API origin; intended production value `https://app.hay.so`. | | `HAY_LEGACY_ACCOUNT_URL` | Explicit compatibility account origin, `https://accounts.hay.so`, distinct from `HAY_BASE_URL`. | | `HAY_REGISTRY_ORIGIN` | Public catalog origin; intended value `https://plugins.hay.so`. | The account and catalog origins require HTTPS in production. Embedded credentials, path prefixes, query strings and fragments are not valid service origins. The desktop’s account/API origin must match the deployed service. Public discovery, `/v1/registry/`, legacy `/index.json` and `/artifacts/` paths can use the registry hostname while authenticated account routes use the account hostname. Connect the registry base address; see the [registry contract](registry-reference/) for discovery, pagination and direct lookup. Registry base addresses may include a nested prefix, while the deployed account service configuration here uses origins without paths. Serve JSON directly: Hay refuses redirected catalog requests and cross-origin artifact paths. Bearer sessions are sent only to the account/API origin and the signed-in owner’s exact personal-registry base, never to public/custom registries, plugin frames or model providers. Private requests require online session validation, cannot escape that base and refuse redirects. ## Resend on Workers Free The server sends verification emails through the fixed Resend HTTPS API with `HAY_EMAIL_MODE=resend`. Store the key in the Worker, not the desktop app, website, plugin state or model context: ```sh npx wrangler secret put RESEND_API_KEY --config wrangler.d1.jsonc ``` Resend’s currently listed free transactional allowance is 3,000 emails per month with a 100-per-day cap. Check [Resend pricing](https://resend.com/pricing?product=transactional) before rollout; those limits can stop new sign-in codes. Keep the selected free plans and do not enable paid upgrades or overages as an automatic fix. The provider receives the destination address and sign-in message for delivery. Hay accepts a send only after a successful API response with a receipt ID; provider acceptance is not proof that the user received the code. Failed, rejected or timed-out sends must remain errors, not successful sign-ins. ## Sending-only verification and existing mail Add the owned sending domain in Resend and use the exact sending records it generates. These may include DKIM TXT and return-path CNAME, SPF or MX records; do not substitute an older provider template. Follow [Resend’s domain verification guide](https://resend.com/docs/add-a-domain). Sending verification does not require moving DNS to Cloudflare or enabling inbound email with Resend. Preserve the current apex mail-forwarding MX, SPF and other verification records. A return-path or bounce MX belongs at the provider-specified subdomain, not as a replacement for the apex inbox routing. Keep verification CNAME records DNS-only when the DNS provider offers proxying. For `hay.so`, Cloudflare is authoritative DNS. Preserve the active receiving provider’s MX/SPF configuration and add only the provider-generated sending records. Sender verification does not create an inbox or configure inbound mail; any receiving-provider change is a separate owner-controlled action. The Hay sending domain is verified, and its domain-restricted Sending access key is stored only as a Cloudflare secret. The deployed Worker uses `HAY_EMAIL_MODE=resend` without the optional native email binding. Real inbox receipt and the account/publication flow have passed the live check above. Local outboxes and fixture senders are still not substitutes for that verification. ## Optional paid native email Cloudflare’s native `EMAIL` binding can send to arbitrary public recipients only on Workers Paid. Sending to a restricted verified destination is not a replacement for public account sign-in. See [Cloudflare Email Service pricing](https://developers.cloudflare.com/email-service/platform/pricing/). If the operator deliberately chooses the paid path, configure `HAY_EMAIL_MODE=binding`, onboard the Cloudflare sending domain, set the permitted `EMAIL_FROM`, and add the `EMAIL` binding. Review billing and DNS separately. Keep this optional; the current choice is Workers Free with an external sender. ## Alternative: PostgreSQL and Hyperdrive A PostgreSQL-backed Worker uses `HYPERDRIVE` instead of `DB`; the Worker reads its `connectionString`. Create the Hyperdrive configuration with query caching disabled so authentication, profile and revocation checks read current state. The optional PostgreSQL profile remains `wrangler.jsonc`. Check it with `npm run build:hyperdrive` and deploy it deliberately with `npx wrangler deploy --config wrangler.jsonc`. Do not replace an existing D1 Worker with this profile without planning the database switch. Run the PostgreSQL migration command `npm run migrate` with `DATABASE_URL`, `HAY_AUTH_SECRET`, the production origins and the selected email mode in a trusted operator environment. The migration is explicit and serialized in the database; production Worker requests do not run schema migrations. A Node host also supports PostgreSQL and SMTP. With Node.js 24.3 or later, configure its environment and run `npm start`; the Node entry point migrates before opening the listener. | Node or PostgreSQL setting | Purpose | | --- | --- | | `DATABASE_URL` | Durable PostgreSQL connection string for the Node server or PostgreSQL migrations. D1 Workers do not need a PostgreSQL URL. | | `NODE_ENV` | Use `production` for production Node hosting. | | `HAY_EMAIL_MODE` | `resend` for the REST sender, `smtp` for Node SMTP, or `binding` for the optional paid Worker sender. | | `SMTP_HOST`, `EMAIL_FROM` | SMTP provider host and permitted sender. | | `SMTP_PORT`, `SMTP_SECURE` | Provider transport settings; port defaults to 587, with STARTTLS unless secure transport is selected. | | `SMTP_USER`, `SMTP_PASSWORD` | Provider credentials when required. | | `HOST`, `PORT` | Node listener address and port; development defaults are `127.0.0.1:4317`. | The Node SMTP transport requires TLS. Set `HAY_TRUST_PROXY=true` only if the Node service cannot be reached except through its trusted reverse proxy; otherwise clients could forge the IP used for rate limits. The static website and docs can be hosted separately. These Workers are not a Spacefast Functions bundle; that runtime has a different database integration. ## Marketplace and personal-registry rollout The marketplace assets live in `hay-server/marketplace/`. `npm run build:marketplace` generates the server asset module; start, test and Worker build/deploy hooks run it automatically. Serve the same shell for `/` and `/plugins/`, with static assets at `/marketplace/`. Discovery stays at `/.well-known/hay-registry.json`. Apply additive D1 migrations `0003_personal_registries.sql` and `0004_marketplace_search.sql` before deploying these routes. The first separates private release bytes and metadata; the second backfills current public search metadata and numeric latest-version selection. Keep existing accounts and immutable release history intact; back up and verify the target before applying migrations. Verify search and details independently of the release-catalog API: browse returns 24 latest plugins per page, details 50 numeric-newest releases. Test out-of-order publication, literal 120-character search and base-relative pagination. Private discovery, pages, release metadata and bytes must require the exact owner’s valid session. Verify 401 for missing/revoked sessions, 404 for another owner, and `private, no-store` plus `Vary: Authorization` on every private response, including errors. Confirm private data is absent from public search/catalog/artifact routes, and tokens never enter public/custom requests or install URLs. Test the user-initiated link handoff separately: overview in Hay, **Review installation**, then inline approval with the pinned digest and unchanged saved registry selection. Browser rendering alone does not establish native protocol registration or installation. On 12 November 2026, migrations `0003_personal_registries.sql` and `0004_marketplace_search.sql` were applied and the marketplace/private-registry Worker deployed. Public HTML, search, details, exact release, discovery, legacy catalog and unauthenticated account checks passed. Private paths rejected unauthenticated requests with HTTP 401 and the required private cache headers. Rebuilt packaged Hay used its existing signed-in account to load the live personal registry successfully. That catalog was empty; the public registry selection was restored afterward. In Hay’s built-in browser, the live marketplace loaded, a Documents search opened its details, and **Install in Hay** opened the native overview. **Review installation** reached the inline installation request with the expected release digest. The demonstration was declined and dismissed, leaving the existing workspace unchanged. Live docs also loaded in the built-in browser. A separate isolated native end-to-end check completed public and private publication, wrong-digest rejection, browser-link handoff, inline approval, empty-record/no-grant installation, session persistence across restart and revocation. PostgreSQL and D1 checks covered owner isolation. A subsequent live check used Hay itself to publish a code-only private release and install it, after inline approval, in a newly created workspace. The installed version, size and SHA-256 matched the reviewed bundle. The recipient began with empty plugin records, documents and sources, no tasks and no granted permissions; source-workspace data stayed intact. The private release was absent from public search, release lookup, plugin details and artifact routes. A distinct, email-verified Hay account was checked with a valid server session before probing the owner’s private discovery, catalog, exact release and artifact endpoints. All four returned 404 with private/no-store and Authorization-varying headers. Anonymous probes returned 401. The second account’s native Hay UI also refused the foreign registry; the client withheld its credential from that other owner’s source. Signing the test account out revoked its session, and a subsequent authenticated probe returned 401. The registry owner remained signed in. The marketplace serves a docs-only snapshot from the canonical `hay-website` checkout at [plugins.hay.so/docs/](https://plugins.hay.so/docs/), configured with `HAY_DOCS_DIR` for another checkout. The exporter emits only documentation and its dependencies under the server’s ignored `dist-marketplace/docs`; production builds require it. Serve local `/docs/`, raw Markdown, SDK references, search and `/llms*.txt` rather than relying on an unavailable external documentation host. Building the snapshot does not deploy `hay.so` or change its DNS. Production HTTP checks passed for the docs overview, marketplace/personal-registry/publishing/registry guides, schemas, agent text files and WOFF2 fonts. Recheck those paths after each deployment. ## Unified web and app account rollout The implemented account pages are `/sign-in`, `/continue`, `/account` and the restricted `/session-frame` on the new default `app.hay.so` origin. Assets live in `hay-server/account/` and are included by the marketplace build. The marketplace receives limited profile status through the account frame, while native Hay provides a small profile control with **Account settings**, **Manage on web** and **Sign out of Hay**. Apply additive D1 migration `0005_unified_login.sql` before deploying this protocol. It adds expiring, single-use sign-in/handoff records without replacing existing account or publication tables. Keep PKCE verifiers in the native process and store only hashes for callback codes and browser bindings. Never place a bearer session in JavaScript, a domain-wide cookie, browser storage or a callback URL. Use host-only Secure/HttpOnly/SameSite=Lax cookies on the account origin, with private/no-store responses and Cookie variation. Account HTML uses `Referrer-Policy: same-origin`; account API JSON uses `Referrer-Policy: no-referrer`. Validate Origin on mutations and restrict redirects to the configured first-party paths. Permit the configured registry in account-page `form-action` so the sign-out POST’s validated 303 return is not blocked; do not broaden the backend return policy. Frame only the session page for the exact registry parent, and verify message source and origin in both directions. Backend PostgreSQL/D1 checks cover proof-plus-code exchange, explicit reapproval/code rotation, no-code pending responses, expiry, cancellation, replay/races, approving-session revocation, browser binding, cookie isolation and separate web/app revocation. Synthetic-browser checks cover email-code fallback, resend/expiry, explicit approval and Open Hay, account handoff/URL cleanup, profile messages, sign-out returns and mobile layouts. These checks do not establish delivered email or the new unified protocol in production. Earlier live account and marketplace evidence above remains scoped to the flows it exercised. ## Optional signup profile setup The shared email flow signs in or creates the account only after valid code verification; code-send alone creates no user. The implemented **Choose your handle** offer is optional, accepts **Display name** and **Handle**, and preserves the original app request or return page on **Save profile** and **Do this later**. Existing complete profiles skip the offer; existing profileless app sessions remain usable. Native direct-code sign-in offers it once, and the browser path does not immediately repeat it natively. Authenticated `/web/handles` and `/v1/handles` checks are advisory and read-only. Profile save reuses the personal publisher principal and namespace. Web saves require matching `expectedUserId` and the exact Origin; native saves guard the expected user in the host. Cookies/tokens and publication approvals keep their existing boundaries. Test account-switch races, handle conflicts, invalid/reserved handles, skip continuation and reused profiles; do not make availability into a reservation or account setup into automatic publication. The onboarding changes passed 27 isolated PostgreSQL/D1 checks. This is implementation/test evidence; no new production onboarding verification is recorded here. Earlier account, migration and publication live evidence remains scoped to the flows it exercised. ## Moving the default account origin Use `https://app.hay.so` for new account UI, browser-session frames and native API configuration. Keep `https://accounts.hay.so` served for legacy clients and existing logins. Do not replace legacy bearer routes with redirects: handle them directly so credentials are never forwarded as part of an origin hop. Preserve the shared account database, authentication secret, account IDs and valid sessions. Native migration is narrowly scoped to an encrypted session saved for exactly the old production account origin when the new configuration is exactly the new production origin. Try canonical first. Only a request carrying that retained credential may retry its original HTTPS origin on a fetch DNS cause of `ENOTFOUND` or `EAI_AGAIN`, before the canonical origin has responded. Fresh email-code login, custom servers, HTTP responses, certificate errors and other failures do not trigger fallback. Preserve the encrypted legacy origin while fallback is in use; after successful canonical validation, resave it as the new origin. A canonical response disables fallback for the running client. Unrelated origins receive no migration exception. A validated legacy **Manage on web** URL may temporarily serve these migrated logins. Host-only browser cookies cannot be copied across origins or widened to `.hay.so`. Carry a valid legacy web session with a short-lived one-use, browser-bound handoff and explicit identity confirmation; the new host then sets its own HTTP-only cookie. Preserve any validated pending app sign-in destination. Both migrated cookies retain the same web-session revocation boundary; do not create an extra browser login that survives legacy web sign-out. Native sessions remain independent. Keep session tokens out of JavaScript, URLs and storage. A valid existing session does not require another email code solely for the move. On 13 November 2026, the Worker was deployed for `app.hay.so`, the legacy account host and the public registry without schema or authentication-secret changes. The live HTTP assertions passed using the new host’s published A record with normal TLS hostname/certificate validation. Those routing checks are separate from the authenticated carry observations below. The completed native regression suite passed 248 tests across 30 files, plus lint, typecheck and build. Three isolated Electron journeys against a real service passed, including host-only-cookie carry between IPv4 and IPv6 hosts while preserving a pending native sign-in request. Backend PostgreSQL/D1 checks also passed. These cover the scoped fallback and migration contract; they are separate from live authenticated carry. After deployment, reloading the owner’s existing Chrome account page on the legacy host retained the signed-in identity without another email code, sign-out or data change. This is observed legacy-browser retention, not new-host carry. After the user approved macOS Keychain access, rebuilt installed Hay reopened with the same signed-in account and workspace. Its profile’s **Manage on web** opened an authenticated legacy-origin handoff for the same identity. This verifies live native credential retention and the narrowly scoped DNS fallback; no new email code, logout or workspace-data change was needed. Chrome subsequently resolved `app.hay.so`. Visiting its account page carried the existing legacy browser login to a one-use new-host confirmation. After explicit **Continue as …**, the account and publisher appeared at `app.hay.so/account?legacy=checked` without another email/code or sign-out. Production browser carry is verified. Canonical native fallback retirement remains covered by regression tests, not yet observed live. Normal Node/macOS system-lookup recovery is also pending; neither limitation reverses the observed native retention or Chrome browser carry. ## Operator DNS diagnostics Earlier packaged-app/browser checks reached the established legacy account and registry hosts with normal DNS. At the start of the `app.hay.so` rollout, the configured local upstream resolver cached a precreation NXDOMAIN while public resolution and the published A record were healthy. Chrome and direct A-record queries later resolved the new host, allowing the authenticated carry above; normal Node/macOS `getaddrinfo` still returned `ENOTFOUND`. The full live HTTP check passed when the verification harness temporarily directed only its own hostname lookup to the published address, preserving normal TLS hostname validation. The normal system-lookup version remained DNS-blocked. No system or application DNS setting was changed. Inspect the affected lookup path and repeat normal-system-DNS checks once it recovers; do not ship a fixed-IP override, disable certificate validation or broaden the native legacy-credential fallback. ## Verification after deployment changes 1. Verify migrations and process routing; a `/health` response alone does not prove email delivery or release persistence. 2. Receive a real sign-in code and check successful, wrong, expired and replayed code cases. 3. Sign out and verify that the revoked session cannot read `/v1/account`. 4. Create a personal publisher and publish a harmless code-only release. 5. Discover the registry from its base address, follow catalog pages and directly look up a release. Verify the artifact’s exact size and digest, nested-prefix behavior, static fallback and absence of account credentials. 6. Reject another account’s namespace claim and a different payload at the same version. 7. Restart or redeploy and verify account, ownership, and release persistence. 8. Check that conflicts do not leave partial publications, legacy-index capacity errors do not discard old releases, and codes, tokens and connection strings are absent from logs and public files. ## Local development and scope The Node development outbox writes verification messages into a private local directory. It requires a loopback-only server and is refused in production. Keep `data/` ignored; never expose those code files or call them delivered email. Use dedicated disposable databases for checks. Local D1 tests exercise the authentication service, ownership, conflict rollback, pagination and legacy-index compatibility limits; they do not establish a deployed Worker or production email delivery. The current public flow supports personal accounts and publishers. Organization ownership is anticipated in the schema, but organization creation, invitations, shared administration and moderation controls are not public app or API features yet. --- Source: https://hay.so/docs/agents.md # Build with an agent Read this page before generating a plugin. Use the linked references as contracts; never infer additional authority from a package, document, website, or model result. ## Choose the right deliverable - For a local tool: create a source folder with `manifest.json` and self-contained `index.html`. - For direct sharing: build a `.hay-plugin.json` bundle or use Hay’s code-only export. - For discovery and updates: publish a registry base address with discovery, immutable artifacts, and a static `hay.registry/1` index or paged catalog. - For direct publication: use the signed-in account’s owned personal publisher and the reviewed release workflow. A working service deployment is required; local export is not publication. ## Accounts and publication authority Hay requires a global account sign-in before app use. The account session belongs to the trusted host, outside workspace files. It is not available through the plugin SDK, and a model provider account is a separate connection. Let the user enter email verification codes in the trusted app or account-host page. Do not request email codes, app callback codes, PKCE verifiers, account handoffs or bearer sessions in agent context, copy them into plugin storage, or forward them to another browser or custom registry. An existing browser session still requires explicit **Continue to Hay** and **Open Hay**; never approve or launch a sign-in callback automatically. The trusted host sends account credentials only to the configured account/API origin and its own exact personal-registry base, after online session validation. Never attach them to custom registries, public marketplace requests, URLs or redirects. A release ID must use the account’s owned personal handle. Review the explicit audience and destination as well as the remapped ID, version, author, code, requested capabilities and exact digest. Public and private histories are independent; a private path is an identifier, never a sharing grant. Do not claim success until a matching publication receipt is returned. An uncertain network response may follow a successful server write; refresh publications before retrying and never change released bytes under the same version. Web and native sessions are separate. **Manage on web** creates a two-minute, single-use handoff bound to the first browser and requiring explicit identity confirmation; app sign-out and web sign-out revoke only their respective sessions. The account-host HTTP-only cookie stays on that host. The marketplace’s account frame relays sanitized profile status to its configured parent only, and the parent verifies both origin and source. Never derive a bearer token from web state or introduce a domain-wide cookie. Read [Hay accounts](accounts/), [publish to Hay](publishing/), and the [publisher API](publisher-api/) for the current contract. Organization publishing is not exposed yet. ## Account creation and optional handles One email flow signs in existing users or creates a new account only after verification. Do not describe requesting a code as account creation, add a separate password signup, or select a handle without the user’s choice. After verification, **Choose your handle** offers **Display name**, **Handle**, **Save profile** and **Do this later**. Explain that both values are public, the handle prefixes release IDs and cannot be changed yet. The entire step is optional for using Hay. Preserve the pending app request or validated web return when saving/skipping; do not autoapprove the following **Continue to Hay**. Existing profiles are reused and profileless restored app sessions are not blocked. Setup remains available in native account settings and the web account page. Use authenticated availability only; an available result is not a reservation. Final save is authoritative and may conflict. Web `/web/profile` includes the expected current user ID and returns only sanitized status; never collect a bearer token or fabricate that identity. The native host also guards the expected user before calling its existing profile endpoint. Profile creation is not plugin publication and grants no capabilities. Public/private releases retain separate explicit review. ## Account-origin migration Use `https://app.hay.so` for new account UI/API requests. Legacy `https://accounts.hay.so` clients and valid sessions remain supported; do not redirect a bearer API request between them. The encrypted native-session exception is exact old-production-origin to exact new-production-origin only. The host tries canonical first and may retry the original HTTPS origin only for the retained credential on `ENOTFOUND`/`EAI_AGAIN`, before a canonical response. No fresh email-code login, custom server, HTTP/certificate error or other failure can use fallback. Keep the encrypted saved origin old until successful canonical validation, then rewrite it; do not pin IPs or disable TLS. A validated legacy **Manage on web** URL is allowed only for this migrated-login scope. Never generalize it to arbitrary hosts or copy a credential into agent context. Browser continuity uses an explicit identity confirmation and a short-lived, one-use handoff bound to the receiving browser. A valid legacy session should not need another email code solely for the move. Migrated cookies share web-session revocation across the old/new hosts; preserve any validated pending app sign-in destination. Native sessions remain independent. Cookies remain host-only and HTTP-only; no domain-wide cookie, browser-storage token or raw bearer in a URL is permitted. Account HTML referrers are same-origin; account API JSON remains no-referrer. The operator record now verifies live installed-app credential retention and scoped legacy DNS fallback, plus Chrome’s authenticated carry to the new host with explicit identity confirmation and no new inbox code. Canonical native fallback retirement is still test-covered only, and normal Node/macOS lookup recovery remains pending. Do not infer those remaining observations from the verified carry or published-address HTTPS checks. ## Marketplace and private-registry actions For public discovery, use the [marketplace APIs](publisher-api/) instead of scanning every release. Search is literal and case-insensitive, up to 120 characters; browse pages contain up to 24 latest plugins and detail pages up to 50 releases in numeric-newest order. Follow returned `next` paths relative to the registry base. A `hay://install` link is a proposal for an exact registry, ID, version and digest. Offer it only as a user-initiated action. Opening it neither installs nor grants access: the user sees an overview, chooses **Review installation** and approves inline. Do not invoke the protocol automatically or silently change registry selection. Use the account-returned `personalRegistryURL` for private releases; do not manufacture it from an email or ask for a token. Private discovery, pages, exact releases and bytes all require the same owner’s valid session. A different owner gets 404, a missing/revoked session 401. Public APIs must not contain private metadata or bytes. The current scope has no private sharing, organization access, promotion or deletion API. Read [personal registries](personal-registries/). ## Free-plan service operations The current Hay deployment keeps Workers Free with D1 and the Resend REST sender. Respect that plan choice; native Cloudflare `EMAIL` sending to public recipients requires a deliberate paid-plan change, not an automatic fallback. For sending-domain setup, use only the exact records generated by the provider, keep verification CNAMEs DNS-only, and preserve existing apex mail MX/SPF. Never copy an API key into a prompt or plugin; keep `RESEND_API_KEY` in the server’s secret store. A healthy account URL, a readable catalog or a provider receipt does not prove that an inbox received a code. Verify the real sign-in and publication flow before describing the public service as ready. The [operator verification record](account-service/) documents live packaged sign-in, the global publisher display and authenticated release history, alongside separately verified production publication and host download/install checks. The full native publish click-through previously ran against a local service. The live marketplace link reached inline installation review and was declined; authenticated personal-registry reading loaded an empty catalog. Full approved public/private publication and installation passed isolated checks; the operator record separately preserves subsequent live publication and owner-isolation evidence. Preserve that evidence boundary when reporting results. ## Implementation checklist 1. Choose a stable plugin ID and a three-component numeric version. 2. Declare only capabilities actually used. 3. Keep HTML, scripts and styles inline; do not add remote SDK scripts or arbitrary fetch calls. 4. Await `Hay.ready`; use the public SDK, not the desktop preload. 5. Render returned data as text and preserve unsaved input across context events. 6. Implement empty, loading, revoked-permission, declined-review and tool-error states. 7. Use real returned IDs, status, revisions and observations; never fabricate completion or evidence. 8. Test inside Hay, including restart, permission revocation, keyboard use and narrow layouts. 9. Build and validate the bundle before preparing a registry. 10. Review exactly what will be shared; exclude live workspace records and secrets. ## Source of truth The [SDK overview](sdk/), [capability table](capabilities/), [package format](manifest/), [coding guide](coding/), [browser guide](browser/), and [registry contract](registry-reference/) define the supported surface. The app source repository contains the current [TypeScript contract](https://github.com/use-hay/app/blob/main/packages/sdk/index.ts) and validators. `/llms.txt` indexes canonical Markdown pages; `/llms-full.txt` is a single-file copy of the docs. Prefer a page’s Markdown link when reading with an agent. The site has no hidden agent-only execution authority. ## Verification and handoff A plain web preview can verify a plugin’s layout with explicit fixtures. It cannot prove host storage, permissions, model routing, native coding, or browser isolation. Run integration checks in Hay and describe which behavior was actually exercised. For coding jobs, keep the approved file and command scope, wait asynchronously, test built web output in the browser after verification, and present a concise overview. The user reviews the exact diff in the side panel before commit. Do not autoapprove a request or invent a successful commit. For registries, connect the base address and discover `.well-known/hay-registry.json`. Resolve catalog, release, pagination and artifact paths against that base; omit credentials and refuse redirects. Only discovery 404/410 allows legacy `index.json` fallback. Verify exact bytes, SHA-256, file size, same-origin paths, manifest equality, retained releases and fresh-workspace installation. The current client bounds browsing at 5,000 releases, 100 pages, 20,000,000 catalog bytes and 30 seconds; direct release lookup avoids the catalog walk, not validation or approval. Do not describe exported files as uploaded, or a self-reported author as verified.