---
title: Deploy the account service
description: Workers, durable account storage, email delivery and explicit migrations.
group: Reference
---

# 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/<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/](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.
