@nilejs/cli 0.0.7 → 1.0.1

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.
@@ -1,6 +1,10 @@
1
1
  # {{projectName}}
2
2
 
3
- Built with [Nile](https://github.com/nile-js/nile).
3
+ Built with [Nile Js](https://nile-js.github.io/nile/). Read the [getting started guide](https://nile-js.github.io/nile/guide/start/getting-started/) for a walkthrough of this starter.
4
+
5
+ The server runs on Bun's native HTTP server (`runtime: "bun", native: true` in `src/index.ts`). REST settings live under `rest`; custom routes and middleware are available on `server.nativeRest`.
6
+
7
+ The included `SKILL.md` gives coding assistants the action pattern, service layout and current error-handling rules. After a Nile upgrade, use `node_modules/@nilejs/nile/SKILL.md` as the current reference.
4
8
 
5
9
  ## Setup
6
10
 
@@ -10,7 +14,7 @@ cp .env.example .env
10
14
  bun run dev
11
15
  ```
12
16
 
13
- The server starts at `http://localhost:8000`. PGLite creates an embedded Postgres database automatically, no external database required.
17
+ The server starts at `http://localhost:8000`. PGlite creates an embedded Postgres database in `data/` on first start, so no database server is required. The first start can take up to 30 seconds.
14
18
 
15
19
  ## Scripts
16
20
 
@@ -24,13 +28,13 @@ The server starts at `http://localhost:8000`. PGLite creates an embedded Postgre
24
28
  | `bun run db:push` | Push schema changes directly to the database |
25
29
  | `bun run db:studio` | Open Drizzle Studio to browse your data |
26
30
 
27
- ## Project Structure
31
+ ## Project structure
28
32
 
29
33
  ```
30
34
  src/
31
35
  index.ts # Server entry point
32
36
  db/
33
- client.ts # PGLite + Drizzle client
37
+ client.ts # PGlite + Drizzle client
34
38
  schema.ts # Drizzle table definitions
35
39
  types.ts # Inferred types from schema
36
40
  index.ts # Barrel export
@@ -40,16 +44,18 @@ src/
40
44
  services/
41
45
  services.config.ts # Service registry
42
46
  tasks/
43
- create.ts # Create task action
44
- list.ts # List tasks action
45
- get.ts # Get task by ID action
46
- update.ts # Update task action
47
- delete.ts # Delete task action
47
+ index.ts # Registers the tasks service
48
+ actions/
49
+ create.ts # Create task action
50
+ list.ts # List tasks action
51
+ get.ts # Get task by ID action
52
+ update.ts # Update task action
53
+ delete.ts # Delete task action
48
54
  ```
49
55
 
50
56
  ## Usage
51
57
 
52
- All requests go through a single POST endpoint. The `intent` field determines the operation.
58
+ HTTP action calls go through a single POST endpoint. The `intent` field determines the operation.
53
59
 
54
60
  ### Explore available services
55
61
 
@@ -75,7 +81,13 @@ curl -X POST http://localhost:8000/api/services \
75
81
  -d '{"intent":"schema","service":"tasks","action":"*","payload":{}}'
76
82
  ```
77
83
 
78
- ## Adding Services
84
+ Responses have exactly `status`, `message` and `data`. The default success message is `Action completed.` Validation failures contain Zod’s readable message and `{ errorCode, issues }` in `data`. Unexpected failures use a generated diagnostic reference linked to the log’s `log_id`; internal `raw` causes are never sent to the frontend.
85
+
86
+ ## Optional features
87
+
88
+ Keep business logic in actions and ordinary functions. Configure other features when the product needs them: [typed Socket.IO topics](https://nile-js.github.io/nile/guide/features/websockets/), [cron jobs](https://nile-js.github.io/nile/guide/features/cron-jobs/), [rate limits](https://nile-js.github.io/nile/guide/features/rate-limiting/), [queues](https://nile-js.github.io/nile/guide/features/queues-debounce-throttle/) and [telemetry](https://nile-js.github.io/nile/guide/features/telemetry/). The frontend package provides both `createNileClient` and a separate `createNileSocketClient`.
89
+
90
+ ## Adding services
79
91
 
80
92
  Generate a new service with the CLI:
81
93
 
@@ -83,9 +95,9 @@ Generate a new service with the CLI:
83
95
  bun run gen service users
84
96
  ```
85
97
 
86
- Or manually create a directory under `src/services/` with action files and register it in `src/services/services.config.ts`.
98
+ The generator offers to register the service in `src/services/services.config.ts`. To add one by hand, create a folder under `src/services/` with an `actions/` folder and create an `index.ts` that registers its actions with `createService`. Import that service into `services.config.ts`. Nile checks this layout at startup and fails on violations by default. Set `project.structure` to `"warn"` explicitly if you need to migrate an older layout gradually.
87
99
 
88
- ## Adding Actions
100
+ ## Adding actions
89
101
 
90
102
  Generate a new action in an existing service:
91
103
 
@@ -93,9 +105,9 @@ Generate a new action in an existing service:
93
105
  bun run gen action users get-user
94
106
  ```
95
107
 
96
- Each action file exports a single action created with `createAction`, which takes a Zod validation schema and a handler function that returns `Ok(data)` or `Err(message)`.
108
+ This creates `src/services/users/actions/get-user.ts`. Each action file exports one action created with `createAction`: a named Zod schema, an input type inferred from it and a named handler that returns `Ok(data)` or `Err(message)`. Import the action in `src/services/users/index.ts` and add it to the service's `actions` list.
97
109
 
98
- ## Generating Schemas & Types
110
+ ## Generating schemas and types
99
111
 
100
112
  Extract Zod validation schemas from your actions and generate TypeScript types:
101
113
 
@@ -105,8 +117,8 @@ bun run gen schema
105
117
 
106
118
  This auto-detects `src/services/services.config.ts`, reads validation schemas from all actions, and outputs two files:
107
119
 
108
- - `src/generated/schemas.ts` — named Zod schema exports
109
- - `src/generated/types.ts` — inferred TypeScript types via `z.infer`
120
+ - `src/generated/schemas.ts`: named Zod schema exports
121
+ - `src/generated/types.ts`: input TypeScript types via `z.input`
110
122
 
111
123
  Options:
112
124
 
@@ -119,14 +131,14 @@ Actions without a validation schema are skipped and listed in the CLI output.
119
131
 
120
132
  ## Database
121
133
 
122
- This project ships with [PGLite](https://electric-sql.com/product/pglite) for zero-setup local development. For production, you can swap it for Postgres, MySQL, SQLite, or any other database supported by Drizzle. Update `src/db/client.ts` with your connection and driver.
134
+ This project ships with [PGlite](https://electric-sql.com/product/pglite) for zero-setup local development. For production, you can swap it for Postgres, MySQL, SQLite, or any other database supported by Drizzle. Update `src/db/client.ts` with your connection and driver.
123
135
 
124
136
  See the [Drizzle getting started guide](https://orm.drizzle.team/docs/get-started) for setup instructions with each supported database.
125
137
 
126
- ## Tech Stack
138
+ ## Tech stack
127
139
 
128
140
  - **Runtime:** [Bun](https://bun.sh)
129
- - **Framework:** [@nilejs/nile](https://github.com/nile-js/nile/tree/main/packages/nile)
130
- - **Database:** [PGLite](https://electric-sql.com/product/pglite) (embedded Postgres)
141
+ - **Framework:** [Nile Js](https://nile-js.github.io/nile/) (`@nilejs/nile`)
142
+ - **Database:** [PGlite](https://electric-sql.com/product/pglite) (embedded Postgres)
131
143
  - **ORM:** [Drizzle](https://orm.drizzle.team)
132
144
  - **Validation:** [Zod](https://zod.dev)
@@ -0,0 +1,343 @@
1
+ ---
2
+ name: nile
3
+ description: Work in a Nile Js backend (@nilejs/nile 1.x). Organize business operations as services and actions with Zod validation, Result returns, hooks, JWT protection and typed clients. Use when adding or changing actions, services, hooks, uploads, topics, schedules or the @nilejs/client calls in a Nile Js project.
4
+ license: MIT
5
+ ---
6
+
7
+ # Working in a Nile Js application
8
+
9
+ Nile Js organizes a backend into **services** (business areas) and **actions** (one operation each). Read the project's `src/services/services.config.ts` first: it is the registry of every action the server exposes. Then read the domain’s `index.ts`, a neighboring action, its model and any global policy hooks. Follow one operation from registration to persistence before changing it.
10
+
11
+ Use Nile’s conventions to make business operations easy to find. Prefer named functions, explicit inputs and early Result branches. Choose FP helpers when they make absence, failure, transformation or async control clearer; ordinary functions and straightforward `if` statements remain useful.
12
+
13
+ Documentation: https://nile-js.github.io/nile/guide/
14
+ Feature pages: https://nile-js.github.io/nile/guide/features/
15
+ Examples: https://nile-js.github.io/nile/guide/examples/
16
+ Every guide page is also listed in https://nile-js.github.io/nile/llms.txt
17
+
18
+ ## Project structure
19
+
20
+ Service and action entry points are the enforced core. The rest of this tree shows where supporting code goes when needed. Add optional layers when a real feature needs them; do not generate empty layers up front.
21
+
22
+ ```text
23
+ src/
24
+ index.ts listener and process lifecycle
25
+ server.ts optional server composition
26
+ config.ts optional environment parsing in one place
27
+ services/
28
+ services.config.ts explicit service registry
29
+ projects/
30
+ index.ts createService and public actions
31
+ actions/
32
+ create.ts a small action
33
+ archive/
34
+ archive.ts a larger action's entry point
35
+ index.ts exports archiveAction
36
+ modules/ archive's private helpers
37
+ modules/ helpers shared within this domain
38
+ db/
39
+ client.ts connection and driver
40
+ models/ persistence operations
41
+ hooks/ shared action policies
42
+ business/ optional reusable domain functions
43
+ generated/ client payload/schema contracts
44
+ ```
45
+
46
+ Create a project with `bunx @nilejs/cli new my-app`. Start it with `bun run dev`. Generate code with `bun run gen service <name>` and `bun run gen action <service> <action>`. Generate Zod schemas and accepted payload types from validations with `bun run gen schema`. Handler types describe parsed output (`z.infer`); client payload types describe input (`z.input`), so fields with defaults can be omitted by callers.
47
+
48
+ Structure violations fail startup by default. An application needs `src/index.ts` or `src/server.ts`, `src/services/services.config.ts`, and each service’s `index.ts` and `actions/`. For a growing action, use `actions/archive/archive.ts`, an `index.ts` export and private helpers in `actions/archive/modules/`. Action entry points above 400 lines are reported for refactoring. `project: { root: ".", structure: "warn" }` reports violations without stopping; `false` disables inspection. Source-less bundles skip inspection unless a root is explicit.
49
+
50
+ ## The action pattern
51
+
52
+ Every action uses a named schema, an inferred input type, a separately written named handler and `createAction<T>`:
53
+
54
+ ```ts
55
+ import { createAction } from "@nilejs/nile";
56
+ import { Err, Ok } from "@nilejs/nile/fp";
57
+ import { z } from "zod";
58
+
59
+ const createProjectSchema = z.object({
60
+ name: z.string().trim().min(3, "Name must have at least 3 characters"),
61
+ });
62
+
63
+ type CreateProjectInput = z.infer<typeof createProjectSchema>;
64
+
65
+ const createProjectHandler = (input: CreateProjectInput) => {
66
+ if (input.name === "system") {
67
+ return Err("Choose a different project name");
68
+ }
69
+
70
+ return Ok({ project: { name: input.name } });
71
+ };
72
+
73
+ export const createProjectAction = createAction<CreateProjectInput>({
74
+ name: "create",
75
+ description: "Create a project",
76
+ validation: createProjectSchema,
77
+ handler: createProjectHandler,
78
+ });
79
+ ```
80
+
81
+ Register the action in its service entry point, then register the service:
82
+
83
+ ```ts
84
+ // src/services/projects/index.ts
85
+ import { createService } from "@nilejs/nile";
86
+ import { createProjectAction } from "./actions/create";
87
+
88
+ export const projectsService = createService({
89
+ name: "projects",
90
+ description: "Project operations",
91
+ actions: [createProjectAction],
92
+ });
93
+ ```
94
+
95
+ ```ts
96
+ // src/services/services.config.ts
97
+ import { createServices } from "@nilejs/nile";
98
+ import { projectsService } from "./projects";
99
+
100
+ export const services = createServices([projectsService]);
101
+ ```
102
+
103
+ Action names are unique within a service. Folder names do not create routes; only the registry does.
104
+
105
+ ## Handlers take (data, context)
106
+
107
+ Nile Js calls every handler with two arguments: the validated input and the execution's `NileContext`. The type marks the context optional. Use `createAction<Input>` for the schema’s parsed output type, and import `NileContext` when annotating a named handler’s second argument. Read an optional context with `?.` or explicitly reject its absence before using it.
108
+
109
+ - `context.getSession("rest")` returns the verified JWT identity (`userId`, `organizationId` and other claims) for a protected action.
110
+ - `context.resources` holds what `createNileServer` received (`logger`, `database`).
111
+ - `context.execution` holds `requestId`, `transport`, `signal` and `clientKey`.
112
+ - `context.updateHookState` and `context.hookContext.state` are per execution. `context.set` and `context.get` are shared by every request in the process.
113
+ - `getContext<T>()` returns the context outside a handler. It throws before `createNileServer` runs.
114
+
115
+ ## Results
116
+
117
+ Handlers return `Ok(value)` or `Err(message)` from `@nilejs/nile/fp`, which re-exports `slang-ts`. Check `isErr` before reading `value`. Use `safeTry` around a call that can throw or reject. Use `utils.handleError({ message, data })` with `import { utils } from "@nilejs/nile"` to log an error and return it as an `Err` in one step.
118
+
119
+ The default success message is `Action completed.` Set `successMessage` on the action, or return `Ok(value, message)` to choose one per call; the handler's message wins.
120
+
121
+ HTTP envelope: success is `{ status: true, message, data }`. Failure is `{ status: false, message, data }`; inspect the envelope rather than assuming every failure has the same HTTP status. Validation failures use Zod’s prettified message (or an explicit validation message) and put a generated unique reference in `data.errorCode` alongside field paths in `data.issues`.
122
+
123
+ Slang 0.2.2 preserves caught causes on `result.raw`. `safeTry` returns a Result directly, including when the callback returns a Result; never expect a Result inside `result.value`. Nested `Ok(Ok(...))` and `Ok(Err(...))` are rejected. Return an existing Result directly.
124
+
125
+ `dbTry` is removed. For a database promise, use `safeTry`, inspect `result.raw` internally and pass `utils.describeError(result.raw)` to `utils.handleError` with a deliberate caller-safe message. `result.error` may contain driver text and must not be forwarded blindly. Unexpected throws inside Nile-managed work are logged and become `Error Code: <unique-code> - could not complete the action`. The generated reference matches the log’s `log_id`. Never use it as a stable error category. Custom success and intentional `Err` messages retain their wording.
126
+
127
+ `raw` is reserved for internal diagnostics and removed recursively from outgoing JSON and Socket.IO messages. Choose another name for public domain fields. Do not rely on that filter to sanitize secrets stored under other names.
128
+
129
+ ## Choose FP helpers for the job
130
+
131
+ Import from `@nilejs/nile/fp`, so application code and Nile share the same Slang types. Import Nile-specific helpers through `utils` from `@nilejs/nile`. Check the installed declarations before using an unfamiliar helper; do not copy APIs from a different FP library.
132
+
133
+ | Need | Use | Important boundary |
134
+ |---|---|---|
135
+ | An operation can succeed or fail | `Ok`, `Err`, `Result` | Check `isErr` before reading `value`; propagate a narrowed failure directly. |
136
+ | A dependency can throw or reject | `safeTry` | Await it. A caught failure still needs a safe caller message. |
137
+ | A value may legitimately be absent | `option`, `Option` | Absence is different from a failed lookup. |
138
+ | Both Result or Option branches produce a value | `match` | Supply both branches; callbacks receive the variant, not its unwrapped payload. |
139
+ | Several domain cases choose behavior | `matchAll` | Include the `_` fallback; keep business cases explicit. |
140
+ | A successful value needs a transformation | `andThen` | It propagates failure; await async transformations. Do not return `undefined` to mean removal. |
141
+ | Several Result-producing steps form a pipeline | `pipe` | Use the installed `.run()` API. A plain sequence of early returns is often easier to read. |
142
+ | Indexed slicing, shaping or set operations need explicit failure handling | `list`, `struct`, `sets`, `Ok.iterate` | Collection chains can return `Fallible`; finish with a deliberate fallback or handle the failure. |
143
+ | One async operation needs cancellation/deadline control | `superPromise` | It settles to a Result. Pass its signal to the dependency. |
144
+ | Several controlled tasks share a concurrency budget | `taskGroup`, `superRunner` | Share the group across calls; creating one per action execution does not bound total concurrency. |
145
+ | A pure calculation is reused with the same arguments | `memoized` | It caches by argument identity; do not cache sessions, permissions or mutable database reads this way. |
146
+ | An in-process notification coordinates local functions | `createChannel`, `createSignal` | These are local primitives, not Nile’s schema-validated Socket.IO topics or a cross-process event bus. |
147
+
148
+ ### Propagate Results instead of nesting them
149
+
150
+ Use an early failure branch when it makes the operation easier to follow. Once a Result is narrowed to `Err`, return it directly. A handler returning a Result does not need another `Ok` around that Result.
151
+
152
+ ```ts
153
+ import { Err, Ok, type Result } from "@nilejs/nile/fp";
154
+
155
+ const calculateTotal = (quantity: number): Result<number, string> => {
156
+ if (quantity < 1) {
157
+ return Err("Choose at least one item");
158
+ }
159
+ return Ok(quantity * 100);
160
+ };
161
+
162
+ export const prepareQuote = (quantity: number) => {
163
+ const total = calculateTotal(quantity);
164
+ if (total.isErr) {
165
+ return total;
166
+ }
167
+ return Ok({ total: total.value });
168
+ };
169
+ ```
170
+
171
+ `andThen` is useful for a short transformation of a successful value. In this Slang version, callbacks may return a plain value or a Result; the returned Result is not nested. A callback returning `undefined` preserves the original value, so do not use that to remove data. Callback exceptions become failures; keep them free of secret-bearing exception messages if the Result will reach a caller.
172
+
173
+ ### Model expected absence explicitly
174
+
175
+ Use an Option when “not configured yet” is a valid answer. Use an Err when the operation cannot proceed without the value. Do not silently turn a failed database call into an empty collection or a missing setting.
176
+
177
+ ```ts
178
+ import { match, option } from "@nilejs/nile/fp";
179
+
180
+ export const getDisplayName = (name: string | null) => {
181
+ return match(option(name), {
182
+ Some: (present) => {
183
+ return present.value;
184
+ },
185
+ None: () => {
186
+ return "Unnamed project";
187
+ },
188
+ });
189
+ };
190
+ ```
191
+
192
+ Slang’s absence rules matter: `0` and `false` are present; `null`, `undefined`, empty strings and non-finite numbers are absent. Use an explicit check or a schema instead when an empty string is meaningful domain data. `.unwrap().else(fallback)` is for a deliberate fallback; `.expect()` and `.panic()` throw and should not replace normal input validation or business refusals.
193
+
194
+ ### Translate dependency failures at their boundary
195
+
196
+ Use `safeTry` for a promise that can throw. Keep the original cause for bounded internal diagnostics and return an intentional message. A returned `Err` from your own code may already be safe and logged; propagate it instead of logging it repeatedly.
197
+
198
+ ```ts
199
+ import { utils, type NileLogger } from "@nilejs/nile";
200
+ import { Ok, safeTry } from "@nilejs/nile/fp";
201
+
202
+ export const readProviderStatus = async (
203
+ url: string,
204
+ signal: AbortSignal,
205
+ logger: NileLogger
206
+ ) => {
207
+ const response = await safeTry(() => fetch(url, { signal: signal }));
208
+ if (response.isErr) {
209
+ return utils.handleError({
210
+ message: "Could not check the provider status",
211
+ atFunction: "readProviderStatus",
212
+ data: utils.describeError(response.raw),
213
+ logger: logger,
214
+ });
215
+ }
216
+ return Ok({ available: response.value.ok });
217
+ };
218
+ ```
219
+
220
+ The same pattern applies to raw database promises. Nile’s model helpers already return Results; inspect those directly. `safeTry` can flatten a Result-returning callback, but a second wrapper is unnecessary when the operation already has the intended failure contract. Never look for another `isErr` inside `result.value`.
221
+
222
+ ### Choose one owner for async policy
223
+
224
+ Use `superPromise` for a dependency operation that needs Slang’s timeout, cancellation or task-group controls. Use Nile’s execution settings and `utils.createQueue`, `utils.createRateLimiter` or `utils.retryFnWithJitter` when the policy belongs to an action or shared application resource. Avoid independently stacked retries and deadlines: they multiply attempts and obscure which operation failed.
225
+
226
+ ```ts
227
+ import { superPromise, taskGroup } from "@nilejs/nile/fp";
228
+
229
+ const providerCalls = taskGroup("provider-status", { concurrency: 4 });
230
+
231
+ export const checkProvider = async (url: string, signal: AbortSignal) => {
232
+ const task = superPromise(
233
+ async (attemptSignal) => {
234
+ const response = await fetch(url, { signal: attemptSignal });
235
+ return { available: response.ok };
236
+ },
237
+ { group: providerCalls, timeout: 3000 }
238
+ );
239
+ const cancel = () => {
240
+ task.abort();
241
+ };
242
+ if (signal.aborted) {
243
+ cancel();
244
+ } else {
245
+ signal.addEventListener("abort", cancel, { once: true });
246
+ }
247
+ try {
248
+ return await task;
249
+ } finally {
250
+ signal.removeEventListener("abort", cancel);
251
+ }
252
+ };
253
+ ```
254
+
255
+ This helper is internal: its failure still needs translation before being returned publicly. A timeout or aborted Result does not roll back completed effects. Retry only operations safe to repeat, use idempotency for writes, and keep retry count, pending work and concurrency bounded. A task group limits running attempts; it does not cap all pending work or persist jobs. Use a Nile queue with explicit capacity when you also need admission control. Long-lived resources belong in the application lifecycle so shutdown can drain or close them.
256
+
257
+ ## Calling actions
258
+
259
+ - HTTP: `POST /api/services` with `{ "intent": "execute", "service": "projects", "action": "create", "payload": { ... } }`.
260
+ - Client (`@nilejs/client`): `createNileClient<Payloads>({ baseUrl })`, then `await nile.invoke({ service, action, payload })`. Every method returns a Result: `if (result.isErr) { ... return; }`, then read `result.value`. On failure `result.error` is the message and `result.details` holds the server's `data`. `data` is a deprecated alias; do not use it.
261
+ - Sockets: `createNileSocketClient<Actions, Topics>` owns its connection. Use `socket.invoke`, `socket.send` and `socket.receive`; check each Result and unsubscribe/close when finished. Declare backend topics and validate their data and scope; frontend types come from shared or generated definitions. Authorization is still a backend policy.
262
+ - Server code: `await server.invoke({ service, action, payload, credentials? })`. Protected actions need `credentials` with headers.
263
+ - Uploads: `nile.upload({ service, action, files, fields })` for `isSpecial: { contentType: "multipart/form-data" }` actions. A JSON request to such an action returns HTTP 400.
264
+
265
+ ## Organize a feature as it grows
266
+
267
+ 1. Find the domain in `services.config.ts` and read its `index.ts`. Name the action for the business operation, then define its input schema and named handler.
268
+ 2. Keep a short, single-use rule in that handler. Extract ordinary domain functions when a rule is reused, complicated enough to test independently, or coordinates a transaction. Do not create a repository, class or service layer solely because a folder exists.
269
+ 3. Put data access in the project’s model convention. `utils.createModel` and `utils.getZodSchema` are optional Drizzle helpers; a handwritten model is also valid. Pass the database or transaction explicitly where transaction ownership matters.
270
+ 4. For a growing action, move its private helpers under its action folder’s `modules/`. Put helpers shared by the domain in the service’s `modules/`. Cross-domain rules can use `business/` when useful. Do not expose a private helper as an action just to call it.
271
+ 5. Register the action in the domain’s `actions` list. An export alone does not make it reachable. Register a new service in the root registry.
272
+ 6. Carry validated input into business functions and trusted identity into policy checks. Keep request identity in scoped sessions and execution state, not process-wide context values.
273
+ 7. Verify the registered operation through `server.invoke` or a transport when changing validation, permissions or registration. Test a transaction’s rollback and idempotency when those are part of the business guarantee.
274
+
275
+ A transaction belongs to the layer that knows which writes must be atomic. Calling several actions or adding an after hook does not make the work transactional. An after hook is not a `finally` block: cleanup belongs in `finally` or the lifecycle, since after hooks do not run after every failure.
276
+
277
+ ## Anti-patterns to avoid
278
+
279
+ | Anti-pattern | Prefer |
280
+ |---|---|
281
+ | `Ok(existingResult)` or checking `result.value.isErr` | One Result boundary, then an early failure branch or a short transformation. |
282
+ | Returning a caught driver’s `result.error` directly | Log bounded diagnostics and choose a safe business-facing message. |
283
+ | `.expect()`, `.panic()` or a non-null assertion to bypass an ordinary refusal | An explicit Err, Option branch or validation schema. |
284
+ | `unwrap().else([])` on a failed write or required lookup | Preserve the failure; default only when absence is a valid answer. |
285
+ | Long chains with hidden writes and logging | Named functions and explicit Result checks around side effects. |
286
+ | Parsing or guessing business values in a before hook | Treat payloads as untrusted; rely on the action’s parsed input for business logic. |
287
+ | `context.set("user", identity)` | Scoped sessions or an application-owned request scope. |
288
+ | Creating a queue, rate limiter or task group inside every invocation | A shared resource with an explicit owner and shutdown policy. |
289
+ | Unbounded `Promise.all` over user-sized inputs | A concurrency limit and bounded pending work. |
290
+ | Memoizing authorization or mutable account state | Fresh checks or an explicit cache with appropriate invalidation. |
291
+ | Treating a generated diagnostic code as `NOT_FOUND` or another stable category | Keep domain decisions in explicit data/schema contracts; use the generated code only to find a log. |
292
+ | Assuming optional schemas, topics or hooks authorize an operation | Enforce authorization at the backend policy boundary. |
293
+
294
+ ## Protection and policy
295
+
296
+ - JWT: set `auth: { secret, method: "header" }` on the server and `isProtected: true` on the action. Both are required.
297
+ - `accessControl` lists role labels. Nile Js stores them but does not enforce them. Enforce them in a global `onBeforeActionHandler` that checks `action.accessControl` against the session's `roles`.
298
+ - Hooks: `hooks.before` and `hooks.after` run another action's handler directly. They do not run that action's JWT check, validation or hooks.
299
+ - Discovery is off by default. When `rest.discovery.secret` is configured, `explore` and `schema` need the matching `payload.discoverySecret`. Enabling discovery without a secret exposes metadata to callers.
300
+
301
+ ## Feature guides
302
+
303
+ Read the page for the feature you change before you write code:
304
+
305
+ - [Validation and errors](https://nile-js.github.io/nile/guide/features/validation-and-errors)
306
+ - [Authentication and hooks](https://nile-js.github.io/nile/guide/features/authentication-and-hooks)
307
+ - [Context and logging](https://nile-js.github.io/nile/guide/features/context-and-logging)
308
+ - [Queues, debounce and throttle](https://nile-js.github.io/nile/guide/features/queues-debounce-throttle)
309
+ - [Telemetry](https://nile-js.github.io/nile/guide/features/telemetry)
310
+ - [WebSockets](https://nile-js.github.io/nile/guide/features/websockets)
311
+ - [Cron jobs](https://nile-js.github.io/nile/guide/features/cron-jobs)
312
+ - [Database models](https://nile-js.github.io/nile/guide/features/database-models)
313
+ - [API discovery and schemas](https://nile-js.github.io/nile/guide/features/api-discovery)
314
+ - [HTTP, WebSocket and direct calls](https://nile-js.github.io/nile/guide/features/calling-actions)
315
+ - [Retries and utilities](https://nile-js.github.io/nile/guide/features/retries-and-utilities)
316
+ - [Rate limiting](https://nile-js.github.io/nile/guide/features/rate-limiting)
317
+ - [File uploads](https://nile-js.github.io/nile/guide/features/file-uploads)
318
+ - [Readiness and shutdown](https://nile-js.github.io/nile/guide/features/readiness-and-shutdown)
319
+
320
+ Advanced pages cover handler context, composed hooks, execution options and testing with `server.invoke`: https://nile-js.github.io/nile/guide/advanced/
321
+
322
+ ## Rules for agents
323
+
324
+ 1. Read `services.config.ts`, the target service’s `index.ts` and its actions before you change anything.
325
+ 2. Follow the existing pattern in that service. Do not add a second architecture.
326
+ 3. Never invent an API. Check the guide, the installed `@nilejs/nile` types in `node_modules`, or the existing code. If it is not there, say so.
327
+ 4. Use named schemas, inferred input types, named handlers, `createAction<T>` and braces on every `if`.
328
+ 5. Start with business rules in the action’s named handler. Move reusable rules or transaction coordination into ordinary domain functions when that makes them easier to share or test. Keep persistence in models or repositories; keep transaction boundaries explicit.
329
+ 6. Typecheck and run the relevant tests after a coherent change. Exercise the registered action through `server.invoke` or a transport when registration, policy or validation changes.
330
+ 7. Do not commit, publish or change secrets, `.env` values or deployment settings unless the user asks.
331
+
332
+ ## Common pitfalls
333
+
334
+ - `isProtected` without `auth` does nothing useful. Configure both.
335
+ - `accessControl` does not block anything by itself. Add a hook that checks it.
336
+ - A before hook sees the raw payload, before validation.
337
+ - A global before hook's returned value does not replace the payload. Use a local hook to transform input.
338
+ - A local hook does not protect the parent action.
339
+ - `context.set` values are shared by all requests. Never store the current user there.
340
+ - A rate limit or topic policy that is missing denies or does not apply. Read the feature page for the default.
341
+ - Multiple replicas each keep their own queues, schedules, rate-limit counters and WebSocket subscriptions.
342
+ - JWT failures return fixed messages such as `JWT authentication failed: token expired` or `JWT authentication failed: invalid token`. They never include the token or its claims, so do not add them back in your own logs.
343
+ - Import Results from `@nilejs/nile/fp`, not a separately installed `slang-ts`. Two copies of `slang-ts` produce type errors.
@@ -1,4 +1,3 @@
1
- version: "3.8"
2
1
 
3
2
  services:
4
3
  nile:
@@ -1,5 +1,3 @@
1
- version: "3.8"
2
-
3
1
  services:
4
2
  nile:
5
3
  build:
@@ -9,28 +7,10 @@ services:
9
7
  - "8000:8000"
10
8
  environment:
11
9
  - NODE_ENV=production
12
- - DB_HOST=postgres
13
- - DB_PORT=5432
14
- - DB_NAME=nile
15
- - DB_USER=postgres
16
- - DB_PASSWORD=postgres
17
- depends_on:
18
- postgres:
19
- condition: service_healthy
20
-
21
- postgres:
22
- image: postgres:16-alpine
23
- environment:
24
- POSTGRES_DB: nile
25
- POSTGRES_USER: postgres
26
- POSTGRES_PASSWORD: postgres
27
10
  volumes:
28
- - postgres_data:/var/lib/postgresql/data
29
- healthcheck:
30
- test: ["CMD-SHELL", "pg_isready -U postgres"]
31
- interval: 5s
32
- timeout: 5s
33
- retries: 5
11
+ # The starter keeps its PGlite database in ./data; this volume keeps it across restarts.
12
+ - nile_data:/app/data
13
+ restart: unless-stopped
34
14
 
35
15
  volumes:
36
- postgres_data:
16
+ nile_data:
@@ -0,0 +1,16 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Local environment
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+
9
+ # Local PGlite database and logs
10
+ data/
11
+ logs/
12
+ *.log
13
+
14
+ # Build output and generated files
15
+ dist/
16
+ .DS_Store
@@ -4,23 +4,24 @@
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "scripts": {
7
- "dev": "bun run src/index.ts",
7
+ "dev": "bun --watch src/index.ts",
8
8
  "gen": "bunx @nilejs/cli generate",
9
9
  "db:generate": "bunx drizzle-kit generate",
10
10
  "db:push": "bunx drizzle-kit push",
11
- "db:studio": "bunx drizzle-kit studio"
11
+ "db:studio": "bunx drizzle-kit studio",
12
+ "typecheck": "tsc --noEmit"
12
13
  },
13
14
  "dependencies": {
14
- "@nilejs/nile": "latest",
15
+ "@nilejs/nile": "^1.0.4",
15
16
  "@electric-sql/pglite": "^0.2.5",
16
- "drizzle-orm": "^0.45.2",
17
- "pino": "^10.3.1",
18
- "slang-ts": "^0.0.7",
19
- "zod": "^4.3.6"
17
+ "drizzle-orm": "^0.45.4",
18
+ "pino": "^10.4.0",
19
+ "zod": "^4.6.7",
20
+ "drizzle-zod": "^0.8.3"
20
21
  },
21
22
  "devDependencies": {
22
23
  "@types/bun": "latest",
23
24
  "drizzle-kit": "^0.31.10",
24
- "typescript": "^5"
25
+ "typescript": "^7.0.2"
25
26
  }
26
27
  }
@@ -1,6 +1,6 @@
1
- import { createModel } from "@nilejs/nile";
1
+ import { utils } from "@nilejs/nile";
2
2
  import { db } from "@/db/client";
3
3
  import { tasks } from "@/db/schema";
4
4
 
5
5
  /** CRUD model for tasks — auto-validates, handles errors, and supports transactions */
6
- export const taskModel = createModel(tasks, { db, name: "task" });
6
+ export const taskModel = utils.createModel(tasks, { db, name: "task" });