create-daloy 1.2.0 → 1.3.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-daloy",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/sbom.cdx.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:14e46560-a86e-5c0f-8279-490ef7794f0e",
4
+ "serialNumber": "urn:uuid:334286a9-ce6f-53b5-9ece-5e69ac3b7aac",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-08-20T11:51:47.001Z",
7
+ "timestamp": "2026-08-28T11:08:56.276Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "1.2.0"
12
+ "version": "1.3.0"
13
13
  }
14
14
  ],
15
15
  "authors": [],
16
16
  "component": {
17
17
  "type": "library",
18
- "bom-ref": "pkg:npm/create-daloy@1.2.0",
18
+ "bom-ref": "pkg:npm/create-daloy@1.3.0",
19
19
  "name": "create-daloy",
20
- "version": "1.2.0",
20
+ "version": "1.3.0",
21
21
  "description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
22
- "purl": "pkg:npm/create-daloy@1.2.0",
22
+ "purl": "pkg:npm/create-daloy@1.3.0",
23
23
  "licenses": [
24
24
  {
25
25
  "license": {
@@ -42,9 +42,9 @@
42
42
  }
43
43
  ],
44
44
  "swid": {
45
- "tagId": "swidtag-create-daloy-1.2.0",
45
+ "tagId": "swidtag-create-daloy-1.3.0",
46
46
  "name": "create-daloy",
47
- "version": "1.2.0",
47
+ "version": "1.3.0",
48
48
  "tagVersion": 0,
49
49
  "patch": false
50
50
  }
@@ -53,7 +53,7 @@
53
53
  "components": [],
54
54
  "dependencies": [
55
55
  {
56
- "ref": "pkg:npm/create-daloy@1.2.0",
56
+ "ref": "pkg:npm/create-daloy@1.3.0",
57
57
  "dependsOn": []
58
58
  }
59
59
  ]
package/sbom.spdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "create-daloy-1.2.0",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.2.0-14e46560-a86e-5c0f-8279-490ef7794f0e",
5
+ "name": "create-daloy-1.3.0",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.3.0-334286a9-ce6f-53b5-9ece-5e69ac3b7aac",
7
7
  "creationInfo": {
8
- "created": "2026-08-20T11:51:47.001Z",
8
+ "created": "2026-08-28T11:08:56.276Z",
9
9
  "creators": [
10
10
  "Tool: daloy-generate-sbom",
11
11
  "Organization: DaloyJS"
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "SPDXID": "SPDXRef-Package-create-daloy",
18
18
  "name": "create-daloy",
19
- "versionInfo": "1.2.0",
19
+ "versionInfo": "1.3.0",
20
20
  "downloadLocation": "https://github.com/daloyjs/daloy",
21
21
  "filesAnalyzed": false,
22
22
  "licenseConcluded": "MIT",
@@ -27,7 +27,7 @@
27
27
  {
28
28
  "referenceCategory": "PACKAGE-MANAGER",
29
29
  "referenceType": "purl",
30
- "referenceLocator": "pkg:npm/create-daloy@1.2.0"
30
+ "referenceLocator": "pkg:npm/create-daloy@1.3.0"
31
31
  }
32
32
  ]
33
33
  }
@@ -53,6 +53,7 @@ Do not write `.js` here — that's the Node NodeNext convention and will fail to
53
53
  6. Keep operation IDs stable and examples schema-valid; `bun run contract` must pass after route, metadata, or OpenAPI-facing changes.
54
54
  7. Every new route ships with a test that covers a happy path and at least one unhappy path.
55
55
  8. After any route change: `bun run gen:openapi && bun run gen:client && bun run contract && bun run typecheck && bun test`.
56
+ 9. Side effects that must outlive a request: enqueue a job (`app.useJobs` + idempotency key); pattern in the skill.
56
57
 
57
58
  ## Secure-by-default (do not let an AI strip these)
58
59
 
@@ -74,4 +75,4 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
74
75
  - Bug fixes include a regression test.
75
76
  - Never bypass safety checks without a clear reason.
76
77
 
77
- For the full workflow — adding routes step-by-step, schema conventions, testing patterns, security guidance, and deployment notes — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
78
+ For the full workflow — routes, background jobs, testing, security, and deployment — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
@@ -4,7 +4,8 @@ description: >-
4
4
  Build, test, and harden this DaloyJS REST API on Bun. Use when the user
5
5
  asks to add or change an endpoint, route, Zod/Standard Schema,
6
6
  middleware, error handling, OpenAPI spec, typed Hey API client, contract
7
- gate, auth, rate limit, or security default. Also use for phrasing like
7
+ gate, auth, rate limit, background jobs, or security default. Also use
8
+ for phrasing like
8
9
  "add GET /...", "new route", "regenerate the client", "bun test", or
9
10
  "fix the 401". Do not use for frontend UI, infra-only work, or unrelated
10
11
  docs.
@@ -217,6 +218,48 @@ app.use(rateLimit({ windowMs: 60_000, max: 120 }));
217
218
 
218
219
  Add CORS only when needed, with an explicit `origin` allowlist.
219
220
 
221
+ ## Background jobs
222
+
223
+ Side effects that must outlive the HTTP request (welcome emails, webhook
224
+ fan-out, thumbnails, nightly reconciliation) belong in a **job**, not
225
+ inline in the handler and not in a fire-and-forget promise.
226
+
227
+ - `app.useJobs({ store, handlers, startWorker })` wires a queue (and an
228
+ optional in-process worker) into the app lifecycle, including the
229
+ graceful-shutdown drain.
230
+ - Enqueue **after** the DB commit, always with an idempotency key:
231
+
232
+ ```ts
233
+ import { jobIdempotencyKey } from "@daloyjs/core";
234
+
235
+ await app.jobs!.enqueue({
236
+ name: "email.welcome",
237
+ payload: { userId: user.id, to: user.email }, // ids, never file bytes
238
+ idempotencyKey: jobIdempotencyKey({ name: "email.welcome", key: user.id }),
239
+ });
240
+ ```
241
+
242
+ - Delivery is **at-least-once**: handlers must be idempotent. Pass a key
243
+ through to downstream APIs (e.g. Stripe's `Idempotency-Key`) when a
244
+ duplicate run would move money or send email.
245
+ - Throw `JobFatalError` for permanent failures (no retry); any other throw
246
+ retries with full-jitter backoff, then dead-letters.
247
+ - `app.cronEnqueue(def, { name, payload })` turns a cron tick into an
248
+ idempotent enqueue. Use it for side effects that must run once
249
+ cluster-wide; keep plain `app.cron()` for process-local maintenance
250
+ (other replicas have their own memory to sweep).
251
+ - Payloads are plain JSON capped at 64 KiB: enqueue ids and blob URLs,
252
+ never file contents.
253
+ - `MemoryJobStore` is for tests (`worker.runOnce()`) and single-process
254
+ dev. Production needs a shared `JobStore` adapter (Redis/Postgres/SQS)
255
+ in app code; `useJobs` warns on Memory in production, and
256
+ `strictProduction: true` refuses to boot.
257
+ - `startWorker: true` in the same process is fine for small deployments;
258
+ split a dedicated worker process (same app, `useJobs` with
259
+ `startWorker`, no public ingress) as you grow.
260
+
261
+ Full reference: <https://daloyjs.dev/docs/jobs>
262
+
220
263
  ## Testing best practices
221
264
 
222
265
  Tests run with `bun test`. Use **in-process** requests through
@@ -19,7 +19,7 @@
19
19
  "hooks:install": "git config core.hooksPath .githooks"
20
20
  },
21
21
  "dependencies": {
22
- "@daloyjs/core": "^1.2.0",
22
+ "@daloyjs/core": "^1.3.0",
23
23
  "zod": "^4.4.3"
24
24
  },
25
25
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md
2
2
 
3
- A [DaloyJS](https://daloyjs.dev) REST API deployed to **Cloudflare Workers**. **Contract-first**: routes are defined with validation schemas (Zod in this template; DaloyJS also supports Standard Schema-compatible validators) and OpenAPI 3.1 is generated from them. `docs: true` is set in `new App({...})`, so `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI) are auto-mounted. DaloyJS is dependency-free and the Scalar UI loads from a CDN, so this adds negligible Worker bundle size; drop `docs` (and the `openapi` block) if you want the smallest possible bundle.
3
+ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Cloudflare Workers**. **Contract-first**: routes are defined with validation schemas (Zod in this template; DaloyJS also supports Standard Schema-compatible validators) and OpenAPI 3.1 is generated from them. `docs: true` is set in `new App({...})`, so `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI) are auto-mounted. DaloyJS is dependency-free and the Scalar UI loads from a CDN, so bundle cost is negligible; drop `docs` (and the `openapi` block) for the smallest bundle.
4
4
 
5
5
  - Package manager: pnpm (use `pnpm` unless the project's `package.json` was rewritten for npm/yarn/bun).
6
6
  - Runtime: Cloudflare Workers (Web Standard `Request`/`Response`).
@@ -37,7 +37,7 @@ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Cloudflare Workers**. **
37
37
  5. Keep `requestId()`, `secureHeaders()`, and `rateLimit()` enabled. The in-memory limiter resets per isolate.
38
38
  6. Stay on the Workers runtime: only Web Standards APIs + Cloudflare bindings. No `node:` modules unless you explicitly add `nodejs_compat` and require it.
39
39
  7. Bindings flow through `env`. Read KV/D1/R2/secrets from the `env` argument; never read them via globals.
40
- 8. Long-running work belongs in `ctx.waitUntil(...)`, not blocking the response.
40
+ 8. Long-running work: `ctx.waitUntil(...)` or a remote-store job via `app.useJobs` (enqueue only, no worker loop on an isolate); never block the response.
41
41
  9. Keep operation IDs stable and examples schema-valid; `pnpm contract` must pass after route, metadata, or OpenAPI-facing changes.
42
42
  10. Every new route ships with a test that covers a happy path and at least one unhappy path.
43
43
 
@@ -64,4 +64,4 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
64
64
  - For deploys, ensure the user has run `wrangler login`; do not authenticate on their behalf.
65
65
  - Never bypass safety checks without a clear reason.
66
66
 
67
- For the full workflow — adding routes step-by-step, bindings, testing patterns, security guidance, and deployment notes — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
67
+ For the full workflow — routes, background jobs, bindings, testing, security, and deployment — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
@@ -4,7 +4,8 @@ description: >-
4
4
  Build, test, and harden this DaloyJS REST API on Cloudflare Workers. Use
5
5
  when the user asks to add or change an endpoint, route, Zod/Standard
6
6
  Schema, middleware, error handling, Worker binding (KV, D1, R2, Queues,
7
- env), contract gate, auth, or rate limit. Also use for phrasing like
7
+ env), contract gate, auth, rate limit, or background jobs. Also use for
8
+ phrasing like
8
9
  "add GET /...", "new route", "wrangler", "fix the 401", or "add a KV
9
10
  binding". Do not use for frontend UI, infra-only work, or unrelated
10
11
  docs.
@@ -226,6 +227,49 @@ Add CORS only when needed, with an explicit `origin` allowlist.
226
227
  4. Store secrets via `wrangler secret put` — they appear on `env` but
227
228
  are not committed to `wrangler.toml`.
228
229
 
230
+ ## Background jobs
231
+
232
+ Side effects that must outlive the HTTP request (welcome emails, webhook
233
+ fan-out, thumbnails, nightly reconciliation) belong in a **job**, not
234
+ inline in the handler and not in a fire-and-forget promise.
235
+
236
+ - `app.useJobs({ store, handlers, startWorker })` wires a queue (and an
237
+ optional in-process worker) into the app lifecycle, including the
238
+ graceful-shutdown drain.
239
+ - Enqueue **after** the DB commit, always with an idempotency key:
240
+
241
+ ```ts
242
+ import { jobIdempotencyKey } from "@daloyjs/core";
243
+
244
+ await app.jobs!.enqueue({
245
+ name: "email.welcome",
246
+ payload: { userId: user.id, to: user.email }, // ids, never file bytes
247
+ idempotencyKey: jobIdempotencyKey({ name: "email.welcome", key: user.id }),
248
+ });
249
+ ```
250
+
251
+ - Delivery is **at-least-once**: handlers must be idempotent. Pass a key
252
+ through to downstream APIs (e.g. Stripe's `Idempotency-Key`) when a
253
+ duplicate run would move money or send email.
254
+ - Throw `JobFatalError` for permanent failures (no retry); any other throw
255
+ retries with full-jitter backoff, then dead-letters.
256
+ - `app.cronEnqueue(def, { name, payload })` turns a cron tick into an
257
+ idempotent enqueue. Use it for side effects that must run once
258
+ cluster-wide; keep plain `app.cron()` for process-local maintenance
259
+ (other replicas have their own memory to sweep).
260
+ - Payloads are plain JSON capped at 64 KiB: enqueue ids and blob URLs,
261
+ never file contents.
262
+ - `MemoryJobStore` is for tests (`worker.runOnce()`) and single-process
263
+ dev. Production needs a shared `JobStore` adapter (Redis/Postgres/SQS)
264
+ in app code; `useJobs` warns on Memory in production, and
265
+ `strictProduction: true` refuses to boot.
266
+ - A Workers isolate **enqueues only**: never `startWorker: true` here —
267
+ the isolate is torn down once the response flushes. Run the worker as a
268
+ separate long-lived Node service (same app code, `useJobs` with
269
+ `startWorker`) against the same remote store.
270
+
271
+ Full reference: <https://daloyjs.dev/docs/jobs>
272
+
229
273
  ## Testing best practices
230
274
 
231
275
  Two patterns:
@@ -16,7 +16,7 @@
16
16
  "hooks:install": "git config core.hooksPath .githooks"
17
17
  },
18
18
  "dependencies": {
19
- "@daloyjs/core": "^1.2.0",
19
+ "@daloyjs/core": "^1.3.0",
20
20
  "zod": "^4.4.3"
21
21
  },
22
22
  "devDependencies": {
@@ -42,6 +42,7 @@ The typed Hey API SDK is generated outside Deno (Hey API has no Deno entrypoint
42
42
  7. Keep operation IDs stable and examples schema-valid; `deno task contract` must pass after route, metadata, or OpenAPI-facing changes.
43
43
  8. Every new route ships with a test that covers a happy path and at least one unhappy path.
44
44
  9. After any route change: `deno task gen:openapi && deno task contract && deno task typecheck && deno task test`.
45
+ 10. Side effects that must outlive a request: enqueue a job (`app.useJobs` + idempotency key); pattern in the skill.
45
46
 
46
47
  ## Secure-by-default (do not let an AI strip these)
47
48
 
@@ -63,4 +64,4 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
63
64
  - Bug fixes include a regression test.
64
65
  - Use `deno task ...`, not `npm`/`pnpm`. There is no `package.json` here.
65
66
 
66
- For the full workflow — adding routes step-by-step, schema conventions, testing patterns, security guidance, and deployment notes — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
67
+ For the full workflow — routes, background jobs, testing, security, and deployment — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
@@ -4,7 +4,8 @@ description: >-
4
4
  Build, test, and harden this DaloyJS REST API on Deno. Use when the user
5
5
  asks to add or change an endpoint, route, Zod/Standard Schema,
6
6
  middleware, error handling, OpenAPI spec, contract gate, Deno
7
- permission, deno.json import, auth, or rate limit. Also use for phrasing
7
+ permission, deno.json import, auth, rate limit, or background jobs.
8
+ Also use for phrasing
8
9
  like "add GET /...", "new route", "deno task", "fix the 401", or "allow
9
10
  net". Do not use for frontend UI, npm/pnpm package.json work, or
10
11
  unrelated docs.
@@ -214,6 +215,48 @@ app.use(rateLimit({ windowMs: 60_000, max: 120 }));
214
215
 
215
216
  Add CORS only when needed, with an explicit `origin` allowlist.
216
217
 
218
+ ## Background jobs
219
+
220
+ Side effects that must outlive the HTTP request (welcome emails, webhook
221
+ fan-out, thumbnails, nightly reconciliation) belong in a **job**, not
222
+ inline in the handler and not in a fire-and-forget promise.
223
+
224
+ - `app.useJobs({ store, handlers, startWorker })` wires a queue (and an
225
+ optional in-process worker) into the app lifecycle, including the
226
+ graceful-shutdown drain.
227
+ - Enqueue **after** the DB commit, always with an idempotency key:
228
+
229
+ ```ts
230
+ import { jobIdempotencyKey } from "@daloyjs/core";
231
+
232
+ await app.jobs!.enqueue({
233
+ name: "email.welcome",
234
+ payload: { userId: user.id, to: user.email }, // ids, never file bytes
235
+ idempotencyKey: jobIdempotencyKey({ name: "email.welcome", key: user.id }),
236
+ });
237
+ ```
238
+
239
+ - Delivery is **at-least-once**: handlers must be idempotent. Pass a key
240
+ through to downstream APIs (e.g. Stripe's `Idempotency-Key`) when a
241
+ duplicate run would move money or send email.
242
+ - Throw `JobFatalError` for permanent failures (no retry); any other throw
243
+ retries with full-jitter backoff, then dead-letters.
244
+ - `app.cronEnqueue(def, { name, payload })` turns a cron tick into an
245
+ idempotent enqueue. Use it for side effects that must run once
246
+ cluster-wide; keep plain `app.cron()` for process-local maintenance
247
+ (other replicas have their own memory to sweep).
248
+ - Payloads are plain JSON capped at 64 KiB: enqueue ids and blob URLs,
249
+ never file contents.
250
+ - `MemoryJobStore` is for tests (`worker.runOnce()`) and single-process
251
+ dev. Production needs a shared `JobStore` adapter (Redis/Postgres/SQS)
252
+ in app code; `useJobs` warns on Memory in production, and
253
+ `strictProduction: true` refuses to boot.
254
+ - `startWorker: true` in the same process is fine for small deployments;
255
+ split a dedicated worker process (same app, `useJobs` with
256
+ `startWorker`, no public ingress) as you grow.
257
+
258
+ Full reference: <https://daloyjs.dev/docs/jobs>
259
+
217
260
  ## Testing best practices
218
261
 
219
262
  Tests run with `deno test`. Use **in-process** `app.request()` — no port
@@ -10,11 +10,11 @@
10
10
  "hooks:install": "git config core.hooksPath .githooks"
11
11
  },
12
12
  "imports": {
13
- "@daloyjs/core": "jsr:@daloyjs/daloy@^1.2.0",
14
- "@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.2.0/banner",
15
- "@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.2.0/contract",
16
- "@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.2.0/deno",
17
- "@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.2.0/openapi",
13
+ "@daloyjs/core": "jsr:@daloyjs/daloy@^1.3.0",
14
+ "@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.3.0/banner",
15
+ "@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.3.0/contract",
16
+ "@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.3.0/deno",
17
+ "@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.3.0/openapi",
18
18
  "zod": "npm:zod@^4.4.3"
19
19
  },
20
20
  "compilerOptions": {
@@ -56,6 +56,7 @@ You import the file you see. On `pnpm build`, TypeScript rewrites the `.ts` spec
56
56
  6. Keep operation IDs stable and examples schema-valid; `pnpm contract` must pass after route, metadata, or OpenAPI-facing changes.
57
57
  7. Every new route ships with a test that covers a happy path and at least one unhappy path.
58
58
  8. After any route change: `pnpm gen && pnpm contract && pnpm typecheck && pnpm test`.
59
+ 9. Side effects that must outlive a request: enqueue a job (`app.useJobs` + idempotency key); pattern in the skill.
59
60
 
60
61
  ## Secure-by-default (do not let an AI strip these)
61
62
 
@@ -74,4 +75,4 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
74
75
 
75
76
  - Bug fixes include a regression test.
76
77
 
77
- For the full workflow — adding routes step-by-step, schema conventions, testing patterns, security guidance, and deployment notes — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
78
+ For the full workflow — routes, background jobs, testing, security, and deployment — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
@@ -4,7 +4,8 @@ description: >-
4
4
  Build, test, and harden this DaloyJS REST API on Node.js. Use when the
5
5
  user asks to add or change an endpoint, route, Zod/Standard Schema,
6
6
  middleware, error handling, OpenAPI spec, typed Hey API client, contract
7
- gate, auth, rate limit, or security default. Also use for phrasing like
7
+ gate, auth, rate limit, background jobs, or security default. Also use
8
+ for phrasing like
8
9
  "add GET /...", "new route", "regenerate the client", or "fix the 401".
9
10
  Do not use for frontend UI, infra-only work, or unrelated docs.
10
11
  license: MIT
@@ -256,6 +257,48 @@ Custom middleware should be small, well-typed, and call `await next()`
256
257
  exactly once. Wrap it in `try { await next() } finally { ... }` if it
257
258
  needs to run code after the handler.
258
259
 
260
+ ## Background jobs
261
+
262
+ Side effects that must outlive the HTTP request (welcome emails, webhook
263
+ fan-out, thumbnails, nightly reconciliation) belong in a **job**, not
264
+ inline in the handler and not in a fire-and-forget promise.
265
+
266
+ - `app.useJobs({ store, handlers, startWorker })` wires a queue (and an
267
+ optional in-process worker) into the app lifecycle, including the
268
+ graceful-shutdown drain.
269
+ - Enqueue **after** the DB commit, always with an idempotency key:
270
+
271
+ ```ts
272
+ import { jobIdempotencyKey } from "@daloyjs/core";
273
+
274
+ await app.jobs!.enqueue({
275
+ name: "email.welcome",
276
+ payload: { userId: user.id, to: user.email }, // ids, never file bytes
277
+ idempotencyKey: jobIdempotencyKey({ name: "email.welcome", key: user.id }),
278
+ });
279
+ ```
280
+
281
+ - Delivery is **at-least-once**: handlers must be idempotent. Pass a key
282
+ through to downstream APIs (e.g. Stripe's `Idempotency-Key`) when a
283
+ duplicate run would move money or send email.
284
+ - Throw `JobFatalError` for permanent failures (no retry); any other throw
285
+ retries with full-jitter backoff, then dead-letters.
286
+ - `app.cronEnqueue(def, { name, payload })` turns a cron tick into an
287
+ idempotent enqueue. Use it for side effects that must run once
288
+ cluster-wide; keep plain `app.cron()` for process-local maintenance
289
+ (other replicas have their own memory to sweep).
290
+ - Payloads are plain JSON capped at 64 KiB: enqueue ids and blob URLs,
291
+ never file contents.
292
+ - `MemoryJobStore` is for tests (`worker.runOnce()`) and single-process
293
+ dev. Production needs a shared `JobStore` adapter (Redis/Postgres/SQS)
294
+ in app code; `useJobs` warns on Memory in production, and
295
+ `strictProduction: true` refuses to boot.
296
+ - `startWorker: true` in the same process is fine for small deployments;
297
+ split a dedicated worker process (same app, `useJobs` with
298
+ `startWorker`, no public ingress) as you grow.
299
+
300
+ Full reference: <https://daloyjs.dev/docs/jobs>
301
+
259
302
  ## Testing best practices
260
303
 
261
304
  Tests live under `tests/` and run with `node --test` (Node's built-in
@@ -20,7 +20,7 @@
20
20
  "hooks:install": "git config core.hooksPath .githooks"
21
21
  },
22
22
  "dependencies": {
23
- "@daloyjs/core": "^1.2.0",
23
+ "@daloyjs/core": "^1.3.0",
24
24
  "zod": "^4.4.3"
25
25
  },
26
26
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md
2
2
 
3
- A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel** on the **Node.js runtime**. **Contract-first**: routes use validation schemas (Zod here; DaloyJS also supports Standard Schema-compatible validators) and generate OpenAPI 3.1. With `docs: true`, DaloyJS auto-mounts `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI).
3
+ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel** on the **Node.js runtime**. **Contract-first**: routes use validation schemas (Zod here; DaloyJS also supports Standard Schema-compatible validators) and generate OpenAPI 3.1. `docs: true` auto-mounts `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI).
4
4
 
5
5
  - Package manager: pnpm (use `pnpm` unless the project's `package.json` was rewritten for npm/yarn/bun).
6
6
  - Runtime: Vercel Node.js Functions on Fluid Compute (Web Standard `Request`/`Response`).
@@ -14,7 +14,7 @@ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel** on the **Node.j
14
14
 
15
15
  ## Commands
16
16
 
17
- - `pnpm dev` — local Node dev server (`src/dev.ts`) on http://localhost:3000 (no `vercel dev` / login needed; serves the same app the Vercel Function runs)
17
+ - `pnpm dev` — local Node dev server (`src/dev.ts`) on http://localhost:3000 (no `vercel dev` needed)
18
18
  - `pnpm typecheck` — `tsc --noEmit`
19
19
  - `pnpm test` — run test suite
20
20
  - `pnpm contract` — run `daloy inspect --check api/index.ts`
@@ -51,6 +51,7 @@ You import the file you see. Vercel resolves `.ts` at deploy time; Node runs it
51
51
  7. Keep a single `api/index.ts` entry and the `vercel.json` `/(.*)` → `/api` rewrite so DaloyJS handles all routing at the site root.
52
52
  8. Keep operation IDs stable and examples schema-valid; `pnpm contract` must pass after route, metadata, or OpenAPI-facing changes.
53
53
  9. Every new route ships with a test that covers a happy path and at least one unhappy path.
54
+ 10. Side effects that must outlive a request: `app.useJobs` + remote store; enqueue only, no worker loop in the Function.
54
55
 
55
56
  ## Secure-by-default (do not let an AI strip these)
56
57
 
@@ -73,4 +74,4 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
73
74
  - For deploys, ensure the user has run `vercel login`; do not authenticate on their behalf.
74
75
  - Never bypass safety checks without a clear reason.
75
76
 
76
- For the full workflow — adding routes step-by-step, schema conventions, testing patterns, security guidance, and deployment notes — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
77
+ For the full workflow — routes, background jobs, testing, security, and deployment — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
@@ -4,8 +4,8 @@ description: >-
4
4
  Build, test, and harden this DaloyJS REST API on Vercel (Node.js
5
5
  runtime). Use when the user asks to add or change an endpoint, route,
6
6
  Zod/Standard Schema, middleware, error handling, OpenAPI spec, typed
7
- client, contract gate, auth, rate limit, or the single Functions
8
- entrypoint / rewrite. Also use for phrasing like "add GET /...", "new
7
+ client, contract gate, auth, rate limit, background jobs, or the
8
+ single Functions entrypoint / rewrite. Also use for phrasing like "add GET /...", "new
9
9
  route", "fix the 401", or "deploy this API". Do not use for Next.js App
10
10
  Router work, frontend UI, or unrelated docs.
11
11
  license: MIT
@@ -201,6 +201,49 @@ Register middleware **before** route definitions. Order matters.
201
201
  Keep the secure baseline (`requestId`, `secureHeaders`, `rateLimit`).
202
202
  Add CORS only when needed, with an explicit `origin` allowlist.
203
203
 
204
+ ## Background jobs
205
+
206
+ Side effects that must outlive the HTTP request (welcome emails, webhook
207
+ fan-out, thumbnails, nightly reconciliation) belong in a **job**, not
208
+ inline in the handler and not in a fire-and-forget promise.
209
+
210
+ - `app.useJobs({ store, handlers, startWorker })` wires a queue (and an
211
+ optional in-process worker) into the app lifecycle, including the
212
+ graceful-shutdown drain.
213
+ - Enqueue **after** the DB commit, always with an idempotency key:
214
+
215
+ ```ts
216
+ import { jobIdempotencyKey } from "@daloyjs/core";
217
+
218
+ await app.jobs!.enqueue({
219
+ name: "email.welcome",
220
+ payload: { userId: user.id, to: user.email }, // ids, never file bytes
221
+ idempotencyKey: jobIdempotencyKey({ name: "email.welcome", key: user.id }),
222
+ });
223
+ ```
224
+
225
+ - Delivery is **at-least-once**: handlers must be idempotent. Pass a key
226
+ through to downstream APIs (e.g. Stripe's `Idempotency-Key`) when a
227
+ duplicate run would move money or send email.
228
+ - Throw `JobFatalError` for permanent failures (no retry); any other throw
229
+ retries with full-jitter backoff, then dead-letters.
230
+ - `app.cronEnqueue(def, { name, payload })` turns a cron tick into an
231
+ idempotent enqueue. Use it for side effects that must run once
232
+ cluster-wide; keep plain `app.cron()` for process-local maintenance
233
+ (other replicas have their own memory to sweep).
234
+ - Payloads are plain JSON capped at 64 KiB: enqueue ids and blob URLs,
235
+ never file contents.
236
+ - `MemoryJobStore` is for tests (`worker.runOnce()`) and single-process
237
+ dev. Production needs a shared `JobStore` adapter (Redis/Postgres/SQS)
238
+ in app code; `useJobs` warns on Memory in production, and
239
+ `strictProduction: true` refuses to boot.
240
+ - Vercel Functions **enqueue only**: never `startWorker: true` here — a
241
+ Function is frozen once the response flushes. Run the worker as a
242
+ separate long-lived Node service (same app code, `useJobs` with
243
+ `startWorker`) against the same remote store.
244
+
245
+ Full reference: <https://daloyjs.dev/docs/jobs>
246
+
204
247
  ## Testing best practices
205
248
 
206
249
  Tests use in-process `app.request(...)` — no port, no Vercel runtime
@@ -16,7 +16,7 @@
16
16
  "hooks:install": "git config core.hooksPath .githooks"
17
17
  },
18
18
  "dependencies": {
19
- "@daloyjs/core": "^1.2.0",
19
+ "@daloyjs/core": "^1.3.0",
20
20
  "zod": "^4.4.3"
21
21
  },
22
22
  "devDependencies": {