@browserlessapi/inferlo 0.5.0 → 0.7.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.
Files changed (138) hide show
  1. package/README.md +14 -5
  2. package/dist/ai/ai-sdk.d.ts +48 -0
  3. package/dist/ai/ai-sdk.js +235 -0
  4. package/dist/ai/ai-sdk.js.map +1 -0
  5. package/dist/ai/index.d.ts +44 -3
  6. package/dist/ai/index.js +164 -9
  7. package/dist/ai/index.js.map +1 -1
  8. package/dist/ai/public.d.ts +6 -1
  9. package/dist/ai/public.js +2 -0
  10. package/dist/ai/public.js.map +1 -1
  11. package/dist/ai/ui.d.ts +47 -0
  12. package/dist/ai/ui.js +33 -0
  13. package/dist/ai/ui.js.map +1 -0
  14. package/dist/api-version.d.ts +32 -0
  15. package/dist/api-version.js +45 -0
  16. package/dist/api-version.js.map +1 -0
  17. package/dist/app.d.ts +60 -6
  18. package/dist/app.js +310 -66
  19. package/dist/app.js.map +1 -1
  20. package/dist/astro.d.ts +60 -0
  21. package/dist/astro.js +68 -0
  22. package/dist/astro.js.map +1 -0
  23. package/dist/audit.d.ts +57 -0
  24. package/dist/audit.js +99 -0
  25. package/dist/audit.js.map +1 -0
  26. package/dist/auth.d.ts +1 -0
  27. package/dist/auth.js +1 -0
  28. package/dist/auth.js.map +1 -1
  29. package/dist/azure.d.ts +66 -0
  30. package/dist/azure.js +119 -0
  31. package/dist/azure.js.map +1 -0
  32. package/dist/cache.d.ts +96 -0
  33. package/dist/cache.js +247 -0
  34. package/dist/cache.js.map +1 -0
  35. package/dist/cli/main.js +28 -7
  36. package/dist/cli/main.js.map +1 -1
  37. package/dist/client-ip.d.ts +36 -1
  38. package/dist/client-ip.js +80 -1
  39. package/dist/client-ip.js.map +1 -1
  40. package/dist/db/memory.js +2 -1
  41. package/dist/db/memory.js.map +1 -1
  42. package/dist/db/postgres.js +4 -1
  43. package/dist/db/postgres.js.map +1 -1
  44. package/dist/db/table.d.ts +37 -5
  45. package/dist/db/table.js +96 -15
  46. package/dist/db/table.js.map +1 -1
  47. package/dist/db/types.d.ts +2 -0
  48. package/dist/describe.d.ts +54 -2
  49. package/dist/describe.js +34 -3
  50. package/dist/describe.js.map +1 -1
  51. package/dist/events.d.ts +134 -0
  52. package/dist/events.js +196 -0
  53. package/dist/events.js.map +1 -0
  54. package/dist/fastify.d.ts +10 -2
  55. package/dist/fastify.js +22 -4
  56. package/dist/fastify.js.map +1 -1
  57. package/dist/h3.d.ts +76 -0
  58. package/dist/h3.js +61 -0
  59. package/dist/h3.js.map +1 -0
  60. package/dist/hapi.d.ts +45 -0
  61. package/dist/hapi.js +57 -0
  62. package/dist/hapi.js.map +1 -0
  63. package/dist/index.d.ts +25 -5
  64. package/dist/index.js +9 -1
  65. package/dist/index.js.map +1 -1
  66. package/dist/jobs.d.ts +70 -10
  67. package/dist/jobs.js +78 -13
  68. package/dist/jobs.js.map +1 -1
  69. package/dist/koa.d.ts +51 -0
  70. package/dist/koa.js +45 -0
  71. package/dist/koa.js.map +1 -0
  72. package/dist/lambda.d.ts +110 -0
  73. package/dist/lambda.js +169 -0
  74. package/dist/lambda.js.map +1 -0
  75. package/dist/langchain.d.ts +58 -0
  76. package/dist/langchain.js +220 -0
  77. package/dist/langchain.js.map +1 -0
  78. package/dist/mcp.d.ts +1 -1
  79. package/dist/mcp.js +7 -3
  80. package/dist/mcp.js.map +1 -1
  81. package/dist/model.d.ts +23 -6
  82. package/dist/model.js +35 -6
  83. package/dist/model.js.map +1 -1
  84. package/dist/module.d.ts +52 -0
  85. package/dist/module.js +152 -0
  86. package/dist/module.js.map +1 -0
  87. package/dist/nextjs.d.ts +35 -0
  88. package/dist/nextjs.js +33 -0
  89. package/dist/nextjs.js.map +1 -0
  90. package/dist/openapi.d.ts +3 -0
  91. package/dist/openapi.js +12 -3
  92. package/dist/openapi.js.map +1 -1
  93. package/dist/permissions.d.ts +26 -0
  94. package/dist/permissions.js +78 -0
  95. package/dist/permissions.js.map +1 -0
  96. package/dist/rate-limit.d.ts +5 -3
  97. package/dist/rate-limit.js +3 -3
  98. package/dist/rate-limit.js.map +1 -1
  99. package/dist/react-router.d.ts +41 -0
  100. package/dist/react-router.js +42 -0
  101. package/dist/react-router.js.map +1 -0
  102. package/dist/redis-events.d.ts +27 -0
  103. package/dist/redis-events.js +163 -0
  104. package/dist/redis-events.js.map +1 -0
  105. package/dist/redis-jobs.d.ts +24 -0
  106. package/dist/redis-jobs.js +203 -0
  107. package/dist/redis-jobs.js.map +1 -0
  108. package/dist/redis.d.ts +30 -0
  109. package/dist/redis.js +106 -0
  110. package/dist/redis.js.map +1 -0
  111. package/dist/request-matcher.d.ts +4 -0
  112. package/dist/request-matcher.js +14 -0
  113. package/dist/request-matcher.js.map +1 -0
  114. package/dist/response-body.d.ts +6 -0
  115. package/dist/response-body.js +7 -0
  116. package/dist/response-body.js.map +1 -0
  117. package/dist/route.d.ts +50 -2
  118. package/dist/route.js +24 -0
  119. package/dist/route.js.map +1 -1
  120. package/dist/schema.d.ts +2 -2
  121. package/dist/serve.d.ts +26 -2
  122. package/dist/serve.js +111 -10
  123. package/dist/serve.js.map +1 -1
  124. package/dist/service-client.d.ts +91 -0
  125. package/dist/service-client.js +177 -0
  126. package/dist/service-client.js.map +1 -0
  127. package/dist/sessions.d.ts +74 -0
  128. package/dist/sessions.js +172 -0
  129. package/dist/sessions.js.map +1 -0
  130. package/dist/sveltekit.d.ts +72 -0
  131. package/dist/sveltekit.js +68 -0
  132. package/dist/sveltekit.js.map +1 -0
  133. package/dist/tracing.d.ts +122 -0
  134. package/dist/tracing.js +210 -0
  135. package/dist/tracing.js.map +1 -0
  136. package/llms.txt +8 -3
  137. package/package.json +78 -4
  138. package/templates/app/AGENTS.md +10 -6
@@ -0,0 +1,122 @@
1
+ /**
2
+ * OpenTelemetry tracing, with no dependency: pass the OpenTelemetry API you already use.
3
+ *
4
+ * import * as otel from "@opentelemetry/api";
5
+ * createApp({ tracing: { api: otel } })
6
+ *
7
+ * Every request gets a server span ("GET /tickets/:id") that continues the caller's trace (W3C traceparent, through
8
+ * the propagator your OpenTelemetry SDK registers), runs the handler inside it so spans you or instrumentations
9
+ * start nest under it, and puts traceId and spanId on the request's log entries. AI calls get client spans with the
10
+ * model, tokens and cost (also with createAI({ tracing }) on its own), and database queries made while a request
11
+ * or job is traced get client spans with the table and operation. Without an SDK, the API's no-op tracer is used
12
+ * and nothing is recorded.
13
+ */
14
+ import type { Adapter } from "./db/types.ts";
15
+ /** The parts of a span Inferlo uses (an @opentelemetry/api Span). */
16
+ export interface OtelSpan {
17
+ spanContext(): {
18
+ traceId: string;
19
+ spanId: string;
20
+ traceFlags: number;
21
+ };
22
+ setAttribute(key: string, value: string | number | boolean): unknown;
23
+ setStatus(status: {
24
+ code: number;
25
+ message?: string;
26
+ }): unknown;
27
+ recordException(exception: unknown): void;
28
+ updateName(name: string): unknown;
29
+ end(): void;
30
+ isRecording(): boolean;
31
+ }
32
+ /** The parts of @opentelemetry/api Inferlo uses: import * as otel from "@opentelemetry/api", then { api: otel }. */
33
+ export interface OtelApi {
34
+ trace: {
35
+ getTracer(name: string, version?: string): {
36
+ startSpan(name: string, options?: {
37
+ kind?: number;
38
+ attributes?: Record<string, string | number | boolean>;
39
+ }, context?: unknown): OtelSpan;
40
+ };
41
+ setSpan(context: unknown, span: OtelSpan): unknown;
42
+ /** The span of a context. Database spans are only started inside one (a request or a job). */
43
+ getSpan?(context: unknown): OtelSpan | undefined;
44
+ };
45
+ context: {
46
+ active(): unknown;
47
+ with<T>(context: unknown, fn: () => T): T;
48
+ };
49
+ propagation: {
50
+ extract(context: unknown, carrier: unknown, getter?: unknown): unknown;
51
+ /** Writes the trace of a context into a carrier (traceparent), so a queued job continues it. */
52
+ inject?(context: unknown, carrier: unknown, setter?: unknown): void;
53
+ };
54
+ SpanKind: {
55
+ SERVER: number;
56
+ CLIENT?: number;
57
+ CONSUMER?: number;
58
+ };
59
+ SpanStatusCode: {
60
+ ERROR: number;
61
+ };
62
+ }
63
+ export interface TracingConfig {
64
+ /** The OpenTelemetry API: import * as otel from "@opentelemetry/api", then createApp({ tracing: { api: otel } }). */
65
+ api: OtelApi;
66
+ /** The tracer name. Default: the app's name (createApp), or "inferlo" (createAI on its own). */
67
+ tracerName?: string;
68
+ }
69
+ type Attributes = Record<string, string | number | boolean>;
70
+ export interface RequestTracing {
71
+ /** Starts the server span of a request and runs fn inside it. */
72
+ run<T>(req: Request, url: URL, fn: (span: OtelSpan) => Promise<T>): Promise<T>;
73
+ }
74
+ /** Checks a tracing option (createApp or createAI) and returns it, or undefined without tracing. */
75
+ export declare function checkTracing(option: unknown, where: "createApp" | "createAI" | "serviceClient"): TracingConfig | undefined;
76
+ /** Checks createApp({ tracing }) and returns the request tracer, or undefined without tracing. */
77
+ export declare function requestTracing(option: unknown, appName: string, appVersion: string): RequestTracing | undefined;
78
+ /** Client spans for calls Inferlo makes: AI models and the database. */
79
+ export interface ClientTracing {
80
+ /** Starts a client span under the active one. The caller ends it (with end or fail). */
81
+ start(name: string, attributes: Attributes): OtelSpan;
82
+ /** Runs fn inside a client span that is active while fn runs, and ends it; errors are recorded on it. */
83
+ run<T>(name: string, attributes: Attributes, fn: (span: OtelSpan) => Promise<T>): Promise<T>;
84
+ /** Records an error on a span and ends it. errorType overrides error.type (by default the error's code or name). */
85
+ fail(span: OtelSpan, error: unknown, errorType?: string): void;
86
+ /** Runs fn with span as the active span. */
87
+ within<T>(span: OtelSpan, fn: () => T): T;
88
+ /** Writes the trace context of span (W3C traceparent, through the registered propagator) into headers. */
89
+ inject(span: OtelSpan, headers: Record<string, string>): void;
90
+ /** True while a span (a request's or a job's) is active. */
91
+ inSpan(): boolean;
92
+ }
93
+ export declare function clientTracing(config: TracingConfig, defaultName: string, version?: string): ClientTracing;
94
+ /**
95
+ * Wraps a database adapter so its queries get client spans ("SELECT tickets") while a request or job is traced.
96
+ * Queries outside a trace (startup, the job worker polling for work) are not traced. Values are never recorded,
97
+ * only the parameterised SQL of raw queries.
98
+ */
99
+ export declare function traceAdapter(adapter: Adapter, tracing: ClientTracing): Adapter;
100
+ /** traceId and spanId for log entries, when the span belongs to a real trace. */
101
+ export declare function traceFields(span: OtelSpan | undefined): {
102
+ traceId?: string;
103
+ spanId?: string;
104
+ };
105
+ /** The trace a queued job carries from where it was enqueued (W3C trace context). */
106
+ export interface JobTraceContext {
107
+ traceparent?: string;
108
+ tracestate?: string;
109
+ }
110
+ export interface JobTracing {
111
+ /** The trace of the active span, to store with a job that is enqueued now. Empty outside a trace. */
112
+ capture(): JobTraceContext;
113
+ /** Runs one attempt of a job in a span that continues the trace it was enqueued in (cron jobs start a new trace). */
114
+ run<T extends string>(job: {
115
+ id: string;
116
+ name: string;
117
+ attempts: number;
118
+ trace?: JobTraceContext;
119
+ }, fn: (fail: (error: unknown) => void) => Promise<T>): Promise<T>;
120
+ }
121
+ export declare function jobTracing(config: TracingConfig, defaultName: string, version?: string): JobTracing;
122
+ export {};
@@ -0,0 +1,210 @@
1
+ import { InferloError } from "./errors.js";
2
+ /** CLIENT and CONSUMER in @opentelemetry/api's SpanKind (the same in every 1.x release). */
3
+ const CLIENT_KIND = 2;
4
+ const CONSUMER_KIND = 4;
5
+ /** Reads incoming trace headers (traceparent, tracestate, baggage) for the propagator. */
6
+ const headerGetter = {
7
+ get(carrier, key) {
8
+ return carrier.get(key) ?? undefined;
9
+ },
10
+ keys(carrier) {
11
+ return [...carrier.keys()];
12
+ },
13
+ };
14
+ const INVALID_TRACE = "00000000000000000000000000000000";
15
+ /** Checks a tracing option (createApp or createAI) and returns it, or undefined without tracing. */
16
+ export function checkTracing(option, where) {
17
+ if (option === undefined || option === false)
18
+ return undefined;
19
+ const config = option;
20
+ const api = config?.api;
21
+ const valid = config !== null &&
22
+ typeof config === "object" &&
23
+ typeof api?.trace?.getTracer === "function" &&
24
+ typeof api.trace.setSpan === "function" &&
25
+ typeof api.context?.with === "function" &&
26
+ typeof api.context.active === "function" &&
27
+ typeof api.propagation?.extract === "function" &&
28
+ typeof api.SpanKind?.SERVER === "number" &&
29
+ typeof api.SpanStatusCode?.ERROR === "number" &&
30
+ (config.tracerName === undefined || (typeof config.tracerName === "string" && config.tracerName.length > 0));
31
+ if (!valid) {
32
+ throw new InferloError("IL6030", "Invalid tracing options: tracing needs { api } with the OpenTelemetry API.", {
33
+ fix: `Install @opentelemetry/api (and an OpenTelemetry SDK that exports the spans), then: import * as otel from "@opentelemetry/api"; ${where}({ tracing: { api: otel } }).`,
34
+ });
35
+ }
36
+ return config;
37
+ }
38
+ /** Checks createApp({ tracing }) and returns the request tracer, or undefined without tracing. */
39
+ export function requestTracing(option, appName, appVersion) {
40
+ const config = checkTracing(option, "createApp");
41
+ if (!config)
42
+ return undefined;
43
+ const api = config.api;
44
+ const tracer = api.trace.getTracer(config.tracerName ?? appName, appVersion);
45
+ return {
46
+ run(req, url, fn) {
47
+ const method = req.method.toUpperCase();
48
+ const parent = api.propagation.extract(api.context.active(), req.headers, headerGetter);
49
+ const attributes = {
50
+ "http.request.method": method,
51
+ "url.path": url.pathname,
52
+ "url.scheme": url.protocol.replace(/:$/, ""),
53
+ "server.address": url.hostname,
54
+ };
55
+ if (url.port)
56
+ attributes["server.port"] = Number(url.port);
57
+ const agent = req.headers.get("user-agent");
58
+ if (agent)
59
+ attributes["user_agent.original"] = agent;
60
+ const span = tracer.startSpan(method, { kind: api.SpanKind.SERVER, attributes }, parent);
61
+ return api.context.with(api.trace.setSpan(parent, span), () => fn(span));
62
+ },
63
+ };
64
+ }
65
+ export function clientTracing(config, defaultName, version) {
66
+ const api = config.api;
67
+ const tracer = api.trace.getTracer(config.tracerName ?? defaultName, version);
68
+ const kind = api.SpanKind.CLIENT ?? CLIENT_KIND;
69
+ const fail = (span, error, errorType) => {
70
+ span.recordException(error instanceof Error ? error : String(error));
71
+ const code = error?.code;
72
+ if (errorType)
73
+ span.setAttribute("error.type", errorType);
74
+ else if (typeof code === "string")
75
+ span.setAttribute("error.type", code);
76
+ else
77
+ span.setAttribute("error.type", error instanceof Error ? error.name : "Error");
78
+ span.setStatus({ code: api.SpanStatusCode.ERROR });
79
+ span.end();
80
+ };
81
+ const start = (name, attributes) => tracer.startSpan(name, { kind, attributes }, api.context.active());
82
+ return {
83
+ start,
84
+ fail,
85
+ within: (span, fn) => api.context.with(api.trace.setSpan(api.context.active(), span), fn),
86
+ inject(span, headers) {
87
+ api.propagation.inject?.(api.trace.setSpan(api.context.active(), span), headers);
88
+ },
89
+ async run(name, attributes, fn) {
90
+ const span = start(name, attributes);
91
+ let result;
92
+ try {
93
+ result = await api.context.with(api.trace.setSpan(api.context.active(), span), () => fn(span));
94
+ }
95
+ catch (error) {
96
+ fail(span, error);
97
+ throw error;
98
+ }
99
+ span.end();
100
+ return result;
101
+ },
102
+ inSpan() {
103
+ if (!api.trace.getSpan)
104
+ return true;
105
+ const active = api.trace.getSpan(api.context.active());
106
+ return active !== undefined && active.spanContext().traceId !== INVALID_TRACE;
107
+ },
108
+ };
109
+ }
110
+ const SQL_OPERATION = /^\s*([a-z]+)/i;
111
+ /**
112
+ * Wraps a database adapter so its queries get client spans ("SELECT tickets") while a request or job is traced.
113
+ * Queries outside a trace (startup, the job worker polling for work) are not traced. Values are never recorded,
114
+ * only the parameterised SQL of raw queries.
115
+ */
116
+ export function traceAdapter(adapter, tracing) {
117
+ const system = adapter.dialect === "postgres" ? "postgresql" : adapter.dialect;
118
+ const traced = (operation, table, query, fn) => {
119
+ if (!tracing.inSpan())
120
+ return fn();
121
+ const attributes = { "db.system.name": system, "db.operation.name": operation };
122
+ if (table)
123
+ attributes["db.collection.name"] = table;
124
+ if (query)
125
+ attributes["db.query.text"] = query;
126
+ return tracing.run(table ? `${operation} ${table}` : operation, attributes, fn);
127
+ };
128
+ const wrapped = {
129
+ ...adapter,
130
+ insert: (model, row) => traced("INSERT", model.name, undefined, () => adapter.insert(model, row)),
131
+ select: (model, query) => traced("SELECT", model.name, undefined, () => adapter.select(model, query)),
132
+ update: (model, where, patch) => traced("UPDATE", model.name, undefined, () => adapter.update(model, where, patch)),
133
+ delete: (model, where) => traced("DELETE", model.name, undefined, () => adapter.delete(model, where)),
134
+ count: (model, where) => traced("COUNT", model.name, undefined, () => adapter.count(model, where)),
135
+ };
136
+ if (adapter.transaction)
137
+ wrapped.transaction = (fn) => adapter.transaction((tx) => fn(traceAdapter(tx, tracing)));
138
+ if (adapter.sql) {
139
+ wrapped.sql = (text, params) => traced((SQL_OPERATION.exec(text)?.[1] ?? "SQL").toUpperCase(), undefined, text, () => adapter.sql(text, params));
140
+ }
141
+ return wrapped;
142
+ }
143
+ /** traceId and spanId for log entries, when the span belongs to a real trace. */
144
+ export function traceFields(span) {
145
+ const context = span?.spanContext();
146
+ if (!context || context.traceId === INVALID_TRACE)
147
+ return {};
148
+ return { traceId: context.traceId, spanId: context.spanId };
149
+ }
150
+ const carrierGetter = {
151
+ get(carrier, key) {
152
+ return key === "traceparent" || key === "tracestate" ? carrier[key] : undefined;
153
+ },
154
+ keys(carrier) {
155
+ return Object.keys(carrier);
156
+ },
157
+ };
158
+ const TRACEPARENT = /^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/;
159
+ export function jobTracing(config, defaultName, version) {
160
+ const api = config.api;
161
+ const tracer = api.trace.getTracer(config.tracerName ?? defaultName, version);
162
+ const kind = api.SpanKind.CONSUMER ?? CONSUMER_KIND;
163
+ return {
164
+ capture() {
165
+ if (!api.propagation.inject)
166
+ return {};
167
+ const carrier = {};
168
+ api.propagation.inject(api.context.active(), carrier);
169
+ const trace = {};
170
+ if (typeof carrier.traceparent === "string" && TRACEPARENT.test(carrier.traceparent)) {
171
+ trace.traceparent = carrier.traceparent;
172
+ if (typeof carrier.tracestate === "string" && carrier.tracestate.length <= 512)
173
+ trace.tracestate = carrier.tracestate;
174
+ }
175
+ return trace;
176
+ },
177
+ async run(job, fn) {
178
+ const parent = job.trace?.traceparent ? api.propagation.extract(api.context.active(), job.trace, carrierGetter) : api.context.active();
179
+ const span = tracer.startSpan(`process ${job.name}`, {
180
+ kind,
181
+ attributes: {
182
+ "messaging.system": "inferlo",
183
+ "messaging.operation.type": "process",
184
+ "messaging.destination.name": job.name,
185
+ "messaging.message.id": job.id,
186
+ "inferlo.job.attempt": job.attempts,
187
+ },
188
+ }, parent);
189
+ const fail = (error) => {
190
+ span.recordException(error instanceof Error ? error : String(error));
191
+ const code = error?.code;
192
+ span.setAttribute("error.type", typeof code === "string" ? code : error instanceof Error ? error.name : "Error");
193
+ span.setStatus({ code: api.SpanStatusCode.ERROR });
194
+ };
195
+ try {
196
+ const outcome = await api.context.with(api.trace.setSpan(parent, span), () => fn(fail));
197
+ span.setAttribute("inferlo.job.outcome", outcome);
198
+ return outcome;
199
+ }
200
+ catch (error) {
201
+ fail(error);
202
+ throw error;
203
+ }
204
+ finally {
205
+ span.end();
206
+ }
207
+ },
208
+ };
209
+ }
210
+ //# sourceMappingURL=tracing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracing.js","sourceRoot":"","sources":["../src/tracing.ts"],"names":[],"mappings":"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"}
package/llms.txt CHANGED
@@ -10,17 +10,22 @@ Key rules: routes are private unless `auth: "public"`; validate input with `t.*`
10
10
  - [Configuration](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/config.md): config({ schema, secrets, environments }), typed settings validated at startup, secrets redacted
11
11
  - [Schemas](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/schema.md): t.* types, modifiers, validation, JSON Schema
12
12
  - [Models and database](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/database.md): model(), queries, adapters, db push, transactions
13
- - [Routes](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/routes.md): options, handler context, return values, errors, middleware, deprecated routes (Deprecation and Sunset headers)
14
- - [Auth](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/auth.md): auth.resolve, roles, bearer API keys, passwords
13
+ - [Routes](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/routes.md): options, handler context, return values, errors, middleware, deprecated routes (Deprecation and Sunset headers), API versions with apiVersion("v1", routes) and an OpenAPI document per version
14
+ - [Auth](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/auth.md): auth.resolve, roles, permissions() with can() and authorize() and the route permission option, bearer API keys, passwords
15
15
  - [AI](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/ai.md): providers, aliases and fallback, structured output, streaming, embeddings, usage and budgets
16
+ - [Modules](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/modules.md): module("billing", { imports, models, routes, jobs, provides }) splits a large app into feature modules with explicit dependencies, createApp({ modules }), checked at startup and shown in describe
16
17
  - [MCP, OpenAPI and describe](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/mcp.md)
17
18
  - [CLI](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/cli.md)
18
19
  - [Testing](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/testing.md)
19
- - [Use with other frameworks](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/integrations.md): the AI module in any framework, toNodeHandler and fromNodeRequest to mount an app in Express, Fastify, NestJS or node:http, inferloAI() middleware for Hono (c.var.ai), Express (req.ai), Elysia (ai) and tRPC (ctx.ai), inferloPlugin for Fastify (request.ai, app under a prefix), InferloModule for NestJS, an AdonisJS provider (ctx.ai, config/ai.ts), InferloAI and inferloAI() for Ts.ED injection, trpcTools() for tRPC procedures as AI tools
20
+ - [Use with other frameworks](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/integrations.md): the AI module in any framework, toNodeHandler and fromNodeRequest to mount an app in Express, Fastify, NestJS or node:http (fallthrough: true passes unmatched requests to the host), inferloAI() middleware for Hono (c.var.ai), Express (req.ai), Koa (ctx.ai), Elysia (ai) and tRPC (ctx.ai), toKoaMiddleware() to mount an app in Koa, inferloPlugin for Fastify (request.ai, app under a prefix), InferloModule for NestJS, an AdonisJS provider (ctx.ai, config/ai.ts), InferloAI and inferloAI() for Ts.ED injection, trpcTools() for tRPC procedures as AI tools, toNextHandlers() for Next.js App Router route handlers, an Encore.ts raw endpoint serving the app with toNodeHandler(app, { prefix }) and createAI in Encore services
20
21
  - [Migrate from Express](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/migrate-from-express.md): move an Express app to Inferlo route by route at the same URLs, with a concept mapping table, shared auth and tests
22
+ - [Migrate from NestJS](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/migrate-from-nestjs.md): move a NestJS app to Inferlo route by route at the same URLs with toNodeHandler(app, { fallthrough: true }), Nest modules, providers, guards, pipes and DTOs mapped to Inferlo modules, plain functions, auth and schemas
23
+ - [Migrate from Hono](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/migrate-from-hono.md): move a Hono app to Inferlo route by route at the same URLs with hono.all("*", (c) => inferlo.fetch(c.req.raw)), Hono validators, middleware, sub-apps and basePath mapped to Inferlo schemas, createApp options, modules and apiVersion
24
+ - [Migrate from Koa](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/migrate-from-koa.md): move a Koa app to Inferlo route by route at the same URLs with toKoaMiddleware(app, { fallthrough: true }) before the Koa router, @koa/router routes, ctx.throw, body parsers and Koa middleware mapped to Inferlo routes, schemas, errors and createApp options
21
25
  - [Security](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/security.md): secure defaults and security headers (createApp({ secureHeaders }))
22
26
  - [Observability](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/observability.md): request ids (X-Request-Id, ctx.requestId), request logs and JSON logs with createApp({ logging: { format: "json", requests: true } }), your own logger, Prometheus metrics with createApp({ metrics: { token } })
23
27
  - [Deployment](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/deploy.md): Node, Bun, Deno, edge, health and readiness endpoints, graceful shutdown with app.close()
28
+ - [Performance](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/performance.md): reproducible throughput and latency benchmark against Fastify, Hono, Express and NestJS (npm run bench:perf) and the latest results
24
29
  - [Support policy](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/support.md): semantic versioning, which versions get fixes, LTS after 1.0, deprecation rules and supported runtime, database and framework versions
25
30
  - [Error codes](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/errors.md): every ILxxxx code with its fix
26
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserlessapi/inferlo",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "The enterprise-ready TypeScript backend framework, with AI models built in: routing, validation, PostgreSQL and migrations, auth, rate limiting, jobs, OpenAPI and MCP, plus Anthropic, OpenAI, Gemini, Ollama and OpenAI-compatible models.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,6 +43,14 @@
43
43
  "types": "./dist/ai/public.d.ts",
44
44
  "default": "./dist/ai/public.js"
45
45
  },
46
+ "./astro": {
47
+ "types": "./dist/astro.d.ts",
48
+ "default": "./dist/astro.js"
49
+ },
50
+ "./azure": {
51
+ "types": "./dist/azure.d.ts",
52
+ "default": "./dist/azure.js"
53
+ },
46
54
  "./elysia": {
47
55
  "types": "./dist/elysia.d.ts",
48
56
  "default": "./dist/elysia.js"
@@ -55,14 +63,42 @@
55
63
  "types": "./dist/fastify.d.ts",
56
64
  "default": "./dist/fastify.js"
57
65
  },
66
+ "./h3": {
67
+ "types": "./dist/h3.d.ts",
68
+ "default": "./dist/h3.js"
69
+ },
70
+ "./hapi": {
71
+ "types": "./dist/hapi.d.ts",
72
+ "default": "./dist/hapi.js"
73
+ },
58
74
  "./hono": {
59
75
  "types": "./dist/hono.d.ts",
60
76
  "default": "./dist/hono.js"
61
77
  },
78
+ "./lambda": {
79
+ "types": "./dist/lambda.d.ts",
80
+ "default": "./dist/lambda.js"
81
+ },
82
+ "./koa": {
83
+ "types": "./dist/koa.d.ts",
84
+ "default": "./dist/koa.js"
85
+ },
86
+ "./nextjs": {
87
+ "types": "./dist/nextjs.d.ts",
88
+ "default": "./dist/nextjs.js"
89
+ },
62
90
  "./nestjs": {
63
91
  "types": "./dist/nestjs.d.ts",
64
92
  "default": "./dist/nestjs.js"
65
93
  },
94
+ "./react-router": {
95
+ "types": "./dist/react-router.d.ts",
96
+ "default": "./dist/react-router.js"
97
+ },
98
+ "./sveltekit": {
99
+ "types": "./dist/sveltekit.d.ts",
100
+ "default": "./dist/sveltekit.js"
101
+ },
66
102
  "./tsed": {
67
103
  "types": "./dist/tsed.d.ts",
68
104
  "default": "./dist/tsed.js"
@@ -78,6 +114,10 @@
78
114
  "./testing": {
79
115
  "types": "./dist/testing.d.ts",
80
116
  "default": "./dist/testing.js"
117
+ },
118
+ "./langchain": {
119
+ "types": "./dist/langchain.d.ts",
120
+ "default": "./dist/langchain.js"
81
121
  }
82
122
  },
83
123
  "bin": {
@@ -93,26 +133,45 @@
93
133
  "scripts": {
94
134
  "build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/cli/main.js",
95
135
  "prepare": "npm run build",
96
- "typecheck": "tsc --noEmit && tsc --noEmit -p examples/helpdesk && tsc --noEmit -p examples/with-hono && tsc --noEmit -p examples/with-express && tsc --noEmit -p examples/with-fastify && tsc --noEmit -p examples/with-elysia && tsc --noEmit -p examples/with-nestjs && tsc --noEmit -p examples/with-trpc && tsc --noEmit -p examples/with-adonisjs && tsc --noEmit -p examples/with-tsed",
136
+ "typecheck": "tsc --noEmit && tsc --noEmit -p examples/helpdesk && tsc --noEmit -p examples/with-hono && tsc --noEmit -p examples/with-express && tsc --noEmit -p examples/with-fastify && tsc --noEmit -p examples/with-elysia && tsc --noEmit -p examples/with-nestjs && tsc --noEmit -p examples/with-trpc && tsc --noEmit -p examples/with-adonisjs && tsc --noEmit -p examples/with-tsed && tsc --noEmit -p examples/with-encore && tsc --noEmit -p examples/with-koa && tsc --noEmit -p examples/with-nextjs && tsc --noEmit -p examples/with-nitro && tsc --noEmit -p examples/with-sveltekit && tsc --noEmit -p examples/with-react-router && tsc --noEmit -p examples/with-lambda && tsc --noEmit -p examples/with-hapi && tsc --noEmit -p examples/with-azure && tsc --noEmit -p examples/with-cloud-run-functions && tsc --noEmit -p examples/with-cloudflare-workers && tsc --noEmit -p examples/with-vercel && tsc --noEmit -p examples/with-netlify && tsc --noEmit -p examples/with-astro && tsc --noEmit -p examples/with-drizzle",
97
137
  "test": "node --test --test-reporter=spec \"test/**/*.test.ts\" \"examples/*/test/*.test.ts\"",
98
138
  "test:bun": "bun test test/",
139
+ "test:deno": "deno test -A --no-check --ignore=test/tsed.test.ts test/ && deno test -A --no-check --ignore=examples/with-tsed examples/ && deno test -A --no-check --unsafe-proto test/tsed.test.ts examples/with-tsed/",
99
140
  "llms": "node scripts/build-llms.ts",
100
141
  "check": "npm run typecheck && npm test && node scripts/build-llms.ts --check",
101
- "bench": "node bench/run.ts"
142
+ "bench": "node bench/run.ts",
143
+ "bench:perf": "node bench/perf/run.ts",
144
+ "bench:perf:check": "node bench/perf/check.ts"
102
145
  },
103
146
  "peerDependencies": {
147
+ "@langchain/core": ">=0.3.0",
148
+ "@opentelemetry/api": ">=1.3.0",
104
149
  "pg": ">=8"
105
150
  },
106
151
  "peerDependenciesMeta": {
152
+ "@langchain/core": {
153
+ "optional": true
154
+ },
155
+ "@opentelemetry/api": {
156
+ "optional": true
157
+ },
107
158
  "pg": {
108
159
  "optional": true
109
160
  }
110
161
  },
111
162
  "devDependencies": {
112
163
  "@adonisjs/core": "^6.21.0",
164
+ "@ai-sdk/openai-compatible": "^3.0.85",
165
+ "@azure/functions": "^4.17.0",
166
+ "@google-cloud/functions-framework": "^5.0.5",
167
+ "@hapi/hapi": "^21.4.13",
168
+ "@hono/node-server": "^2.1.6",
169
+ "@langchain/core": "^1.3.5",
113
170
  "@nestjs/common": "^12.3.3",
114
171
  "@nestjs/core": "^12.3.3",
115
172
  "@nestjs/platform-express": "^12.3.3",
173
+ "@opentelemetry/api": "^1.10.0",
174
+ "@sveltejs/kit": "^2.70.3",
116
175
  "@trpc/server": "^11.24.0",
117
176
  "@tsed/core": "^8.45.0",
118
177
  "@tsed/di": "^8.45.0",
@@ -120,17 +179,32 @@
120
179
  "@tsed/platform-http": "^8.45.0",
121
180
  "@tsed/platform-params": "^8.45.0",
122
181
  "@tsed/schema": "^8.45.0",
182
+ "@types/autocannon": "^7.12.7",
123
183
  "@types/express": "^5.0.6",
184
+ "@types/koa": "^3.0.3",
124
185
  "@types/node": "^22.18.0",
125
186
  "@types/pg": "^8.15.0",
187
+ "ai": "^7.0.163",
188
+ "astro": "^7.4.7",
189
+ "autocannon": "^8.0.0",
190
+ "drizzle-orm": "^0.45.4",
126
191
  "elysia": "^1.4.30",
192
+ "encore.dev": "^1.58.8",
127
193
  "express": "^5.3.0",
128
194
  "fastify": "^5.12.8",
195
+ "h3": "^2.0.8",
129
196
  "hono": "^4.13.16",
197
+ "koa": "^3.3.0",
198
+ "miniflare": "^4.20260730.0",
199
+ "next": "^16.4.5",
130
200
  "pg": "^8.16.0",
201
+ "react": "^19.3.1",
202
+ "react-router": "^8.5.1",
203
+ "redis": "^6.3.1",
131
204
  "reflect-metadata": "^0.2.2",
132
205
  "rxjs": "^7.8.2",
133
- "typescript": "^5.9.3"
206
+ "typescript": "^5.9.3",
207
+ "zod": "^4.6.7"
134
208
  },
135
209
  "publishConfig": {
136
210
  "access": "public"
@@ -30,22 +30,26 @@ Do not finish until `inferlo check` passes.
30
30
  ## Rules
31
31
 
32
32
  1. **One way to do each thing.** Routes: `route.get/post/put/patch/delete(path, options).handle(fn)`. Validation: `t.*` schemas. Database: `db.<table>.<method>`. AI: `ai.generate` / `ai.stream` / `ai.embed`.
33
- 2. **Routes are private by default.** A route without `auth` needs a signed-in user. Use `auth: "public"` only for routes anyone may call, and `auth: "<role>"` for role checks.
33
+ 2. **Routes are private by default.** A route without `auth` needs a signed-in user. Use `auth: "public"` only for routes anyone may call, and `auth: "<role>"` for role checks. Browser sign-in uses cookie sessions: `export const session = sessions({ secret: settings.SESSION_SECRET, user: ({ userId }, { db }) => ... })`, `createApp({ auth: session })`, `await session.signIn(ctx, user.id)` in the login route and `session.signOut(ctx)` in logout; browsers send the CSRF token in `X-CSRF-Token` or a `_csrf` form field on POST, PUT, PATCH and DELETE (403 IL3005 without it).
34
34
  3. **Validate every input with a schema.** `body`, `query` and `params` options. Reuse model schemas: `Ticket.pick("title", "body")`, `Ticket.omit("ownerId")`, `.partial()` for PATCH.
35
- 4. **Check ownership.** Fetch with `db.x.get(id)` (throws 404) and compare `ownerId` to `user.id`; throw `notFound()` when it does not match, so other users' ids are not revealed.
35
+ 4. **Check ownership.** Fetch with `db.x.get(id)` (throws 404) and compare `ownerId` to `user.id`; throw `notFound()` when it does not match, so other users' ids are not revealed. When more than one rule decides access (owners, admins, teams), define named permissions once in `src/permissions.ts` with `export const access = permissions({ "notes.update": ({ user, note }) => note.ownerId === user.id || (user.roles ?? []).includes("admin") })`, pass `permissions: access` to `createApp`, and call `await access.authorize("notes.update", { user, note })` in routes and jobs (403 IL3004 when denied). Permissions that need only the user go on the route: `{ permission: "reports.view" }`; add `permissions: typeof access` to `Register` (with `import type`) so permission names are type-checked. When changes must be on record (who changed what), add `AuditLog` to the models and call `await audit("notes.update", { model: "notes", before: note, after: updated })` after the change, in routes and jobs (jobs pass `userId`).
36
36
  5. **Related rows:** a `t.ref` field named `<name>Id` is a relation: `db.notes.findMany({ include: { owner: true } })` adds `note.owner` with one extra query, and `db.users.findMany({ include: { notes: true } })` adds `user.notes` (the rows that point to each user). Never load related rows in a loop. For lists clients page through, use `db.notes.paginate({ where, orderBy: { createdAt: "desc" }, cursor: query.cursor, limit: query.limit })`, which returns `{ items, nextCursor }`.
37
- 6. **Never write raw SQL with string concatenation.** Use the table methods. If you really need SQL, use `db.$sql(text, params)` with `$1` placeholders.
37
+ 6. **Never write raw SQL with string concatenation.** Use the table methods. If you really need SQL, use `db.$sql(text, params)` with `$1` placeholders. Code that takes a pg client (Drizzle, plain `pg`) gets it from `db.$pgClient()`, and inside `db.$transaction(async (tx) => ...)` from `tx.$pgClient()`, so it runs in the same transaction.
38
38
  7. **Model ids, timestamps:** every model already has `id`, `createdAt`, `updatedAt`. Never define them or send them as input.
39
39
  8. **New field?** Add it to the model in `src/models.ts`, then run `npx inferlo db push` in development, or `npx inferlo db migrate <name>` to write a migration file for production (applied with `npx inferlo db migrate --apply`). Use `.optional()` or `.default()` for new fields on existing tables. Never edit `migrations/snapshot.json`, and never edit a migration file that has been applied (`--apply` warns with IL4018 and does not run the change): put changes in a new migration. Removing a model or field needs `npx inferlo db migrate <name> --allow-drop` (it deletes that data); never use it for a rename. Changing a field's type or default, making it required or optional, or changing an enum's values needs `--allow-change`; give a newly required field a `.default()` so existing NULL rows get a value, and before removing an enum value add an `UPDATE` in the new migration file (before its `ADD CONSTRAINT`) that moves rows off it (IL4021).
40
40
  9. **New resource?** `npx inferlo add resource <plural_name>` generates the model and CRUD routes. Then edit them.
41
- 10. **AI calls** use aliases from `src/app.ts` (`model: "fast"`), never hard-coded provider model ids in routes. Use `output: schema` for structured data and `for: user.id` so usage is tracked per user. To let the model act, pass routes as tools: `ai.generate({ prompt, tools: [createNote, getNote], for: user.id })`. Each tool call runs as the current user with that route's auth and validation. For work that is not a route (an external API, a calculation), make a function tool: `tool("getWeather", { description, input: t.object({ ... }) }).handle(async ({ input }) => ...)`. To send images: `ai.generate({ prompt, images: [{ url }] })` or `[{ base64, mediaType: "image/png" }]`. Give routes that call AI models, send email or check passwords a `rateLimit: { max, per, by }` (IL2005 is the 429 it returns).
41
+ 10. **AI calls** use aliases from `src/app.ts` (`model: "fast"`), never hard-coded provider model ids in routes. Use `output: schema` for structured data and `for: user.id` so usage is tracked per user. A provider without a built-in adapter (Mistral, Bedrock, Azure, Vertex, Groq, ...) is added through its Vercel AI SDK package: `providers: { mistral: aiSdk(createMistral({ apiKey: settings.MISTRAL_API_KEY })) }`, then aliases name `"mistral:<model id>"`. When the app uses LangChain agents, give them Inferlo's model and tools: `toLangChainModel(ai, { model: "fast", for: user.id })` and `toLangChainTools([getWeather, createNote], { ai })` from `@browserlessapi/inferlo/langchain`, never a second set of provider keys. To let the model act, pass routes as tools: `ai.generate({ prompt, tools: [createNote, getNote], for: user.id })`. Each tool call runs as the current user with that route's auth and validation. For work that is not a route (an external API, a calculation), make a function tool: `tool("getWeather", { description, input: t.object({ ... }) }).handle(async ({ input }) => ...)`. To send images: `ai.generate({ prompt, images: [{ url }] })` or `[{ base64, mediaType: "image/png" }]`. Give routes that call AI models, send email or check passwords a `rateLimit: { max, per, by }` (IL2005 is the 429 it returns). Streaming to a chat UI built with the Vercel AI SDK (`useChat`): `route.post("/chat", { body: uiChatBody }).handle(({ body, ai, user }) => ai.stream({ model: "fast", messages: fromUIMessages(body.messages), for: user.id }).toResponse({ format: "ai-sdk" }))`.
42
42
  11. **Errors:** throw `notFound()`, `forbidden()`, `badRequest()` or `httpError(status, message)`. Every Inferlo error has a code (ILxxxx), a fix and a docs link: read the fix before changing code.
43
43
  12. **Imports:** relative imports end in `.ts` (`import { Note } from "../models.ts"`).
44
44
  13. **Configuration:** read environment variables only through `settings` from `src/config.ts`. A new variable is a new field in its `t.object({ ... })` schema (`.default(...)` or `.optional()` unless the app cannot start without it), and API keys, passwords and connection URLs go in `secrets` so they are never logged. Never read `process.env` in routes.
45
45
  14. **Expose to AI agents:** add `tool: true` to a route to make it an MCP tool at `/mcp`. Same auth applies.
46
- 15. **Slow work after the response** (email, slow AI calls) is a background job: `export const x = job("send-email", { input: t.object({ ... }) }).handle(async ({ input, db, ai }) => { ... })` in `src/jobs.ts`, registered with `createApp({ jobs: [jobs] })`, queued with `await ctx.jobs.enqueue(x, { ... })`. Pass ids in the input, not rows. In tests, run queued jobs with `await app.jobs.runDue()`. Jobs run in the server process (`app.jobs.start()` in `src/server.ts`); with `createApp({ jobQueue: { store: "database" } })` they are kept in Postgres and `npm run worker` (`inferlo worker`) runs them in separate processes. Scheduled work is a cron next to its job: `export const nightly = cron("0 3 * * *", cleanupJob)` (UTC; the job's input must be `t.object({})`).
46
+ 15. **Slow work after the response** (email, slow AI calls) is a background job: `export const x = job("send-email", { input: t.object({ ... }) }).handle(async ({ input, db, ai }) => { ... })` in `src/jobs.ts`, registered with `createApp({ jobs: [jobs] })`, queued with `await ctx.jobs.enqueue(x, { ... })`. Pass ids in the input, not rows. In tests, run queued jobs with `await app.jobs.runDue()`. Jobs run in the server process (`app.jobs.start()` in `src/server.ts`); with `createApp({ jobQueue: { store: "database" } })` they are kept in Postgres (or with `createApp({ redis, jobQueue: { store: "redis" } })` in Redis) and `npm run worker` (`inferlo worker`) runs them in separate processes. Scheduled work is a cron next to its job: `export const nightly = cron("0 3 * * *", cleanupJob)` (UTC; the job's input must be `t.object({})`). When other parts of the app react to something that happened, publish an event instead of enqueuing each job: `export const orderPaid = event("order.paid", { payload: t.object({ ... }) })` in `src/events.ts`, jobs subscribed with `on(orderPaid, sendReceipt)` (the job's input is `orderPaid.payload`) in `src/jobs.ts`, and `await ctx.events.publish(orderPaid, { ... })` after the change is saved. To send events to other services, give each service a `name` and set `createApp({ name, redis, events: { transport: "redis" } })`; subscribers use the same `on(event, job)`.
47
47
  16. **Security headers and health:** every response already gets security headers (CSP, HSTS over HTTPS, nosniff, frame and referrer policies) and `GET /health` and `GET /ready` are built in (`health: true` in `src/app.ts`). Change a header with `createApp({ secureHeaders: { "Content-Security-Policy": "..." } })` instead of setting it in every route, never turn them all off without a proxy that adds them, and never add your own `/health` route. On SIGTERM, `src/server.ts` calls `app.close()`, which finishes in-flight requests and jobs first. Prometheus metrics (`GET /metrics`: requests, jobs, AI calls, tokens and cost) are served when `METRICS_TOKEN` is set; never add your own `/metrics` route.
48
- 17. **Replacing a route:** do not delete a route clients use. Add the new one and mark the old one `deprecated: { since: "YYYY-MM-DD", sunset: "YYYY-MM-DD", link: "https://..." }` in its options; it then sends the standard Deprecation and Sunset headers and is marked in OpenAPI and MCP. Remove it after the sunset date.
48
+ 17. **Large apps: feature modules.** When the app grows to several features, group each feature's models, routes and jobs in `src/<feature>/module.ts` with `export const billing = module("billing", { imports: [users], models: [...], routes: [routes], jobs: [jobs], provides: { createInvoice } })` and register it with `createApp({ modules: [users, billing] })`. A module that uses another module's models or `provides` lists it in `imports` (IL6023 and IL6024 tell you when one is missing). Call other modules only through their `provides`; never import their internal files. Keep every model in `Register` in `src/models.ts`.
49
+ 18. **Replacing a route:** do not delete a route clients use. Add the new one and mark the old one `deprecated: { since: "YYYY-MM-DD", sunset: "YYYY-MM-DD", link: "https://..." }` in its options; it then sends the standard Deprecation and Sunset headers and is marked in OpenAPI and MCP. Remove it after the sunset date. For a breaking change to many routes, publish a new API version instead: `createApp({ routes: [apiVersion("v1", [notesV1]), apiVersion("v2", [notes])] })` serves them under `/v1` and `/v2`, each with its own OpenAPI document (`/v1/openapi.json`); write route paths without the version prefix.
50
+ 19. **Caching:** for results that are slow or expensive to compute, use the built-in cache instead of a module-level `Map`: `` return cache.getOrSet(`notes:stats:${user.id}`, { ttlMs: 60_000, tags: ["notes"] }, () => computeStats(db, user.id)) `` in routes and jobs (`ctx.cache`), and `await cache.invalidate("notes")` after notes change. `ttlMs` is required; put the user id in the key of anything that differs per user; values come back as JSON (dates as strings). To cache a whole GET route instead, add the route option `cache: { ttlMs: 60_000, tags: ["notes"] }` (one response per user by default; `by: "public"` only for data that is the same for everyone) and invalidate the tag where the data changes. With several instances, use `createApp({ cache: { store: "database" } })`.
51
+ 20. **Calling another Inferlo service:** `const tickets = serviceClient({ baseUrl: settings.TICKETS_URL, token: settings.TICKETS_TOKEN })`, then in a handler `await tickets.call(getTicket, { params: { id }, ctx })` with the other service's route imported, so the call is typed. Catch `ServiceCallError` and check `error.remoteStatus` for answers you expect (404). Never build these URLs with `fetch` by hand. When the app uses `tracing: { api: otel }`, pass the same `tracing` to `serviceClient` so calls join the trace.
52
+ 21. **Data of many organisations (multi-tenant):** give the signed-in user a `tenantId` (the organisation's id) and make tables whose rows belong to one organisation tenant-scoped: `model("projects", { ... }, { tenant: true })`. `ctx.db` then filters every query by and stamps every insert with the user's tenant, so never add `tenantId` to a `where`, a body schema or an insert. Jobs queued (or events published) by a request run for the request's tenant: their `ctx.db` is scoped the same way, so do not pass the tenant id in the job input. In scripts use `app.db.$tenant(tenantId).projects`, and `app.jobs.enqueue(job, input, { tenantId })`. For limits and caches shared by an organisation use `rateLimit: { max, per, by: "tenant" }` and `cache: { ttlMs, by: "tenant" }`; `ctx.audit` records the tenant itself.
49
53
 
50
54
  ## Testing
51
55