@palbase/backend 17.4.0 → 18.0.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 (72) hide show
  1. package/dist/bin/palbase-backend.cjs +1848 -0
  2. package/dist/bin/palbase-backend.cjs.map +1 -0
  3. package/dist/bin/palbase-backend.d.cts +1 -0
  4. package/dist/bin/palbase-backend.d.ts +1 -0
  5. package/dist/bin/palbase-backend.js +168 -0
  6. package/dist/bin/palbase-backend.js.map +1 -0
  7. package/dist/chunk-7D4SUZUM.js +38 -0
  8. package/dist/chunk-7D4SUZUM.js.map +1 -0
  9. package/dist/chunk-N32VDWKH.js +172 -0
  10. package/dist/chunk-N32VDWKH.js.map +1 -0
  11. package/dist/chunk-POYAFBLF.js +189 -0
  12. package/dist/chunk-POYAFBLF.js.map +1 -0
  13. package/dist/chunk-QMVK4X3V.js +200 -0
  14. package/dist/chunk-QMVK4X3V.js.map +1 -0
  15. package/dist/chunk-SSGAMC26.js +342 -0
  16. package/dist/chunk-SSGAMC26.js.map +1 -0
  17. package/dist/chunk-VYH4U7ZQ.js +1138 -0
  18. package/dist/chunk-VYH4U7ZQ.js.map +1 -0
  19. package/dist/{chunk-AAN642N5.js → chunk-W5ODXPY3.js} +2 -336
  20. package/dist/chunk-W5ODXPY3.js.map +1 -0
  21. package/dist/chunk-YL4C5NRY.js +90 -0
  22. package/dist/chunk-YL4C5NRY.js.map +1 -0
  23. package/dist/db/env.cjs.map +1 -1
  24. package/dist/db/env.d.cts +21 -1
  25. package/dist/db/env.d.ts +21 -1
  26. package/dist/db/index.cjs.map +1 -1
  27. package/dist/db/index.d.cts +2 -1
  28. package/dist/db/index.d.ts +2 -1
  29. package/dist/db/index.js +9 -6
  30. package/dist/{index-VLrU7rSW.d.ts → endpoint-B0LpZixz.d.cts} +124 -685
  31. package/dist/{index-BA_oFAz9.d.cts → endpoint-B0LpZixz.d.ts} +124 -685
  32. package/dist/engine/index.cjs +1797 -0
  33. package/dist/engine/index.cjs.map +1 -0
  34. package/dist/engine/index.d.cts +7 -0
  35. package/dist/engine/index.d.ts +7 -0
  36. package/dist/engine/index.js +43 -0
  37. package/dist/engine/index.js.map +1 -0
  38. package/dist/index-B46CGNvx.d.cts +839 -0
  39. package/dist/index-BGSCWlUa.d.cts +674 -0
  40. package/dist/index-DZDUMth5.d.ts +839 -0
  41. package/dist/index-g-EzitI-.d.ts +674 -0
  42. package/dist/index.cjs +1031 -11
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +290 -532
  45. package/dist/index.d.ts +290 -532
  46. package/dist/index.js +999 -509
  47. package/dist/index.js.map +1 -1
  48. package/dist/openapi/index.cjs +6464 -0
  49. package/dist/openapi/index.cjs.map +1 -0
  50. package/dist/openapi/index.d.cts +170 -0
  51. package/dist/openapi/index.d.ts +170 -0
  52. package/dist/openapi/index.js +6248 -0
  53. package/dist/openapi/index.js.map +1 -0
  54. package/dist/registry-3BLYv4si.d.ts +338 -0
  55. package/dist/registry-Cw0YEYCg.d.cts +338 -0
  56. package/dist/test/index.js +2 -0
  57. package/dist/test/index.js.map +1 -1
  58. package/docs/database.md +16 -3
  59. package/docs/llms-full.txt +16 -3
  60. package/package.json +43 -13
  61. package/stager/package.json +4 -0
  62. package/stager/return_types.js +338 -0
  63. package/stager/stage.js +78 -0
  64. package/stager/throw_analysis.js +726 -0
  65. package/template/AGENTS.md +261 -0
  66. package/template/config/secrets.ts +24 -0
  67. package/template/controllers/health.controller.ts +30 -0
  68. package/template/db/schema.ts +35 -0
  69. package/template/package.json +18 -0
  70. package/template/tsconfig.json +30 -0
  71. package/LICENSE +0 -21
  72. package/dist/chunk-AAN642N5.js.map +0 -1
@@ -0,0 +1,261 @@
1
+ # Palbase backend project
2
+
3
+ This project runs inside the Palbase backend runtime. You author TypeScript; the
4
+ runtime discovers it and serves it as a typed HTTP API, and generates the client
5
+ SDKs your apps call. Routing is explicit: a controller is a **class** whose
6
+ methods are routes.
7
+
8
+ Everything comes from one package:
9
+
10
+ ```ts
11
+ import { Controller, Get, Post, Patch, Delete, Body, QueryParams, Param, User, z, Database, Secrets, Conflict, NotFound } from "@palbase/backend";
12
+ ```
13
+
14
+ > Needs `experimentalDecorators: true` — already set in this scaffold's
15
+ > `tsconfig.json`. They are legacy decorators; there is no `reflect-metadata`.
16
+
17
+ ## What is here
18
+
19
+ ```
20
+ controllers/<name>.controller.ts a @Controller class; each method is a route
21
+ db/schema.ts the database: tables, columns, RLS policies
22
+ config/secrets.ts the secrets this backend needs (names only)
23
+ ```
24
+
25
+ Two folders appear as the project grows, and both are conventions the tooling
26
+ already understands:
27
+
28
+ ```
29
+ models/<controller>/<endpoint>.ts zod schemas, once they outgrow the controller
30
+ services/<name>.service.ts business logic, calls Database/Cache/Storage
31
+ ```
32
+
33
+ Put logic in a service. Keep controller methods thin — they validate through the
34
+ decorator schemas and delegate. Do not call `Database` from a controller.
35
+
36
+ ## Controllers — a class of routes
37
+
38
+ `@Controller("/base", { auth? })` sets the base path and an optional default auth
39
+ for every route in the class. Each route is a method decorated with
40
+ `@Get/@Post/@Put/@Patch/@Delete/@Query("<subpath>", { auth? })`. The served path
41
+ is `base + subpath`.
42
+
43
+ **The class does not have to be exported.** `@Controller` records the class, so
44
+ importing the file IS the registration — there is no module to list it in.
45
+
46
+ The generated clients name a call `<class minus "Controller", lowercased>.<method>`:
47
+ `TodosController.list` is `pb.todos.list()`. The verb and the path do not affect
48
+ it, so restructure paths freely — but renaming a class or a method renames the
49
+ call in every app. Those two names are your public API.
50
+
51
+ ```ts
52
+ // controllers/todos.controller.ts
53
+ import { Controller, Get, Post, Patch, Delete, Body, Param, User, NotFound } from "@palbase/backend";
54
+ import type { UserT } from "@palbase/backend";
55
+ import { todoService } from "../services/todo.service";
56
+ import { TodoSchema } from "../models/todos/shared";
57
+ import { CreateTodoBody } from "../models/todos/create";
58
+
59
+ @Controller("/todos")
60
+ class TodosController {
61
+ private todos = todoService;
62
+
63
+ @Get("")
64
+ list(@User() user: UserT): Promise<TodoSchema[]> {
65
+ return this.todos.list(user.id);
66
+ }
67
+
68
+ @Post("")
69
+ create(@Body(CreateTodoBody) body: CreateTodoBody, @User() user: UserT): Promise<TodoSchema> {
70
+ return this.todos.create(user.id, body.title);
71
+ }
72
+
73
+ @Get("/{id}")
74
+ async get(@Param("id") id: string, @User() user: UserT): Promise<TodoSchema> {
75
+ const todo = await this.todos.get(user.id, id);
76
+ if (!todo) throw new NotFound("no todo with that id");
77
+ return todo;
78
+ }
79
+
80
+ @Delete("/{id}")
81
+ remove(@Param("id") id: string, @User() user: UserT): Promise<void> {
82
+ return this.todos.remove(user.id, id);
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### Parameter decorators — each request piece, injected
88
+
89
+ | Decorator | Injects | Notes |
90
+ |---|---|---|
91
+ | `@Body(schema)` | request body | annotate `: T` (the same-named `z.infer` type) |
92
+ | `@QueryParams(schema)` | query string | typed + validated → OpenAPI query params |
93
+ | `@Param("id")` | one path param | typed `string` |
94
+ | `@Headers(schema?)` | request headers | raw `Record<string,string>`; pass a schema to type + validate |
95
+ | `@User()` | the signed-in user (`: UserT`, non-null) | `@OptionalUser()` (`UserT \| null`) on `auth: false` routes |
96
+ | `@Client()` | parsed client info | name / version / platform |
97
+ | `@RequestId()` / `@TraceId()` | ids | `: string` |
98
+ | `@Req()` | the raw request | escape hatch |
99
+
100
+ > `@User()` (value) and `UserT` (type) are imported separately —
101
+ > `import { User } from "@palbase/backend"` and
102
+ > `import type { UserT } from "@palbase/backend"` — because the value name
103
+ > `User` is the decorator. Write `@User() user: UserT`.
104
+
105
+ `@Query(subpath)` is the HTTP **QUERY** method (RFC 10008): a read that is safe
106
+ and idempotent but carries a body. Its input goes in `@Body` — never combine it
107
+ with `@QueryParams`.
108
+
109
+ ### Output and errors
110
+
111
+ The 200 response **is** the method's return type, and it must NAME a zod schema.
112
+ There is no response decorator: both the runtime's output validation and the
113
+ client codegen read the annotation.
114
+
115
+ ```ts
116
+ @Get("") list(): Promise<TodoSchema[]> // array (auto-wrapped in z.array)
117
+ @Get("/{id}") one(): Promise<TodoSchema> // one object
118
+ @Get("/ping") ping(): void // no response body
119
+ ```
120
+
121
+ `: TodoSchema` (non-Promise) and `: Promise<z.infer<typeof TodoSchema>>` are
122
+ equally valid. A **missing** annotation, an inline object (`Promise<{ ok: boolean }>`),
123
+ a union, or a name with no matching zod schema in scope is a hard build error —
124
+ name the schema.
125
+
126
+ Throw a global error class from anywhere; no request object needed:
127
+
128
+ ```ts
129
+ import { NotFound, Conflict, Forbidden, Unauthorized, BadRequest, TooManyRequests } from "@palbase/backend";
130
+
131
+ throw new Conflict("title already taken"); // → 409, standard envelope
132
+ throw new BadRequest({ fields: ["title"] }); // data-first: the data, then an optional message
133
+ throw new TooManyRequests({ retryAfter: 30 }); // data-first
134
+ ```
135
+
136
+ ### Auth is on unless you opt out
137
+
138
+ Every route requires a signed-in user by default. `@Controller("/x", { auth: false })`
139
+ opens all of a class's routes; `@Get("", { auth: true })` overrides it for one.
140
+ Resolution is route → controller → default `true`.
141
+
142
+ ## Models — schemas, one file per endpoint
143
+
144
+ ```ts
145
+ // models/todos/create.ts
146
+ import { z } from "@palbase/backend";
147
+ export const CreateTodoBody = z.object({ title: z.string().min(1) });
148
+ export type CreateTodoBody = z.infer<typeof CreateTodoBody>;
149
+ ```
150
+
151
+ Each name is a zod **value** (passed to `@Body`/`@QueryParams`, named by a return
152
+ type) plus an optional same-named `z.infer` **type**, which is what lets the
153
+ annotation read `Promise<TodoSchema>` instead of `Promise<z.infer<typeof TodoSchema>>`.
154
+ Shared response schemas go in `models/<controller>/shared.ts`.
155
+
156
+ ## Services — business logic
157
+
158
+ Plain classes exported as a singleton. No DI container, no decorators.
159
+
160
+ ```ts
161
+ // services/todo.service.ts
162
+ import { Database } from "@palbase/backend";
163
+
164
+ export class TodoService {
165
+ list(userId: string) {
166
+ return Database.tables.todos.findMany({ user_id: userId });
167
+ }
168
+ create(userId: string, title: string) {
169
+ return Database.tables.todos.insert({ user_id: userId, title });
170
+ }
171
+ }
172
+
173
+ export const todoService = new TodoService();
174
+ ```
175
+
176
+ `Database.tables.<name>` gives `insert`, `update(id, data)`, `delete(id)`,
177
+ `findById(id)`, `findMany(filter?)`, and `Database.transaction(fn)` for a plan
178
+ that commits or rolls back as one.
179
+
180
+ ## The database — declared, not migrated
181
+
182
+ `db/schema.ts` is the database. There are no migration files to write, order, or
183
+ reconcile; three verbs share one engine:
184
+
185
+ - **plan** — what it would take to make the live database match the declaration
186
+ - **apply** — make it match, in one transaction
187
+ - **push** — ship the code, and REFUSE while the two still disagree
188
+
189
+ That refusal is the point: code and schema move at different speeds, and a deploy
190
+ that lands a handler against a column that does not exist yet fails at the first
191
+ request, in production. So: edit `db/schema.ts`, plan, apply, then push.
192
+
193
+ RLS is on by default and a table with no policies is deny-all. Add
194
+ `policies: [policy(...)]` so Postgres enforces ownership — a query that forgets
195
+ its `where user_id = …` still cannot read another user's rows.
196
+
197
+ After a schema change, regenerate the typed surface so `Database.tables.*` keeps
198
+ matching what is actually there (`palbase --help` names the command).
199
+
200
+ ## Config as code
201
+
202
+ `config/*.ts` declares module configuration the same git-authoritative way the
203
+ schema does, applied on deploy:
204
+
205
+ - `config/secrets.ts` — `defineSecrets({ secrets: [secret("NAME", …)] })`
206
+ - `config/storage.ts` — `defineStorage(...)` buckets
207
+ - `config/notifications.ts` — `defineNotifications(...)` providers
208
+ - `config/flags.ts` — `defineFlags(...)` feature flags
209
+
210
+ ### Secrets
211
+
212
+ A secret's **value** never enters this repository. `config/secrets.ts` declares
213
+ the NAME and whether it is required; the value lives in the environment's vault
214
+ and reaches the process through `Secrets.get("NAME")`:
215
+
216
+ ```ts
217
+ import { Secrets } from "@palbase/backend";
218
+ const dsn = await Secrets.get("SENTRY_DSN"); // string | null
219
+ ```
220
+
221
+ Declaring it is what turns a missing secret into a refused push that names it,
222
+ instead of a 500 on the first request that touches it. Never read
223
+ `process.env` for one, and never commit a `.env`.
224
+
225
+ Provider certs and keys are the same rule: author them with the guided CLI,
226
+ which uploads the file to the vault and writes only the non-secret fields into
227
+ `config/notifications.ts`.
228
+
229
+ ## Tests
230
+
231
+ `npm test` runs Node's own test runner over every `*.test.ts`. Nothing to
232
+ install and nothing to configure — Node discovers the files and strips the types
233
+ itself.
234
+
235
+ Test the **service** layer. Controllers are thin by construction, so the logic
236
+ that can be wrong lives in a service, which is a plain class you construct
237
+ directly. A service that needs `Database` takes it as a constructor argument
238
+ instead of importing the singleton, and the test passes a stand-in.
239
+
240
+ Node **strips** types, it never emits code, so TypeScript features that require
241
+ emitted code are refused outright: parameter properties (`constructor(private x: T)`),
242
+ `enum`, and `namespace` all fail with `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`. The
243
+ refusal is per FILE, so one parameter property anywhere in a test's import graph
244
+ takes down every test in that file at once — it reads like a dozen broken tests
245
+ and is one keyword. Assign fields in the constructor body; use a `const` object
246
+ or a union instead of `enum`; use a module instead of `namespace`.
247
+
248
+ Node's ESM resolver wants the extension on relative imports, so a test imports
249
+ `./todo.service.ts`, not `./todo.service`. This scaffold's `tsconfig.json`
250
+ allows that.
251
+
252
+ `npm test` answers "is the logic right". It is not the deploy's own validation —
253
+ decorators, return types, SDK major — which answers "would this ship".
254
+
255
+ ## Commands and the full reference
256
+
257
+ This file deliberately does not list the CLI surface: commands change, and a copy
258
+ inside your repository goes stale silently. `palbase --help` is authoritative.
259
+
260
+ The complete SDK reference — controllers, models, schema, services, errors,
261
+ jobs, hooks, webhooks — lives at <https://app.dev.palbase.studio/docs/backend>.
@@ -0,0 +1,24 @@
1
+ import { defineSecrets, secret } from "@palbase/backend";
2
+
3
+ // The secrets this backend needs, declared next to the code that needs them.
4
+ //
5
+ // This file holds NAMES, never values. A value lives only in the environment's
6
+ // vault and reaches the process through `Secrets.get("NAME")`; nothing here —
7
+ // and nothing in the repository this is committed to — ever holds one.
8
+ //
9
+ // Declaring the requirement is what makes a missing secret a sentence on your
10
+ // terminal instead of a 500 on the first request that touches it: `push`
11
+ // compares this list against the target environment's vault and refuses,
12
+ // naming what is missing, before anything ships.
13
+ //
14
+ // `required` defaults to true. The example below is optional because the
15
+ // backend genuinely runs without it — a required secret in a fresh scaffold
16
+ // would just block your first deploy. Delete it and declare your own.
17
+ export default defineSecrets({
18
+ secrets: [
19
+ secret("SENTRY_DSN", {
20
+ required: false,
21
+ description: "crash reporting endpoint — errors go unreported without it",
22
+ }),
23
+ ],
24
+ });
@@ -0,0 +1,30 @@
1
+ import { Controller, Get, z } from "@palbase/backend";
2
+
3
+ // A route's success response IS its return type — a NAMED zod schema, read by
4
+ // both the runtime (output validation) and the client codegen. Small projects
5
+ // declare it beside the route; once a controller grows, move the schemas to
6
+ // `models/<controller>/<endpoint>.ts` and import them.
7
+ export const HealthResponse = z.object({
8
+ status: z.string(),
9
+ });
10
+ export type HealthResponse = z.infer<typeof HealthResponse>;
11
+
12
+ // A controller is a CLASS whose methods are routes. There is no module to
13
+ // register it in and no export to remember: `@Controller` records the class,
14
+ // and importing the file is the registration.
15
+ //
16
+ // The full path is the controller base + the method subpath, so this serves
17
+ // `GET /health`. The generated clients call it `pb.health.check()` —
18
+ // `<class minus "Controller", lowercased>.<method>`, never the verb or the
19
+ // path. Renaming either renames the call in every app, so treat those two
20
+ // names as your public API.
21
+ //
22
+ // Auth is required unless a route opts out. A health check is the rare route
23
+ // that genuinely has no caller identity, hence `auth: false`.
24
+ @Controller("/health", { auth: false })
25
+ class HealthController {
26
+ @Get("")
27
+ check(): HealthResponse {
28
+ return { status: "ok" };
29
+ }
30
+ }
@@ -0,0 +1,35 @@
1
+ import { defineSchema, policy, text, timestamp, uuid } from "@palbase/backend";
2
+
3
+ // `db/schema.ts` IS the database. There are no migration files to write, order,
4
+ // or reconcile: the CLI diffs this declaration against the live database, shows
5
+ // you what it would take to make them match, applies it in one transaction, and
6
+ // REFUSES to push code while the two still disagree.
7
+ //
8
+ // Row-Level Security is on by default. A table with RLS and no policies is
9
+ // deny-all — the correct starting state, because nothing reads a table until a
10
+ // policy says who may. `notes` below is scoped to its owner; delete it and
11
+ // declare your own.
12
+ export default defineSchema({
13
+ tables: {
14
+ notes: {
15
+ columns: {
16
+ id: uuid().primaryKey().defaultRandom(),
17
+ // A real foreign key to the tenant's own `auth.users`, so deleting an
18
+ // account takes its rows with it instead of leaving orphans an erasure
19
+ // request cannot reach. The referenced id is text, so this column is.
20
+ user_id: text().notNull().referencesAuthUser("cascade"),
21
+ body: text().notNull(),
22
+ created_at: timestamp().defaultNow(),
23
+ },
24
+ policies: [
25
+ // Postgres enforces ownership, not the handler: a query that forgets
26
+ // its `where user_id = …` still cannot see another user's rows.
27
+ policy("notes_owner")
28
+ .for("all")
29
+ .to("authenticated")
30
+ .using("user_id = (select auth.uid())")
31
+ .withCheck("user_id = (select auth.uid())"),
32
+ ],
33
+ },
34
+ },
35
+ });
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "palbase-backend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "A Palbase backend — class controllers, a declared database, and the secrets it needs.",
7
+ "scripts": {
8
+ "test": "node --test --experimental-strip-types",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@palbase/backend": "latest"
13
+ },
14
+ "devDependencies": {
15
+ "@types/node": "^22",
16
+ "typescript": "^5"
17
+ }
18
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "types": ["node"],
14
+ // The route decorators are legacy parameter decorators — no reflect-metadata,
15
+ // but they do not compile without this.
16
+ "experimentalDecorators": true,
17
+ // `npm test` runs Node's own test runner, whose ESM resolver requires the
18
+ // .ts extension on relative imports. This permits it; it does not require
19
+ // it, so imports elsewhere stay extensionless.
20
+ "allowImportingTsExtensions": true
21
+ },
22
+ "include": [
23
+ "controllers/**/*.ts",
24
+ "models/**/*.ts",
25
+ "services/**/*.ts",
26
+ "config/**/*.ts",
27
+ "db/**/*.ts",
28
+ "*.d.ts"
29
+ ]
30
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024-present Palbase
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.