@palbase/backend 39.0.0 → 39.1.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.
package/docs/auth.md CHANGED
@@ -5,6 +5,26 @@ unless it opts out with `auth: false`. Client SDKs attach the user's token
5
5
  automatically, so on the backend you declare what a route needs and inject the
6
6
  user.
7
7
 
8
+ ## Temporary verification failures
9
+
10
+ An absent or invalid access token produces `401 unauthorized` on a protected
11
+ route. If the runtime cannot retrieve a usable signing keyset, it instead
12
+ returns `503 auth_unavailable`. A network failure, an unsuccessful JWKS response,
13
+ or a keyset with no usable signing keys is an availability failure, not evidence
14
+ that the caller's token is invalid. Keep the session and retry after the service
15
+ recovers; signing in again is not required while the token remains valid.
16
+
17
+ The same rule applies to upload authorization. Requests that cannot be verified
18
+ do not reach the handler or the database, and uploads receive no grant. Public
19
+ requests without credentials continue to work.
20
+
21
+ Known signing keys remain usable within their trust TTL (five minutes by
22
+ default). Expired keysets are not trusted during an outage. Concurrent refreshes
23
+ share one request, and failed refreshes and unknown key IDs share a one-second
24
+ retry interval so recovery and key rotation do not wait for the entire TTL.
25
+
26
+ ## Declaring authentication
27
+
8
28
  ```ts
9
29
  import { Controller, Get, Post, Body, User, OptionalUser } from "@palbase/backend";
10
30
  import type { UserT } from "@palbase/backend";
package/docs/database.md CHANGED
@@ -29,6 +29,56 @@ await Database.public.todos.delete(todo.id);
29
29
 
30
30
  See [schema.md](./schema.md) for the full typed-table surface.
31
31
 
32
+ ## Tenant-scoped CRUD — `defineRepository`
33
+
34
+ Most repository classes are the same two lines per method: add the tenant column
35
+ to the predicate, then unwrap the result. Writing that by hand is what makes it
36
+ forgettable, and one forgotten predicate shows one tenant's row to another.
37
+ `defineRepository` produces a BASE CLASS that writes the predicate once.
38
+
39
+ ```ts
40
+ import { defineRepository } from "@palbase/backend";
41
+
42
+ export class TodoRepository extends defineRepository(
43
+ Database.public.todos,
44
+ { tenant: "household_id" },
45
+ ) {}
46
+ ```
47
+
48
+ The subclass gets six typed methods, all scoped to the tenant you pass first:
49
+ `list(tenant)`, `find(tenant, id)`, `insert(tenant, values)`,
50
+ `update(tenant, id, patch)`, `updateScoped(tenant, id, patch)` and
51
+ `delete(tenant, id)`. Add your own methods in the subclass body as usual.
52
+
53
+ `insert` does NOT take the tenant column in its payload — the repository writes
54
+ it. `update` throws `NotFound` when nothing matches, so the caller does not need
55
+ a `null` branch; `updateScoped` runs the same predicate and returns `null`
56
+ instead, for when absence is a value.
57
+
58
+ The base class constructor takes no arguments, so the DI container resolves the
59
+ subclass by its own name with no extra metadata. `Database` stays ambient rather
60
+ than injected: it is request-scoped, so a reference captured in a constructor
61
+ would carry one request's client into another.
62
+
63
+ ### A row key that is not `id`
64
+
65
+ The row key defaults to `id`, but it is not fixed — `defineTable` does not
66
+ require an `id` column, and a primary key can sit on any column. Name it:
67
+
68
+ ```ts
69
+ export class DocRepository extends defineRepository(
70
+ Database.public.docs,
71
+ { tenant: "org_id", key: "slug" },
72
+ ) {}
73
+
74
+ await new DocRepository().find("org_1", "getting-started");
75
+ ```
76
+
77
+ The key parameter carries the column's OWN type, so a numeric key is typed
78
+ `number` rather than assumed to be a string. If the table has no `id` column and
79
+ you do not name a key, the surface refuses at compile time and the refusal says
80
+ what to do (`MissingRowKey`).
81
+
32
82
  ## Transaction boundaries and retries
33
83
 
34
84
  Ordinary Database writes commit when the request completes. `$transaction(fn)`
@@ -795,6 +795,26 @@ unless it opts out with `auth: false`. Client SDKs attach the user's token
795
795
  automatically, so on the backend you declare what a route needs and inject the
796
796
  user.
797
797
 
798
+ ## Temporary verification failures
799
+
800
+ An absent or invalid access token produces `401 unauthorized` on a protected
801
+ route. If the runtime cannot retrieve a usable signing keyset, it instead
802
+ returns `503 auth_unavailable`. A network failure, an unsuccessful JWKS response,
803
+ or a keyset with no usable signing keys is an availability failure, not evidence
804
+ that the caller's token is invalid. Keep the session and retry after the service
805
+ recovers; signing in again is not required while the token remains valid.
806
+
807
+ The same rule applies to upload authorization. Requests that cannot be verified
808
+ do not reach the handler or the database, and uploads receive no grant. Public
809
+ requests without credentials continue to work.
810
+
811
+ Known signing keys remain usable within their trust TTL (five minutes by
812
+ default). Expired keysets are not trusted during an outage. Concurrent refreshes
813
+ share one request, and failed refreshes and unknown key IDs share a one-second
814
+ retry interval so recovery and key rotation do not wait for the entire TTL.
815
+
816
+ ## Declaring authentication
817
+
798
818
  ```ts
799
819
  import { Controller, Get, Post, Body, User, OptionalUser } from "@palbase/backend";
800
820
  import type { UserT } from "@palbase/backend";
@@ -1163,6 +1183,56 @@ await Database.public.todos.delete(todo.id);
1163
1183
 
1164
1184
  See [schema.md](./schema.md) for the full typed-table surface.
1165
1185
 
1186
+ ## Tenant-scoped CRUD — `defineRepository`
1187
+
1188
+ Most repository classes are the same two lines per method: add the tenant column
1189
+ to the predicate, then unwrap the result. Writing that by hand is what makes it
1190
+ forgettable, and one forgotten predicate shows one tenant's row to another.
1191
+ `defineRepository` produces a BASE CLASS that writes the predicate once.
1192
+
1193
+ ```ts
1194
+ import { defineRepository } from "@palbase/backend";
1195
+
1196
+ export class TodoRepository extends defineRepository(
1197
+ Database.public.todos,
1198
+ { tenant: "household_id" },
1199
+ ) {}
1200
+ ```
1201
+
1202
+ The subclass gets six typed methods, all scoped to the tenant you pass first:
1203
+ `list(tenant)`, `find(tenant, id)`, `insert(tenant, values)`,
1204
+ `update(tenant, id, patch)`, `updateScoped(tenant, id, patch)` and
1205
+ `delete(tenant, id)`. Add your own methods in the subclass body as usual.
1206
+
1207
+ `insert` does NOT take the tenant column in its payload — the repository writes
1208
+ it. `update` throws `NotFound` when nothing matches, so the caller does not need
1209
+ a `null` branch; `updateScoped` runs the same predicate and returns `null`
1210
+ instead, for when absence is a value.
1211
+
1212
+ The base class constructor takes no arguments, so the DI container resolves the
1213
+ subclass by its own name with no extra metadata. `Database` stays ambient rather
1214
+ than injected: it is request-scoped, so a reference captured in a constructor
1215
+ would carry one request's client into another.
1216
+
1217
+ ### A row key that is not `id`
1218
+
1219
+ The row key defaults to `id`, but it is not fixed — `defineTable` does not
1220
+ require an `id` column, and a primary key can sit on any column. Name it:
1221
+
1222
+ ```ts
1223
+ export class DocRepository extends defineRepository(
1224
+ Database.public.docs,
1225
+ { tenant: "org_id", key: "slug" },
1226
+ ) {}
1227
+
1228
+ await new DocRepository().find("org_1", "getting-started");
1229
+ ```
1230
+
1231
+ The key parameter carries the column's OWN type, so a numeric key is typed
1232
+ `number` rather than assumed to be a string. If the table has no `id` column and
1233
+ you do not name a key, the surface refuses at compile time and the refusal says
1234
+ what to do (`MissingRowKey`).
1235
+
1166
1236
  ## Transaction boundaries and retries
1167
1237
 
1168
1238
  Ordinary Database writes commit when the request completes. `$transaction(fn)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palbase/backend",
3
- "version": "39.0.0",
3
+ "version": "39.1.1",
4
4
  "description": "Palbase Backend SDK — class controllers (@Controller/@Get/@Post + @Body/@QueryParams/@Param), error classes, schema DSL",
5
5
  "license": "MIT",
6
6
  "repository": {