hayDocs
Documentation / Reference

Deploy the account service

Workers, durable account storage, email delivery and explicit migrations.

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

CheckStatus
Historical account health check on legacy accounts.hay.soVerified HTTP 200 before the default-origin move.
New app.hay.so routing and HTTPSVerified against its published address with normal TLS hostname validation; authenticated carry is a separate check.
Registry discovery at plugins.hay.soVerified HTTP 200 and hay.registry.discovery/1.
Paged catalog and exact release lookupVerified HTTP 200, response schemas and matching release metadata.
Legacy /index.jsonVerified HTTP 200 for the current catalog.
Unauthenticated GET /v1/accountVerified HTTP 401.
Real inbox code and account sign-inVerified against the deployed service, including the rebuilt packaged Hay app.
Native global account and publisher historyVerified in Settings and Share & publish using live native credentials; existing workspace and model settings were preserved.
Owned public release and authenticated release historyVerified for the reviewed code-only release below.
Sign-out and session revocationVerified; the revoked session is rejected with HTTP 401.
Production download and installation codeVerified 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 and immutable artifact 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

SettingPurpose
DBD1 database binding for the default hosted profile.
HAY_EMAIL_MODESet resend for the Workers Free setup.
RESEND_API_KEYResend sending key, stored as a Worker secret.
EMAIL_FROMSender permitted by the verified domain; intended value sign-in@hay.so.
HAY_AUTH_SECRETAt least 32 random characters, stored as a Worker secret.
HAY_BASE_URLDefault account/API origin; intended production value https://app.hay.so.
HAY_LEGACY_ACCOUNT_URLExplicit compatibility account origin, https://accounts.hay.so, distinct from HAY_BASE_URL.
HAY_REGISTRY_ORIGINPublic 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 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 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. 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.

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 settingPurpose
DATABASE_URLDurable PostgreSQL connection string for the Node server or PostgreSQL migrations. D1 Workers do not need a PostgreSQL URL.
NODE_ENVUse production for production Node hosting.
HAY_EMAIL_MODEresend for the REST sender, smtp for Node SMTP, or binding for the optional paid Worker sender.
SMTP_HOST, EMAIL_FROMSMTP provider host and permitted sender.
SMTP_PORT, SMTP_SECUREProvider transport settings; port defaults to 587, with STARTTLS unless secure transport is selected.
SMTP_USER, SMTP_PASSWORDProvider credentials when required.
HOST, PORTNode 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/<id>, 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/, 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.

SDK 0.1 · Package and registry format v1