@palbase/backend 33.0.0 → 33.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.
Files changed (53) hide show
  1. package/dist/bin/palbase-backend.cjs.map +1 -1
  2. package/dist/bin/palbase-backend.js +4 -4
  3. package/dist/{chunk-DCDHAKF3.js → chunk-IKDONZ5D.js} +4 -4
  4. package/dist/{chunk-C4ZA5AT2.js → chunk-SI4KGEM3.js} +1 -1
  5. package/dist/{chunk-C4ZA5AT2.js.map → chunk-SI4KGEM3.js.map} +1 -1
  6. package/dist/{chunk-QMFOL2K6.js → chunk-TVCCR6SO.js} +2 -2
  7. package/dist/{chunk-N54QZER3.js → chunk-WWUG2QXF.js} +2 -2
  8. package/dist/{chunk-GOPZPM2A.js → chunk-YIQ4RS4F.js} +2 -2
  9. package/dist/db/index.cjs.map +1 -1
  10. package/dist/db/index.d.cts +1 -1
  11. package/dist/db/index.d.ts +1 -1
  12. package/dist/db/index.js +2 -2
  13. package/dist/engine/index.cjs.map +1 -1
  14. package/dist/engine/index.d.cts +3 -3
  15. package/dist/engine/index.d.ts +3 -3
  16. package/dist/engine/index.js +4 -4
  17. package/dist/{index-Bi74dcOu.d.ts → index-D-3duy8Y.d.ts} +2 -2
  18. package/dist/{index-BWgnGj68.d.cts → index-DB_nW-AV.d.cts} +19 -10
  19. package/dist/{index-g6iQyYci.d.cts → index-DLveQoOf.d.cts} +2 -2
  20. package/dist/{index-Dg10RnZO.d.ts → index-DMZG3kpo.d.ts} +19 -10
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +6 -6
  23. package/dist/index.d.ts +6 -6
  24. package/dist/index.js +4 -4
  25. package/dist/openapi/index.d.cts +2 -2
  26. package/dist/openapi/index.d.ts +2 -2
  27. package/dist/{registry-6VT5RPeO.d.cts → registry-DSTThhKf.d.cts} +1 -1
  28. package/dist/{registry-B-sxJJN0.d.ts → registry-JjF5lcj4.d.ts} +1 -1
  29. package/dist/test/index.cjs +1120 -4
  30. package/dist/test/index.cjs.map +1 -1
  31. package/dist/test/index.d.cts +1 -1
  32. package/dist/test/index.d.ts +1 -1
  33. package/dist/test/index.js +1121 -5
  34. package/dist/test/index.js.map +1 -1
  35. package/docs/README.md +123 -79
  36. package/docs/auth.md +11 -7
  37. package/docs/background.md +16 -7
  38. package/docs/database.md +22 -18
  39. package/docs/endpoints.md +26 -20
  40. package/docs/errors.md +2 -3
  41. package/docs/events.md +63 -29
  42. package/docs/getting-started.md +8 -7
  43. package/docs/llms-full.txt +399 -232
  44. package/docs/migrations.md +6 -5
  45. package/docs/routing.md +34 -11
  46. package/docs/schema.md +61 -36
  47. package/docs/services.md +30 -13
  48. package/package.json +2 -1
  49. package/stack-images.json +24 -0
  50. /package/dist/{chunk-DCDHAKF3.js.map → chunk-IKDONZ5D.js.map} +0 -0
  51. /package/dist/{chunk-QMFOL2K6.js.map → chunk-TVCCR6SO.js.map} +0 -0
  52. /package/dist/{chunk-N54QZER3.js.map → chunk-WWUG2QXF.js.map} +0 -0
  53. /package/dist/{chunk-GOPZPM2A.js.map → chunk-YIQ4RS4F.js.map} +0 -0
@@ -90,7 +90,7 @@ Code and schema move at different speeds, so three things keep them together:
90
90
  ## Your schema change is not live anywhere you have not applied it
91
91
 
92
92
  The local stack and every Environment hold their own database, and editing
93
- Editing `db/*.ts` changes none of them: `palbase db apply` is what moves the local
93
+ `db/*.ts` changes none of them: `palbase db apply` is what moves the local
94
94
  one, `palbase push` is what moves an Environment's. Until then the declaration is
95
95
  ahead of the tables.
96
96
 
@@ -165,10 +165,11 @@ existing data gets there.
165
165
 
166
166
  ## Row-Level Security
167
167
 
168
- Add `rls: true` + `policies: [policy(...)]` to a table in `db/*.ts`; the
169
- generated migration emits the `ENABLE ROW LEVEL SECURITY` + `CREATE POLICY` DDL.
170
- See [schema.md](./schema.md) for the column builders, the policy DSL, and typed
171
- `Database.public.*` access.
168
+ Add `policies: () => [policy(...)]` to a table in `db/*.ts` — a CALLBACK, not an
169
+ array — and the generated migration emits the `ENABLE ROW LEVEL SECURITY` +
170
+ `CREATE POLICY` DDL. RLS is on by default, so `rls: true` is only worth writing
171
+ as a deliberate deny-all with no policies. See [schema.md](./schema.md) for the
172
+ column builders, the policy DSL, and typed `Database.public.*` access.
172
173
 
173
174
  ### Hand-writing a policy
174
175
 
package/docs/routing.md CHANGED
@@ -4,8 +4,11 @@ Routes are declared in code with **class controllers**. A controller is a class
4
4
  decorated with `@Controller(basePath)`; each route is a method decorated with
5
5
  `@Get`/`@Post`/`@Put`/`@Patch`/`@Delete`/`@Query`. Request input + context are
6
6
  injected into the method via **parameter decorators** (`@Body`/`@QueryParams`/
7
- `@Param`/`@User`/…). Putting a controller file under `controllers/` mounts it
8
- there is no central router and no manual registration.
7
+ `@Param`/`@User`/…). A controller is mounted by being LISTED: its module names
8
+ it in `controllers`, and that list is the only place ownership is decided. A
9
+ class no module lists does not exist — the build refuses it by name, and it
10
+ never reaches the route table or the OpenAPI document. Being in a particular
11
+ folder grants nothing.
9
12
 
10
13
  ```ts
11
14
  import { Controller, Get, Post, Body, QueryParams, Param, User } from "@palbase/backend";
@@ -14,31 +17,51 @@ import { Controller, Get, Post, Body, QueryParams, Param, User } from "@palbase/
14
17
  ## Controllers — class + method decorators
15
18
 
16
19
  `@Controller(basePath)` marks the class and sets the mount path. Each route
17
- method declares its verb + subpath; the real work lives in a `services/` class
18
- (the controller method is thin).
20
+ method declares its verb + subpath; the real work lives in an `@Injectable()`
21
+ service that arrives through the CONSTRUCTOR (the controller method is thin).
19
22
 
20
23
  ```ts
21
- // controllers/places.controller.ts
24
+ // modules/places/places.controller.ts
22
25
  import { Controller, Get, Post, Body, User } from "@palbase/backend";
23
26
  import type { UserT } from "@palbase/backend";
24
- import { placeService } from "../services/place.service.js";
25
- import { ImportNearbyBody } from "../models/places/import.js";
26
- import type { PlaceSchema } from "../models/places/shared.js"; // the return TYPE names the 200 schema
27
+ import { PlaceService } from "./place.service.js";
28
+ import { ImportNearbyBody } from "./dto/import.js";
29
+ import { PlaceSchema } from "./dto/shared.js"; // the return TYPE names the 200 schema
27
30
 
28
31
  @Controller("/places")
29
- export default class PlacesController {
32
+ export class PlacesController {
33
+ constructor(private readonly places: PlaceService) {}
34
+
30
35
  @Post("/import")
31
36
  importNearby(@Body(ImportNearbyBody) body: ImportNearbyBody, @User() user: UserT): PlaceSchema {
32
- return placeService.importNearby(body.lat, body.lng, user.id);
37
+ return this.places.importNearby(body.lat, body.lng, user.id);
33
38
  }
34
39
 
35
40
  @Get("/favorites", { auth: false })
36
41
  listFavorites(): PlaceSchema[] {
37
- return placeService.listFavorites();
42
+ return this.places.listFavorites();
38
43
  }
39
44
  }
40
45
  ```
41
46
 
47
+ The class is mounted by being LISTED, and nothing else mounts it:
48
+
49
+ ```ts
50
+ // modules/places/places.module.ts
51
+ import { Module, type Token } from "@palbase/backend";
52
+
53
+ import { PlacesController } from "./places.controller.js";
54
+ import { PlaceService } from "./place.service.js";
55
+
56
+ @Module({
57
+ controllers: [PlacesController as Token],
58
+ providers: [PlaceService as Token],
59
+ exports: [],
60
+ imports: [],
61
+ })
62
+ export class PlacesModule {}
63
+ ```
64
+
42
65
  | Method name | Verb | Full path | operationId (dotted) |
43
66
  |---|---|---|---|
44
67
  | `importNearby` | POST | `/places/import` | `places.importNearby` |
package/docs/schema.md CHANGED
@@ -19,9 +19,10 @@ those values — never a dictionary, because a name in a dictionary key is a sec
19
19
  place the name is written, and a table built under a key does not yet know what
20
20
  to call itself when a sibling references it.
21
21
 
22
- Each table's only required field is `columns`; `rls` and `policies` enable
23
- [Row-Level Security](#row-level-security-rls), and `indexes` declares plain
24
- btree [indexes](#indexes).
22
+ Each table's only required field is `columns`; `policies` (a callback — see
23
+ [Row-Level Security](#row-level-security-rls)) declares the RLS rules, `rls`
24
+ toggles enforcement (default `true`), and `indexes` declares plain btree
25
+ [indexes](#indexes).
25
26
 
26
27
  ```ts
27
28
  import {
@@ -447,14 +448,28 @@ You do **not** wire anything per endpoint. Saving a file under `db/` regenerates
447
448
  of the schema, no generic, no cast:
448
449
 
449
450
  ```ts
450
- // services/room.service.ts — the layer that touches the database.
451
- import { Database } from "@palbase/backend";
451
+ // modules/rooms/room.service.ts — the layer that touches the database.
452
+ import { Database, Injectable } from "@palbase/backend";
453
+ import type { Tables } from "@palbase/backend/env";
454
+
455
+ type Room = Tables["rooms"]["row"]; // typed from your db/*.ts
452
456
 
453
- type RoomsTable = typeof Database.public.rooms; // typed from your db/*.ts
457
+ /** `Database.public.rooms` is a VALUE, and a dependency is named by its
458
+ * parameter's TYPE — so the seam is an `abstract class`, one table wide. */
459
+ export abstract class RoomRepo {
460
+ abstract insert(row: { name: string }): Promise<Room>;
461
+ }
454
462
 
463
+ @Injectable()
464
+ export class DbRoomRepo extends RoomRepo {
465
+ insert(row: { name: string }): Promise<Room> {
466
+ return Database.public.rooms.insert(row); // typed: rooms.nope ← compile error
467
+ }
468
+ }
469
+
470
+ @Injectable()
455
471
  export class RoomService {
456
- private readonly rooms: RoomsTable;
457
- constructor(rooms: RoomsTable) { this.rooms = rooms; }
472
+ constructor(private readonly rooms: RoomRepo) {}
458
473
 
459
474
  async create(name: string) {
460
475
  const room = await this.rooms.insert({ name });
@@ -462,41 +477,48 @@ export class RoomService {
462
477
  // room.nope ← compile error
463
478
  }
464
479
  }
465
-
466
480
  ```
467
481
 
468
482
  ```ts
469
- // rooms.module.ts
483
+ // modules/rooms/rooms.module.ts — the four lists that make these classes exist.
470
484
  import { Module, type Token } from "@palbase/backend";
471
- import { RoomsController } from "./controllers/rooms.controller.ts";
472
- import { RoomService } from "./services/room.service.ts";
473
-
474
- @Module({ controllers: [RoomsController as Token], providers: [RoomService as Token] })
485
+ import { RoomsController } from "./rooms.controller.ts";
486
+ import { DbRoomRepo, RoomService } from "./room.service.ts";
487
+
488
+ @Module({
489
+ controllers: [RoomsController as Token],
490
+ providers: [RoomService as Token, DbRoomRepo as Token],
491
+ exports: [],
492
+ imports: [],
493
+ })
475
494
  export class RoomsModule {}
476
495
  ```
477
496
 
478
497
  ```ts
479
- // controllers/rooms.controller.ts — HTTP only; no `Database` import here.
498
+ // modules/rooms/rooms.controller.ts — HTTP only; no `Database` import here.
480
499
  import { Controller, Post, Body, z } from "@palbase/backend";
481
- import { RoomService } from "../services/room.service.js";
500
+ import { RoomService } from "./room.service";
482
501
 
483
502
  const CreateRoomBody = z.object({ name: z.string() });
484
503
  const RoomOut = z.object({ id: z.string(), name: z.string() });
485
504
 
486
505
  @Controller("/rooms")
487
- export default class RoomsController {
506
+ export class RoomsController {
507
+ constructor(private readonly rooms: RoomService) {}
508
+
488
509
  @Post("")
489
510
  // The return type names the 200 schema — `z.infer<typeof RoomOut>` works
490
511
  // inline, no separate `export type` needed.
491
512
  create(@Body(CreateRoomBody) body: z.infer<typeof CreateRoomBody>): Promise<z.infer<typeof RoomOut>> {
492
- return roomService.create(body.name);
513
+ return this.rooms.create(body.name);
493
514
  }
494
515
  }
495
516
  ```
496
517
 
497
- `Database.public.<name>` exposes `insert`, `update(id, data)`, `delete(id)`,
498
- `findById(id)`, `findMany(query?)`. `Database.$transaction(fn)` yields a `tx`
499
- whose `tx.tables.<name>` is typed from the same schema, but carries plan
518
+ `Database.public.<name>` exposes `insert(values)`,
519
+ `update({ where: { id }, set })`, `delete(id)`, `findById(id)`,
520
+ `findMany(query?)`. `Database.$transaction(fn)` yields a `tx`
521
+ whose `tx.public.<name>` is typed from the same schema, but carries plan
500
522
  operations (`insert`/`insertMany`/`updateWhere`/`deleteWhere`/`select`) rather
501
523
  than awaited calls — see [database.md](./database.md#transactions). The raw
502
524
  string-keyed ops (`Database.$insert("rooms", …)`, `Database.$query(…)`) are still
@@ -518,10 +540,11 @@ missing `WHERE user_id = …` in your handler can no longer leak another user's
518
540
  rows — the policy enforces it. This is the recommended way to scope data per
519
541
  user.
520
542
 
521
- Add `policies` (and optionally `rls`) to a table. `policies` being non-empty
522
- implies `rls: true` automatically (a table with policies must have RLS enabled
523
- or the policies are inert). Set `rls: true` with no policies only as a
524
- deliberate deny-all intermediate step.
543
+ Add `policies` to a table a **callback** returning the list, `policies: () =>
544
+ […]`, not a bare array. RLS defaults to `true`, and a non-empty `policies` forces
545
+ it on regardless (a table with policies must have RLS enabled or the policies are
546
+ inert). Write `rls: true` with no policies only as a deliberate deny-all
547
+ intermediate step.
525
548
 
526
549
  ### The `policy()` builder
527
550
 
@@ -588,15 +611,17 @@ from `auth.uid()`, so a policy cannot ask about somebody else. A revoked role is
588
611
  gone from the very next statement; there is nothing cached.
589
612
 
590
613
  ```ts
591
- policy("notes_owner").for("all")
592
- .using("user_id = (select auth.uid())")
593
- .withCheck("user_id = (select auth.uid())"),
594
-
595
- // Moderation. Permissive policies are OR'd, so these widen only.
596
- policy("notes_moderate_read").for("select")
597
- .using("(select auth.has_permission('notes.delete_any'))"),
598
- policy("notes_delete_any").for("delete")
599
- .using("(select auth.has_permission('notes.delete_any'))"),
614
+ policies: () => [
615
+ policy("notes_owner").for("all")
616
+ .using("user_id = (select auth.uid())")
617
+ .withCheck("user_id = (select auth.uid())"),
618
+
619
+ // Moderation. Permissive policies are OR'd, so these widen only.
620
+ policy("notes_moderate_read").for("select")
621
+ .using("(select auth.has_permission('notes.delete_any'))"),
622
+ policy("notes_delete_any").for("delete")
623
+ .using("(select auth.has_permission('notes.delete_any'))"),
624
+ ],
600
625
  ```
601
626
 
602
627
  > **The read policy is not optional.** `DELETE … WHERE id = $1` has to FIND the
@@ -626,8 +651,8 @@ export const todos = defineTable("todos", {
626
651
  done: boolean().default(false),
627
652
  created_at: timestamp().defaultNow(),
628
653
  },
629
- // `policies` non-empty ⇒ RLS is enabled + FORCEd automatically.
630
- policies: [
654
+ // `policies` is a CALLBACK. Non-empty ⇒ RLS is enabled + FORCEd automatically.
655
+ policies: () => [
631
656
  // Read: a user sees only their own todos.
632
657
  policy("pb_todos_owner_select")
633
658
  .for("select")
package/docs/services.md CHANGED
@@ -3,7 +3,7 @@
3
3
  **Two different things are called "services" here, and this page is the second
4
4
  one.** Read the first paragraph before searching this page for a layout rule.
5
5
 
6
- - **YOUR service layer** — `services/<name>.service.ts`, the plain classes your
6
+ - **YOUR service layer** — `modules/<domain>/<name>.service.ts`, the `@Injectable()` classes your
7
7
  own business logic lives in. The scaffold ships one, and the contract is at
8
8
  the bottom of this page: [Your own service layer](#your-own-service-layer).
9
9
  - **THE service singletons** — the platform objects you import from
@@ -26,9 +26,26 @@ turns out to be empty — it fails at the import.
26
26
  a channel, but cannot subscribe (a stateless request can't hold a socket).
27
27
  Subscription lives on the client SDK (`pb.realtime`).
28
28
 
29
+ `Auth` is available here for exactly one thing: **granting and revoking roles**.
30
+ Signing in, signing up and session handling run on the client SDK — a backend
31
+ handler never holds a user's credentials — but assignment is server work, so
32
+ `assignRole`, `revokeRole` and `rolesOf` are on this side:
33
+
34
+ ```ts
35
+ import { Auth } from "@palbase/backend";
36
+
37
+ await Auth.assignRole(userId, "moderator"); // takes effect on the next request
38
+ await Auth.revokeRole(userId, "moderator"); // and so does this
39
+ const held = await Auth.rolesOf(userId); // string[]
40
+ ```
41
+
42
+ A role that does not exist throws `RoleNotDefined` naming it — declare it first
43
+ with `palbase roles create`. A failed read throws rather than answering `[]`:
44
+ "nobody could say" and "holds nothing" are different claims, and a handler that
45
+ confuses them denies a user everything they hold.
46
+
29
47
  **Not available to backend handlers** (do not import them here): Functions, CMS,
30
- Links, Analytics, and Auth. Auth runs on the client SDK; the others are out of
31
- scope for backend endpoints.
48
+ Links and Analytics out of scope for backend endpoints.
32
49
 
33
50
  ## Cache
34
51
 
@@ -158,7 +175,7 @@ Writes mirror the `Database` / `Database.$asService()` model:
158
175
  - `Flags.setOverride(key, value)` (default) writes an override for the **current
159
176
  request user** — no `userId` argument, no admin power. It errors on an
160
177
  anonymous request (no signed-in user).
161
- - `Flags.asService()` returns the cross-user admin surface
178
+ - `Flags.$asService()` returns the cross-user admin surface
162
179
  (`setOverrideForUser`, `setOverridesForUser`, `clearOverrideForUser`,
163
180
  `clearAllOverridesForUser`, `batchSetOverrides`) for writing overrides for an
164
181
  **arbitrary** user. Explicit and greppable, just like `Database.$asService()`.
@@ -167,8 +184,8 @@ Writes mirror the `Database` / `Database.$asService()` model:
167
184
  // Current request user — no userId needed:
168
185
  await Flags.setOverride("new-checkout", true);
169
186
 
170
- // Cross-user admin write — explicit target, via asService():
171
- await Flags.asService().setOverrideForUser("user_123", "new-checkout", true);
187
+ // Cross-user admin write — explicit target, via $asService():
188
+ await Flags.$asService().setOverrideForUser("user_123", "new-checkout", true);
172
189
  ```
173
190
 
174
191
  ## Realtime
@@ -219,8 +236,8 @@ to drive live chat, presence, dashboards, and other push features.
219
236
  ## Your own service layer
220
237
 
221
238
  The singletons above are what a service CALLS. This section is the layer that
222
- calls them — `services/<name>.service.ts`, which the scaffold ships a working
223
- example of (`services/note.service.ts` and the controller that uses it).
239
+ calls them — `modules/<domain>/<name>.service.ts`, which the scaffold ships a working
240
+ example of (`modules/notes/note.service.ts` and the controller that uses it).
224
241
 
225
242
  The contract is three rules, and the scaffold's own test enforces all three:
226
243
 
@@ -235,7 +252,7 @@ it.** Mark the class `@Injectable()` and name what it needs as ordinary
235
252
  constructor parameters:
236
253
 
237
254
  ```ts
238
- // services/note.service.ts
255
+ // modules/notes/note.service.ts
239
256
  import { Database, Injectable } from "@palbase/backend";
240
257
 
241
258
  type NotesTable = typeof Database.public.notes;
@@ -251,7 +268,7 @@ export class NoteService {
251
268
  ```
252
269
 
253
270
  ```ts
254
- // services/report.service.ts — a service that depends on another service
271
+ // modules/reports/report.service.ts — a service that depends on another service
255
272
  import { Injectable } from "@palbase/backend";
256
273
  import { NoteService } from "./note.service.ts";
257
274
 
@@ -268,7 +285,7 @@ export class ReportService {
268
285
  A controller asks the same way, and nothing wires it by hand:
269
286
 
270
287
  ```ts
271
- // controllers/notes.controller.ts
288
+ // modules/notes/notes.controller.ts
272
289
  import { Controller, Get } from "@palbase/backend";
273
290
  import { NoteService } from "../services/note.service.ts";
274
291
 
@@ -310,7 +327,7 @@ never reaches the route table, the dispatcher or the OpenAPI document.
310
327
  ```ts
311
328
  // notes.module.ts — beside the domain it owns, not in a directory we name
312
329
  import { Module, type Token } from "@palbase/backend";
313
- import { NotesController } from "./controllers/notes.controller.ts";
330
+ import { NotesController } from "./modules/notes/notes.controller.ts";
314
331
  import { NoteService } from "./services/note.service.ts";
315
332
  import { ReportService } from "./services/report.service.ts";
316
333
 
@@ -363,7 +380,7 @@ NoteService())` is ordinary TypeScript, and the container is not required for it
363
380
  to work.
364
381
 
365
382
  ```ts
366
- // services/note.service.test.ts — `npm test`, no database
383
+ // modules/notes/note.service.test.ts — `npm test`, no database
367
384
  import { test } from "node:test";
368
385
  import assert from "node:assert/strict";
369
386
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palbase/backend",
3
- "version": "33.0.0",
3
+ "version": "33.0.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": {
@@ -91,6 +91,7 @@
91
91
  "files": [
92
92
  "dist",
93
93
  "docs",
94
+ "stack-images.json",
94
95
  "stager",
95
96
  "template"
96
97
  ],
@@ -0,0 +1,24 @@
1
+ {
2
+ "33": [
3
+ {
4
+ "env": "PALBASE_PALSVC_IMAGE",
5
+ "ref": "ghcr.io/palgroup/palbase/palsvc:0.42.0",
6
+ "build": "cd v2 && DOCKER_BUILDKIT=1 docker build -t palbase-palsvc -f Dockerfile ."
7
+ },
8
+ {
9
+ "env": "PALBASE_RUNTIME_IMAGE",
10
+ "ref": "ghcr.io/palgroup/palbase/runtime-dev:0.42.0",
11
+ "build": "cd v2/runtime && DOCKER_BUILDKIT=1 docker build --target dev -t palbase-runtime-dev -f Dockerfile ."
12
+ },
13
+ {
14
+ "env": "PALBASE_EDGE_IMAGE",
15
+ "ref": "ghcr.io/palgroup/palbase/edge:0.42.0",
16
+ "build": "cd v2/deploy/envoy && DOCKER_BUILDKIT=1 docker build -t palbase-edge ."
17
+ },
18
+ {
19
+ "env": "PALBASE_POSTGRES_IMAGE",
20
+ "ref": "pgvector/pgvector:pg16",
21
+ "build": ""
22
+ }
23
+ ]
24
+ }