@hitch42/applet 0.1.0-beta.3 → 0.1.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/_internal/server.d.ts +14 -1
  2. package/dist/_internal/server.js +23 -74
  3. package/dist/applet.js +53 -29
  4. package/dist/build-BfbTbt_r.js +2 -0
  5. package/dist/{build-Ch3pALLA.js → build-CAb26kMY.js} +30 -14
  6. package/dist/ci-5qWl7lT3.js +68 -0
  7. package/dist/client.d.ts +3 -3
  8. package/dist/{contract-DMrpbaLJ.d.ts → contract-rP_eV9PK.d.ts} +1 -1
  9. package/dist/{deploy-DP2Bg6nt.js → deploy-D_OFJ6-Q.js} +193 -30
  10. package/dist/email-DBTVhNB_.js +62 -0
  11. package/dist/generate-BgepN4-b.js +2 -0
  12. package/dist/{generate-BuszZ9-U.js → generate-CBPnHoqX.js} +40 -30
  13. package/dist/index.d.ts +122 -11
  14. package/dist/index.js +141 -26
  15. package/dist/{init-BjWWDqqX.js → init-tfzwuYnk.js} +12 -8
  16. package/dist/load-config-BYSxDVAl.js +27 -0
  17. package/dist/login-C7RKmmTe.js +12 -0
  18. package/dist/{logs-m00pzr_O.js → logs-DgjgAxj2.js} +3 -3
  19. package/dist/{manifest-DyoJ1MIL.js → manifest-Bp87Uz0a.js} +6 -0
  20. package/dist/{manifest-BQCAxATS.d.ts → manifest-DD_SGkS6.d.ts} +5 -0
  21. package/dist/me-BW7Paary.js +32 -0
  22. package/dist/{project-MN3mFxeE.js → project-D6XHllmC.js} +26 -20
  23. package/dist/prompt-ChS-grYO.js +29 -0
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +40 -42
  26. package/dist/schedules-dhU1yyFr.js +42 -0
  27. package/dist/server.d.ts +3 -3
  28. package/dist/telephony-protocol-D-nf3Gca.d.ts +76 -0
  29. package/dist/telephony.d.ts +77 -0
  30. package/dist/telephony.js +607 -0
  31. package/dist/{upload-YbRKEqR6.d.ts → upload-DKZzz5kW.d.ts} +6 -6
  32. package/dist/{variables-CP7Ce3Np.js → variables-Bwnr7oxf.js} +3 -28
  33. package/dist/vite.d.ts +1 -1
  34. package/dist/vite.js +458 -27
  35. package/dist/workflow-engine-MogejNfF.js +117 -0
  36. package/dist/{auth-DaVK0k5R.js → workspace-XVtM12ZC.js} +44 -4
  37. package/package.json +6 -1
  38. package/scaffold/.agents/skills/hitch-applets/SKILL.md +34 -15
  39. package/scaffold/.agents/skills/hitch-applets/references/email-tracking.md +49 -0
  40. package/scaffold/.agents/skills/hitch-applets/references/files.md +3 -3
  41. package/scaffold/.agents/skills/hitch-applets/references/manifest.md +21 -2
  42. package/scaffold/.agents/skills/hitch-applets/references/performance.md +74 -0
  43. package/scaffold/.agents/skills/hitch-applets/references/records.md +30 -8
  44. package/scaffold/.agents/skills/hitch-applets/references/schedules.md +7 -2
  45. package/scaffold/.agents/skills/hitch-applets/references/server.md +0 -3
  46. package/scaffold/.agents/skills/hitch-applets/references/workflows.md +11 -3
  47. package/scaffold/.agents/skills/hitch-cli/SKILL.md +129 -0
  48. package/scaffold/.agents/skills/hitch-data/SKILL.md +51 -0
  49. package/scaffold/.agents/skills/hitch-data/references/attributes.md +121 -0
  50. package/scaffold/.agents/skills/hitch-data/references/limits.md +64 -0
  51. package/scaffold/.agents/skills/hitch-theory/SKILL.md +150 -0
  52. package/scaffold/.github/workflows/hitch.yml +1 -7
  53. package/dist/build-BqoQ-uNz.js +0 -2
  54. package/dist/generate-CRVA0Ll5.js +0 -2
  55. package/dist/load-config-BnxocLAQ.js +0 -56
@@ -1,5 +1,7 @@
1
+ import { createRequire } from "node:module";
2
+ import { existsSync } from "node:fs";
1
3
  import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
4
+ import { basename, dirname, join, resolve } from "node:path";
3
5
  import { spawn } from "node:child_process";
4
6
  import { homedir } from "node:os";
5
7
  //#region ../shared/auth/src/store.mts
@@ -326,6 +328,33 @@ async function loginWithDevice(instanceUrl, options = {}) {
326
328
  };
327
329
  }
328
330
  //#endregion
331
+ //#region ../shared/auth/src/workspace.mts
332
+ const ORG_CODE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
333
+ const require = createRequire(import.meta.url);
334
+ function readWorkspaceConfig(dir) {
335
+ const path = resolve(dir, "hitch.config.ts");
336
+ if (!existsSync(path)) return void 0;
337
+ let module;
338
+ try {
339
+ module = require(path);
340
+ } catch (error) {
341
+ throw new Error(`Failed to load ${path}.`, { cause: error });
342
+ }
343
+ const config = module !== null && typeof module === "object" && "default" in module ? module.default : module;
344
+ if (config === null || typeof config !== "object" || !("sdk" in config) || typeof config.sdk !== "string" || config.sdk.length === 0 || !("org" in config) || typeof config.org !== "string" || !ORG_CODE_PATTERN.test(config.org)) throw new Error(`Invalid Hitch config in ${path}: "sdk" must be a package spec and "org" an organization code.`);
345
+ const githubWorkflow = "githubWorkflow" in config ? config.githubWorkflow : void 0;
346
+ if (githubWorkflow !== void 0 && typeof githubWorkflow !== "boolean") throw new Error(`Invalid Hitch config in ${path}: "githubWorkflow" must be a boolean.`);
347
+ return {
348
+ sdk: config.sdk,
349
+ org: config.org,
350
+ githubWorkflow
351
+ };
352
+ }
353
+ function findWorkspaceConfig(cwd) {
354
+ const appletsDir = dirname(cwd);
355
+ return readWorkspaceConfig(cwd) ?? (basename(appletsDir) === "applets" ? readWorkspaceConfig(dirname(appletsDir)) : void 0);
356
+ }
357
+ //#endregion
329
358
  //#region src/auth.ts
330
359
  async function fetchWithOrgAuth(orgCode, options, request) {
331
360
  const fetcher = options.fetch ?? globalThis.fetch;
@@ -334,9 +363,10 @@ async function fetchWithOrgAuth(orgCode, options, request) {
334
363
  const auth = await resolveAuthForOrg(instanceUrl, orgCode, options.store);
335
364
  if (auth !== void 0) {
336
365
  const response = await request(organizationUrl, bearerToken(auth));
337
- if (response.status !== 401 && response.status !== 403) return response;
366
+ rejectEnvironmentKey(response, auth);
367
+ if (response.status !== 401) return response;
338
368
  }
339
- if (!(options.stdinIsTTY ?? process.stdin.isTTY === true)) throw new Error("not logged in — set HITCH_API_KEY or run in a terminal to log in");
369
+ if (!(options.stdinIsTTY ?? process.stdin.isTTY === true)) throw new AuthenticationError("Not logged in. Run applet login, or set HITCH_API_KEY.");
340
370
  await (options.login ?? loginWithDevice)(instanceUrl, {
341
371
  expectedOrg: orgCode,
342
372
  fetch: fetcher,
@@ -347,4 +377,14 @@ async function fetchWithOrgAuth(orgCode, options, request) {
347
377
  return request(organizationUrl, bearerToken(credentials));
348
378
  }
349
379
  //#endregion
350
- export { resolveInstanceUrl as i, organizationInstanceUrl as n, resolveAuthForOrg as r, fetchWithOrgAuth as t };
380
+ //#region src/workspace.ts
381
+ function resolveWorkspaceOrg(appletRoot) {
382
+ const appletsDir = dirname(appletRoot);
383
+ if (basename(appletsDir) === "applets") {
384
+ const config = readWorkspaceConfig(dirname(appletsDir));
385
+ if (config !== void 0) return config.org;
386
+ }
387
+ throw new Error(`No Hitch workspace found for ${appletRoot}. An applet must live at <workspace>/applets/<code> with hitch.config.ts in the workspace root.`);
388
+ }
389
+ //#endregion
390
+ export { loginWithDevice as a, organizationInstanceUrl as c, resolveInstanceUrl as d, readWorkspaceConfig as i, rejectEnvironmentKey as l, fetchWithOrgAuth as n, AuthenticationError as o, findWorkspaceConfig as r, bearerToken as s, resolveWorkspaceOrg as t, resolveAuthForOrg as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hitch42/applet",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.9",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "applet": "./dist/applet.js"
@@ -27,6 +27,10 @@
27
27
  "source": "./src/react/index.ts",
28
28
  "default": "./dist/react.js"
29
29
  },
30
+ "./telephony": {
31
+ "source": "./src/telephony/index.ts",
32
+ "default": "./dist/telephony.js"
33
+ },
30
34
  "./vite": {
31
35
  "source": "./src/vite.ts",
32
36
  "default": "./dist/vite.js"
@@ -53,6 +57,7 @@
53
57
  "@tanstack/react-query-persist-client": "5.101.4",
54
58
  "@tanstack/router-cli": "^1.167.24",
55
59
  "@tanstack/router-plugin": "^1.168.23",
60
+ "@telnyx/webrtc": "^2.27.10",
56
61
  "@tiptap/core": "^3.30.3",
57
62
  "@tiptap/extension-image": "^3.30.3",
58
63
  "@tiptap/extension-list": "^3.30.3",
@@ -13,21 +13,27 @@ Treat each applet as an isolated microfrontend. Keep it separate from the data s
13
13
 
14
14
  This file holds the common rules. Read each matching reference before you edit an applet. Paths are relative to this file.
15
15
 
16
- | Read this | When the task involves |
17
- | ------------------------- | -------------------------------------------------------------------------------------------------------- |
18
- | `references/client.md` | Client routes, navigation, page layout, forms, controls, or other user interface work |
19
- | `references/records.md` | Listing, querying, creating, or updating records; filters, includes, aggregates; a missing record method |
20
- | `references/files.md` | Uploading or reading files, attachment attributes, download links |
21
- | `references/markdown.md` | Rendering or editing markdown, image uploads, Hitch file links |
22
- | `references/server.md` | The applet server: secrets, env variables, external services, the `api` client |
23
- | `references/schedules.md` | Cron schedules and background runs |
24
- | `references/workflows.md` | Multi-step server work: `createWorkflow`, `step.do`, `step.sleep`, starting, reading, and listing runs |
25
- | `references/manifest.md` | The applet definition in `applet.manifest.ts`: objects, access, variables, and packages |
16
+ | Read this | When the task involves |
17
+ | ------------------------------ | -------------------------------------------------------------------------------------------------------- |
18
+ | `references/client.md` | Client routes, navigation, page layout, forms, controls, or other user interface work |
19
+ | `references/records.md` | Listing, querying, creating, or updating records; filters, includes, aggregates; a missing record method |
20
+ | `references/files.md` | Uploading or reading files, attachment attributes, download links |
21
+ | `references/email-tracking.md` | Creating pixels and reading raw email hits |
22
+ | `references/markdown.md` | Rendering or editing markdown, image uploads, Hitch file links |
23
+ | `references/server.md` | The applet server: secrets, env variables, external services, the `api` client |
24
+ | `references/schedules.md` | Cron schedules and background runs |
25
+ | `references/workflows.md` | Multi-step server work: `createWorkflow`, `step.do`, `step.sleep`, starting, reading, and listing runs |
26
+ | `references/manifest.md` | The applet definition in `applet.manifest.ts`: objects, access, variables, and packages |
27
+ | `references/performance.md` | Data access design: client versus server, N+1 reads, includes, aggregates, pagination, batch writes |
28
+
29
+ For attribute types, filters, and API limits, read `../hitch-data/SKILL.md`. To inspect or change Hitch data and settings from the shell, read `../hitch-cli/SKILL.md`.
26
30
 
27
31
  ## Work with the workspace
28
32
 
29
33
  - Run package commands with `pnpm`.
30
- - If `package.json` is absent, read `sdk` from `hitch.config.ts`. Run `pnpm dlx <sdk> bootstrap` from the workspace root.
34
+ - Run `pnpm exec applet login` from the workspace root or an applet directory to sign in. It uses the organization in `hitch.config.ts` and shares credentials with `hitch login`. Add `--no-browser` to print the sign-in URL without opening a browser.
35
+ - A server permission error is not a login request.
36
+ - After a workspace is linked to a repository, deploy in two ways. Run `pnpm exec applet deploy` to release a change immediately. Push to the production branch to deploy through CI and keep releases in sync with the repository.
31
37
  - Run `pnpm exec applet new <code>` from the workspace root to create a full applet.
32
38
  - Add `--client-only` or `--server-only` when the applet needs only one part.
33
39
  - Use `--client-only` for a user interface that uses the current user's Hitch access and needs no server.
@@ -41,16 +47,21 @@ This file holds the common rules. Read each matching reference before you edit a
41
47
  - Run `pnpm --dir applets/<code> run generate` after a Hitch object, operation, or theme changes.
42
48
  - Run generation at the start of a session when the task depends on current Hitch types.
43
49
  - Run `pnpm --dir applets/<code> run dev` and `pnpm --dir applets/<code> run build` from the workspace root.
50
+ - The dev server runs as you in the production org. Reads and writes use real data.
51
+ - The dev server prints a preview link. Open the link to see the local applet inside Hitch.
52
+ - Chrome asks once to allow local network access for the Hitch site.
53
+ - The preview lasts until you select Exit or close the tab.
54
+ - A pending `requiresChangeset` schema is not available in dev. Deploy to test it.
44
55
  - Run `pnpm --dir applets/<code> run deploy` only when the user requests a deployment.
45
- - Edit only `hitch.config.ts`, `applet.manifest.ts`, and files in `src`.
46
- - Treat all other workspace files as generated output. The applet CLI replaces them.
56
+ - Edit only `hitch.config.ts`, `applet.manifest.ts`, files in `src`, and `.gitignore` rules below the generated block.
57
+ - Treat all other workspace files as generated output. The applet CLI replaces them. It creates `.github/workflows/hitch.yml` only when the file is absent. To stop it, set `githubWorkflow: false` in `hitch.config.ts` and delete the file.
47
58
  - Remember that generation refreshes the local theme copy. Deployed applets load the public org theme from its Hitch `.com` URL.
48
59
 
49
60
  ## Use Vite and Vitest
50
61
 
51
62
  - Import configuration APIs from `vite`.
52
63
  - Import test APIs from `vitest`.
53
- - Keep Vite configuration in the generated `vite.config.ts` file.
64
+ - Do not edit `vite.config.ts`. The applet CLI rewrites it. Do not add another Vite config.
54
65
  - Add applet packages to `dependencies` or `devDependencies` in `applet.manifest.ts`.
55
66
 
56
67
  ## Access Hitch data
@@ -118,7 +129,7 @@ Read `references/files.md` before download links, attachment attributes, or serv
118
129
  - Add useful `console.<level>` calls around important server work and failures. Deployed logs often provide the clearest evidence for applet issues.
119
130
  - Choose `console.log`, `console.info`, `console.warn`, or `console.error` deliberately. The console method sets the entry level used by log filters.
120
131
  - Run `pnpm --dir applets/<code> exec applet logs --filter '{"type":"applet"}'` from the workspace root to inspect applet code logs.
121
- - Include the applet type filter during diagnosis. Without it, Cloudflare invocation entries can hide the useful applet entries.
132
+ - Include the applet type filter during diagnosis. Without it, Cloudflare invocation entries can hide the useful applet entries. For a workflow run, use `--run <id>` or `--filter '{"type":"workflow"}'` instead.
122
133
  - Do not log secrets, tokens, or personal data.
123
134
 
124
135
  ## Verify changes
@@ -135,3 +146,11 @@ pnpm --dir applets/<code> run build
135
146
  If missing packages block a check, stop package checks. Do not retry the check through another binary.
136
147
 
137
148
  If a command fails, report the exact blocker. Do not deploy as a verification step.
149
+
150
+ ## Deploy from GitHub
151
+
152
+ - A push to the production branch runs `applet deploy` in the workspace root through GitHub Actions.
153
+ - An applet deploys when its own folder changed since its current CI release. Every applet deploys when a file outside `applets/` changed. An applet without a CI release always deploys. An applet also deploys when CI cannot load the commit of its current release.
154
+ - The deploy skips other applets and logs the reason. A skip is expected, not a failure.
155
+ - A schema change in Hitch alone does not trigger CI.
156
+ - Do not import from a sibling applet folder. The change check does not follow such imports.
@@ -0,0 +1,49 @@
1
+ # Email tracking
2
+
3
+ Create one pixel for each email. Put its URL in the HTML before send.
4
+ Add the pixel next to CID images. A CID image cannot make a tracking request.
5
+
6
+ ```ts
7
+ const { id, url } = await hitch.email.tracking.createPixel({
8
+ tags: { campaignId: "fall", buyerId: "acme", version: "v2" },
9
+ });
10
+ const html = `<p>Hello</p><img src="${url}" width="1" height="1" alt="">`;
11
+ // Send the email with the applet's email provider.
12
+ await hitch.email.tracking.markSent({ id });
13
+ ```
14
+
15
+ `markSent({ id, sentAt })` accepts an ISO time. A later call replaces the sent time.
16
+ `get({ id })` returns one tracker. `list()` returns up to 100 trackers by default.
17
+ Use `filter`, `sentAt`, `cursor`, and `limit` to select a page.
18
+
19
+ ```ts
20
+ const tracker = await hitch.email.tracking.get({ id });
21
+ const query = {
22
+ filter: { buyerId: "acme" },
23
+ sentAt: { $gte: "2026-10-01T00:00:00Z" },
24
+ limit: 100,
25
+ };
26
+ const page = await hitch.email.tracking.list(query);
27
+ const next = page.nextCursor
28
+ ? await hitch.email.tracking.list({ ...query, cursor: page.nextCursor })
29
+ : null;
30
+ const opened =
31
+ tracker.lastHitAt !== null &&
32
+ new Date(tracker.lastHitAt).getTime() >
33
+ new Date(tracker.sentAt ?? tracker.createdAt).getTime() + 60_000;
34
+ ```
35
+
36
+ The example rule counts a tracker as opened when `lastHitAt` is more than 60 seconds after send or creation.
37
+ Hitch records `firstHitAt` and `lastHitAt`. It does not classify opens.
38
+ Each page must repeat the same filter, `sentAt`, and limit.
39
+
40
+ Tags stay fixed after creation. Use at most 10 tags. A key starts with an ASCII letter.
41
+ The rest contains ASCII letters, digits, or underscores, up to 64 characters total.
42
+ Each value has 1 to 200 characters. A filter matches every tag by exact equality.
43
+ Everyone with applet access sees all its trackers.
44
+
45
+ Gmail can fetch through `GoogleImageProxy` a few seconds after send.
46
+ A real Gmail open has the same user agent. A Gmail draft preview can fetch before send.
47
+ Apple Mail Privacy Protection fetches on delivery with `Mozilla/5.0`.
48
+ That fetch looks like a real open. Proton fetches on delivery.
49
+ Clients with images off never fetch the pixel.
@@ -67,7 +67,7 @@ Use `download` when a component needs one fresh link without query state:
67
67
 
68
68
  ```ts
69
69
  const file = await hitch.files.download(id);
70
- file.downloadUrl; // a download link, valid for one hour
70
+ file.downloadUrl; // a download link, valid for at least a day
71
71
  ```
72
72
 
73
73
  Use the `get` query utilities when React needs file metadata as query state.
@@ -86,11 +86,11 @@ if (fileQuery.data) {
86
86
  row.filename; // "report.csv"
87
87
  row.contentType; // "text/csv"
88
88
  row.sizeBytes; // 1024
89
- downloadUrl; // a download link, valid for one hour
89
+ downloadUrl; // a download link, valid for at least a day
90
90
  }
91
91
  ```
92
92
 
93
- A `downloadUrl` expires after one hour. Call `get` again for a fresh link instead of storing one.
93
+ A `downloadUrl` expires after 24 to 48 hours. Call `get` again for a fresh link instead of storing one.
94
94
 
95
95
  ## Link a file to a record
96
96
 
@@ -11,6 +11,7 @@ The applet tool reads `org` from `hitch.config.ts` in the workspace root.
11
11
  - `name` gives the applet a display name.
12
12
  - `code` gives the applet its stable code and URL prefix.
13
13
  - `objects` declares record objects and attribute access.
14
+ - `requiresChangeset` optionally names the schema changeset that the code needs.
14
15
  - `variables` optionally declares runtime variables.
15
16
  - `schedules` optionally declares scheduled server calls.
16
17
  - `dependencies` optionally declares applet runtime packages.
@@ -42,6 +43,22 @@ The manifest and applet definition reject unknown fields.
42
43
  - Expect the Vite base path to be `/<code>/`.
43
44
  - Include the code in each client route path.
44
45
 
46
+ ## Set `requiresChangeset`
47
+
48
+ Set `requiresChangeset` when the code needs a schema change that is not live yet.
49
+
50
+ ```ts
51
+ requiresChangeset: "0192a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
52
+ ```
53
+
54
+ - Use the ID of a changeset in the same organization.
55
+ - Generate, build, and deploy all use this field. The CLI has no changeset flag.
56
+ - While the changeset is pending, types and the build use the schema that the changeset will produce.
57
+ - While the changeset is pending, a deploy binds the release to the changeset. The release goes live when a person applies the changeset in Hitch.
58
+ - After the changeset applies, the field uses the live schema. A deploy then promotes as usual. Remove the field when it is convenient.
59
+ - A deploy fails when the changeset was abandoned. Remove the field or name a pending changeset.
60
+ - A git push uses the same rules. The push never applies the changeset.
61
+
45
62
  ## Declare objects
46
63
 
47
64
  Add one entry for each object that the applet needs.
@@ -75,14 +92,16 @@ Each object entry has five required fields.
75
92
 
76
93
  The object declaration caps record access for the applet identity.
77
94
 
78
- - Declare only the objects that scheduled applet work needs.
79
- - Declare only the attributes that scheduled applet work needs.
95
+ - Declare only the objects that the applet uses.
96
+ - Declare only the attributes that the applet uses.
97
+ - The declaration sets both the generated contract and the applet identity's cap.
80
98
  - Use `readOnly` for attributes that the applet identity must not change.
81
99
  - Use `readWrite` for attributes that the applet identity can change.
82
100
  - Set `canCreate` and `canDelete` to the required values.
83
101
  - The generated contract omits undeclared objects.
84
102
  - The generated contract removes `create` when `canCreate` is false.
85
103
  - The generated contract removes `delete` when `canDelete` is false.
104
+ - The generated contract removes `update` and `updateWhere` when `readWrite` is empty.
86
105
  - Regenerate after an object grant changes.
87
106
  - Do not restore a removed object or method with a cast.
88
107
 
@@ -0,0 +1,74 @@
1
+ # Performance
2
+
3
+ Design the data access of a screen or a job before you write it. Decide the number of requests, the page size, and the largest dataset the code can touch.
4
+
5
+ ## Prefer the client SDK
6
+
7
+ - Read and write from the browser with the client SDK for normal user work. It uses the current user's access and needs no server.
8
+ - Add a server procedure only when the work needs a secret, an external service, a schedule, a workflow, or fan-in of many reads into one response.
9
+ - Do not proxy a single Hitch call through the server. The proxy adds a hop and adds nothing.
10
+
11
+ ## Consolidate on the server when needed
12
+
13
+ - When a report or dashboard needs many Hitch reads, make one applet server request, not hundreds of browser requests. A server call started by a user request keeps that user's identity.
14
+ - On the server, collect the pages, join the sets, and return one shaped result.
15
+ - Cache a server result in TanStack Query with a JSON-serializable value only. Do not put `Set`, `Map`, `Date`, or class instances in the cache.
16
+ - Version the query key when the result shape changes.
17
+
18
+ ## Do not write N+1 reads
19
+
20
+ Never call `get` once per row of a list. Use one of these instead:
21
+
22
+ - **Include.** Pass `include` on the query to hydrate owner reference and user attributes in the same page. Include a reference only when each record links to a small number of targets. Include has no cap on the number of linked records it returns.
23
+ - **One page plus one related query.** Collect the IDs from the page, then run one `query` on the related object with `$in` on its reference attribute, for example `{ company_id: { $in: companyIds } }`. Hitch has no filter on record `id`. The related query is a page too, so pass its cursor when it can exceed 200 records.
24
+ - **Aggregate.** Use `aggregate` for a count, sum, average, minimum, or maximum. Group by up to two dimensions. Give each aggregate its own `filter` to get several metrics in one call. Never load records to count them in JavaScript.
25
+ - **Promise.all.** Run a known, small, independent set of requests concurrently. Do not run one request per row this way.
26
+
27
+ ## Filter in Hitch, not in the browser
28
+
29
+ - Put every filter, sort, and search term in the query body.
30
+ - A search box that filters the loaded page hides every record on later pages. That is not search.
31
+ - Ordered, text-match, and sort operations need the `indexed` flag on the attribute. Read `../../hitch-data/references/limits.md`.
32
+
33
+ ## Page every list
34
+
35
+ - Every list request returns at most 200 records. A larger `limit` is clamped to 200 with no error.
36
+ - Show a page size or a bounded scroll area in the product, with a next-page control that passes the returned `next_cursor` as `cursor`.
37
+ - Loop the cursor only when the code must visit the whole dataset. Name the bound in a comment, and stop at it.
38
+ - Do not write a whole-dataset loop for a number that `aggregate` can return.
39
+
40
+ ## Batch writes
41
+
42
+ Send many writes as one batch. A batch runs in order, in one transaction, and holds at most 100 entries.
43
+
44
+ ```ts
45
+ import { hitch } from "@hitch42/applet/server";
46
+
47
+ const companyId = crypto.randomUUID();
48
+
49
+ await hitch.batch(
50
+ (tx) => {
51
+ tx.companies.create({ body: { id: companyId, name: "Acme" } });
52
+ tx.deals.create({ body: { name: "Acme renewal", company_id: companyId } });
53
+ tx.deals.update({ params: { id: dealId }, body: { stage: "won" } });
54
+ tx.deals.delete({ params: { id: staleDealId } });
55
+ tx.deals.updateWhere({ body: { filter: { stage: "open" }, set: { stage: "lost" } } });
56
+ },
57
+ { idempotencyKey: `import-${runId}-${chunkIndex}` },
58
+ );
59
+ ```
60
+
61
+ - Call `hitch.batch(...)` on the server. Call `hitch.batch.call(...)` on the client.
62
+ - The `tx` calls buffer. Nothing sends until the callback resolves.
63
+ - A failed entry rolls back the whole batch. The error names the entry index and the entry error.
64
+ - A later entry can reference a record that an earlier entry created. Supply the record `id` as a UUID on create.
65
+ - Pass `dryRun: true` to validate a batch with no write. The response carries `dry_run: true`.
66
+ - Pass `idempotencyKey` on every batch that a retry can send twice. After a successful batch, the same key replays the stored response with `replayed: true`. The same key with a different body fails with HTTP 422. A failed batch or a dry run does not consume the key.
67
+ - Chunk a large import into batches of 100. Give each chunk its own idempotency key.
68
+ - Single `create`, `update`, and `delete` calls have no idempotency key. Use a batch when a retry must be safe.
69
+
70
+ ## Bound background work
71
+
72
+ - Process an import, sync, or backfill in batches. Save a checkpoint after each durable batch.
73
+ - Resume from the last checkpoint. Do not restart from the beginning.
74
+ - Make a skipped or failed item visible in the run summary. Never convert a failure into an empty result.
@@ -16,13 +16,14 @@ Use the generated Hitch contract as the typed data plane.
16
16
  - `list` sends `GET /v1/<objectCode>` and lists records with query parameters.
17
17
  - `create` sends `POST /v1/<objectCode>` and creates one record.
18
18
  - `get` sends `GET /v1/<objectCode>/{id}` and gets one record.
19
+ - `history` sends `GET /v1/<objectCode>/{id}/history` and gets record changes.
19
20
  - `update` sends `PATCH /v1/<objectCode>/{id}` and updates one record.
20
21
  - `delete` sends `DELETE /v1/<objectCode>/{id}` and deletes one record.
21
22
  - `query` sends `POST /v1/<objectCode>/query` and lists records with a structured body.
22
23
  - `updateWhere` sends `POST /v1/<objectCode>/update` and updates matching records.
23
24
  - `aggregate` sends `POST /v1/<objectCode>/aggregate` and calculates aggregate values.
24
25
 
25
- - Pass an object ID in `params.id` for `get`, `update`, and `delete`.
26
+ - Pass a record ID in `params.id` for `get`, `history`, `update`, and `delete`.
26
27
  - Pass record fields in `body` for `create` and `update`.
27
28
  - Pass structured query data in `body` for `query`.
28
29
 
@@ -73,6 +74,25 @@ const deal = await hitch.records.deals.get({
73
74
  - Do not use `.queryOptions(...)` or `.mutationOptions(...)` on the server.
74
75
  - Use the server SDK for imperative data access, secrets, or external services.
75
76
 
77
+ ## Read history
78
+
79
+ ```ts
80
+ const page = await hitch.records.deals.history({
81
+ params: { id },
82
+ query: { limit: 25, cursor },
83
+ });
84
+
85
+ for (const entry of page.data) {
86
+ for (const change of entry.changes) {
87
+ if (change.attributeCode === "stage") console.log(change.to);
88
+ }
89
+ }
90
+ ```
91
+
92
+ You can read record history if and only if you can read the record.
93
+ The history uses the current schema. `attributeId` tracks an attribute across renames. `attributeCode` is its current code. Older history cannot follow a rename, so some changes do not appear.
94
+ Use `next_cursor` as the next `cursor`.
95
+
76
96
  ## Build a query
77
97
 
78
98
  The `query` body accepts `filter`, `sort`, `limit`, `cursor`, and `include`.
@@ -86,7 +106,7 @@ const page = await hitch.records.deals.query({
86
106
  sort: { attribute: "amount", direction: "desc" },
87
107
  limit: 50,
88
108
  cursor,
89
- include: ["company"],
109
+ include: "company",
90
110
  },
91
111
  });
92
112
  ```
@@ -97,25 +117,26 @@ const page = await hitch.records.deals.query({
97
117
  - Use a direct field value as shorthand for `$eq`.
98
118
  - Use `$eq`, `$in`, or `$not_empty` for identity fields.
99
119
  - Use `$eq` or `$not_empty` for checkbox fields.
100
- - Use only `$not_empty` for attachment fields.
120
+ - Use `$eq`, `$in`, or `$not_empty` for attachment fields. Pass file IDs.
101
121
  - Use `$eq`, `$in`, `$not_empty`, `$contains`, `$starts_with`, or `$ends_with` for text fields.
102
122
  - Use `$eq`, `$in`, `$not_empty`, `$lt`, `$lte`, `$gt`, or `$gte` for ordered fields.
103
- - Pass `true` to `$not_empty`.
123
+ - Use `$contains`, `$starts_with`, `$ends_with`, `$lt`, `$lte`, `$gt`, `$gte`, and `sort` only on an attribute with the `indexed` flag. Read `../../hitch-data/references/limits.md`.
124
+ - Pass `true` to `$not_empty` for set values, or `false` for empty values.
104
125
  - Pass an array to `$in`.
105
126
  - Pass a string to `$contains`, `$starts_with`, and `$ends_with`.
106
127
  - Sort with `{ attribute, direction }`.
107
128
  - Use `asc` or `desc` as the sort direction.
108
- - Do not sort by multi-value, reference, user, or attachment attributes.
129
+ - Do not sort by multi-value, reference, user, attachment, or rich text attributes.
109
130
  - Omit `sort` to sort by `createdAt` in descending order.
110
131
  - Use a positive integer for `limit`.
111
132
  - The service caps `limit` at 200 and uses 50 by default.
112
- - Pass the returned pagination cursor back as `cursor`.
113
- - Pass one attribute code or an array of attribute codes as `include`.
133
+ - Pass the returned `next_cursor` back as `cursor`. A null `next_cursor` means the last page.
134
+ - Pass `include` as one string. Use the include name, which is the code without its `_id` or `_ids` suffix. Separate several names with commas.
114
135
 
115
136
  ## Aggregate records
116
137
 
117
138
  Pass `filter`, `aggregates`, and `groupBy` in the aggregate body.
118
- An aggregate or group attribute can name one reference step and a target attribute, for example `company_id.industry`.
139
+ An aggregate or group attribute can name one single-value owner reference and a target attribute, for example `company_id.industry`.
119
140
  Each aggregate can have its own `filter` with the same syntax as the top-level `filter`, including nested reference filters.
120
141
  `groupBy` accepts an array of up to two dimensions.
121
142
  The array form returns `keys` and `labels` arrays per group.
@@ -147,6 +168,7 @@ The generated contract contains only objects declared in the manifest.
147
168
 
148
169
  - The contract removes `create` when `canCreate` is false.
149
170
  - The contract removes `delete` when `canDelete` is false.
171
+ - The contract removes `update` and `updateWhere` when `readWrite` is empty.
150
172
  - Treat a missing method as a missing manifest grant, and edit the manifest when required.
151
173
  - Run generation after a manifest or Hitch schema change.
152
174
  - Do not bypass the contract with a cast.
@@ -2,6 +2,12 @@
2
2
 
3
3
  Use a schedule to call an applet server procedure from a cron expression.
4
4
 
5
+ ## Local dev
6
+
7
+ The dev server does not run cron.
8
+ Run `pnpm --dir applets/<code> exec applet schedules run <schedule>` from the workspace root while `pnpm --dir applets/<code> run dev` runs.
9
+ The run acts as you, not as the applet. The run writes real data.
10
+
5
11
  ## Define the target procedure
6
12
 
7
13
  - Add the target to the applet's server router.
@@ -44,7 +50,7 @@ schedules: [
44
50
  - The five fields are minute, hour, day of month, month, and day of week.
45
51
  - The registered router type checks the path after type generation.
46
52
  - A nested procedure uses its nested router path.
47
- - The scheduler invokes the named router procedure with no input.
53
+ - The scheduler invokes the named router procedure with the input `{}`. Accept an empty object or declare no input.
48
54
 
49
55
  ## Understand the run identity
50
56
 
@@ -82,5 +88,4 @@ const uploaded = await hitch.files.upload(file);
82
88
  - Read the stored status, path, HTTP status, and error details.
83
89
  - Check the applet logs for procedure details.
84
90
  - Follow the log commands in the main Hitch applet skill.
85
- - Include `--level log` during diagnosis.
86
91
  - Do not log secrets, tokens, or personal data.
@@ -29,9 +29,6 @@ export default { router };
29
29
  ```
30
30
 
31
31
  - Nest procedure objects when the applet needs nested paths.
32
- - Do not name a top-level procedure `hitch`.
33
- - The client reserves `hitch` for the Hitch data client.
34
- - The generated API client type rejects a router with that reserved key.
35
32
 
36
33
  ## Call the router from the client
37
34
 
@@ -2,6 +2,15 @@
2
2
 
3
3
  Use a workflow for server work that runs in steps, retries a step, or waits between steps.
4
4
 
5
+ ## Local dev
6
+
7
+ In `pnpm --dir applets/<code> run dev`, workflows run in the dev server as you. The server keeps runs in memory.
8
+ Runs disappear when the dev server stops.
9
+ In local dev, a step fails when it runs longer than 5 minutes. A deployed step can run for 15 minutes.
10
+ Set `HITCH_DEV_SKIP_SLEEP=1` in the shell that runs `pnpm --dir applets/<code> run dev` to skip sleeps and step retry delays.
11
+ A `.env` file does not set this value for the dev server.
12
+ With this value, a polling loop that sleeps between checks calls Hitch without pause.
13
+
5
14
  ## Define a workflow
6
15
 
7
16
  - Import `createWorkflow` from `@hitch42/applet/server`.
@@ -63,11 +72,10 @@ export default { router, workflows };
63
72
  - Each step has the same limits as a scheduled run: 30,000 milliseconds of CPU and 1,000 subrequests.
64
73
  - A step that still fails after its retries throws inside the workflow function. Catch it to run a compensating step. An error you do not catch fails the run.
65
74
  - A run keeps the release it started on. A new release does not stop or change a run that is in progress or asleep.
66
- - An owner or admin can terminate an open run from the applet settings page. A terminated run has the status `terminated` and does not resume.
75
+ - The owner or a member with the `applets:manage` capability can terminate an open run from the applet settings page. A terminated run has the status `terminated` and does not resume.
67
76
 
68
77
  ## Diagnose a failed run
69
78
 
70
79
  - Call `get(id)` and read `error`.
71
80
  - Workflow step output is in the applet logs with the type `workflow`, the run id, and the workflow code.
72
- - Run `applet logs <code> --run <id>` to see one run.
73
- - Include `--level log` during diagnosis.
81
+ - Run `pnpm --dir applets/<code> exec applet logs --run <id>` from the workspace root to see one run.