@palbase/backend 1.0.0 → 2.0.2
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/README.md +24 -0
- package/dist/{chunk-7N5ICXCB.js → chunk-L36JLUPO.js} +11 -2
- package/dist/chunk-L36JLUPO.js.map +1 -0
- package/dist/{endpoint-yn2xcrWu.d.cts → endpoint-BlcY2xNA.d.cts} +11 -11
- package/dist/{endpoint-knNIeovM.d.ts → endpoint-Djk5L6G2.d.ts} +11 -11
- package/dist/index.cjs +14 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -16
- package/dist/index.d.ts +44 -16
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +5 -1
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +1 -1
- package/dist/test/index.d.ts +1 -1
- package/dist/test/index.js +1 -1
- package/docs/README.md +51 -0
- package/docs/background.md +56 -0
- package/docs/database.md +60 -0
- package/docs/endpoints.md +116 -0
- package/docs/errors.md +48 -0
- package/docs/events.md +59 -0
- package/docs/getting-started.md +49 -0
- package/docs/llms-full.txt +713 -0
- package/docs/llms.txt +16 -0
- package/docs/routing.md +34 -0
- package/docs/schema.md +82 -0
- package/docs/services.md +105 -0
- package/package.json +5 -3
- package/dist/chunk-7N5ICXCB.js.map +0 -1
package/dist/test/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as DBClient, l as PBRequest, d as PalbaseModuleClients, L as Logger, C as CacheClient, Q as QueueClient, U as User } from '../endpoint-
|
|
1
|
+
import { D as DBClient, l as PBRequest, d as PalbaseModuleClients, L as Logger, C as CacheClient, Q as QueueClient, U as User } from '../endpoint-Djk5L6G2.js';
|
|
2
2
|
import 'zod';
|
|
3
3
|
import '../schema-BqfEhIC0.js';
|
|
4
4
|
|
package/dist/test/index.js
CHANGED
package/docs/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Palbase Backend SDK (`@palbase/backend`)
|
|
2
|
+
|
|
3
|
+
Write your backend as TypeScript files. Palbase discovers them by path, runs
|
|
4
|
+
them inside the managed backend runtime, and exposes them as a typed HTTP API.
|
|
5
|
+
|
|
6
|
+
This is **not** Express, Fastify, or a Supabase Edge Function. There is no
|
|
7
|
+
`app.get(...)`, no manual route registration, no `import express`. You export
|
|
8
|
+
definitions; the runtime wires them up.
|
|
9
|
+
|
|
10
|
+
## The two mental models (important)
|
|
11
|
+
|
|
12
|
+
| You are writing… | Handler receives | Services come from |
|
|
13
|
+
|------------------|------------------|--------------------|
|
|
14
|
+
| **Endpoints** (`endpoints/**`) | a single `req` ([PBRequest](./endpoints.md)) | imported singletons: `import { Database } from "@palbase/backend"` |
|
|
15
|
+
| **Workers, Jobs, Hooks, Webhooks** | a `ctx` object | `ctx.db`, `ctx.log`, `ctx.cache`, `ctx.queue` |
|
|
16
|
+
|
|
17
|
+
Endpoints use `req` + imported singletons. Everything else uses `ctx`. Do not
|
|
18
|
+
mix them: there is no `ctx` inside an endpoint handler, and no imported
|
|
19
|
+
`Database` singleton call inside a worker (use `ctx.db`).
|
|
20
|
+
|
|
21
|
+
## Project shape
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
my-backend/
|
|
25
|
+
├── package.json # depends on @palbase/backend
|
|
26
|
+
├── endpoints/ # HTTP endpoints (file-based routing)
|
|
27
|
+
│ └── hello/get.ts # → GET /hello
|
|
28
|
+
├── db/schema.ts # table definitions (optional, enables typed DB)
|
|
29
|
+
├── workers/ # background job handlers (optional)
|
|
30
|
+
├── jobs/ # cron-scheduled jobs (optional)
|
|
31
|
+
├── hooks/ # auth/storage/document event hooks (optional)
|
|
32
|
+
├── webhooks/ # inbound provider webhooks (optional)
|
|
33
|
+
└── middleware/ # cross-cutting request middleware (optional)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Documentation
|
|
37
|
+
|
|
38
|
+
| Topic | File |
|
|
39
|
+
|-------|------|
|
|
40
|
+
| Getting started | [getting-started.md](./getting-started.md) |
|
|
41
|
+
| File-based routing | [routing.md](./routing.md) |
|
|
42
|
+
| Endpoints & `req` | [endpoints.md](./endpoints.md) |
|
|
43
|
+
| Database & transactions | [database.md](./database.md) |
|
|
44
|
+
| Schema & typed DB | [schema.md](./schema.md) |
|
|
45
|
+
| Services (Cache, Queue, Storage, …) | [services.md](./services.md) |
|
|
46
|
+
| Errors | [errors.md](./errors.md) |
|
|
47
|
+
| Workers & Jobs | [background.md](./background.md) |
|
|
48
|
+
| Hooks & Webhooks | [events.md](./events.md) |
|
|
49
|
+
|
|
50
|
+
For AI coding tools: a single concatenated corpus is generated at
|
|
51
|
+
[`llms-full.txt`](./llms-full.txt) (and an index at [`llms.txt`](./llms.txt)).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Workers & Jobs
|
|
2
|
+
|
|
3
|
+
Workers and jobs use the **`ctx` model**: their handler receives a context
|
|
4
|
+
object with `ctx.db`, `ctx.log`, `ctx.cache`, `ctx.queue`, `ctx.env`. They do
|
|
5
|
+
**not** receive a `req`, and you do **not** import the `Database` singleton
|
|
6
|
+
inside them — use `ctx.db`.
|
|
7
|
+
|
|
8
|
+
## Workers (queue consumers)
|
|
9
|
+
|
|
10
|
+
A worker processes jobs pushed via `Queue.push(name, payload)`. File lives under
|
|
11
|
+
`workers/`.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// workers/process-order.ts
|
|
15
|
+
import { defineWorker } from "@palbase/backend";
|
|
16
|
+
|
|
17
|
+
interface OrderPayload { orderId: string; amount: number; }
|
|
18
|
+
|
|
19
|
+
export default defineWorker<OrderPayload>({
|
|
20
|
+
name: "process-order", // must match the Queue.push() name
|
|
21
|
+
retry: 5, // optional, default 3
|
|
22
|
+
timeout: 60, // optional, seconds
|
|
23
|
+
backoff: "exponential", // "exponential" | "linear" | "fixed", default exponential
|
|
24
|
+
handler: async (ctx, payload) => {
|
|
25
|
+
ctx.log.info(`processing ${payload.orderId} for $${payload.amount}`);
|
|
26
|
+
await ctx.db.update("orders", payload.orderId, { status: "processed" });
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Enqueue from an endpoint:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { Queue } from "@palbase/backend";
|
|
35
|
+
await Queue.push("process-order", { orderId: "ord_1", amount: 1000 });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Jobs (cron-scheduled)
|
|
39
|
+
|
|
40
|
+
A job runs on a cron schedule. File lives under `jobs/`.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// jobs/cleanup.ts
|
|
44
|
+
import { defineJob } from "@palbase/backend";
|
|
45
|
+
|
|
46
|
+
export default defineJob({
|
|
47
|
+
name: "cleanup-expired",
|
|
48
|
+
schedule: "0 3 * * *", // standard cron
|
|
49
|
+
timeout: 120, // optional, seconds
|
|
50
|
+
handler: async (ctx) => {
|
|
51
|
+
const expired = await ctx.db.findMany("sessions", { expired: true });
|
|
52
|
+
for (const s of expired) await ctx.db.delete("sessions", s.id as string);
|
|
53
|
+
ctx.log.info(`cleaned ${expired.length} sessions`);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
package/docs/database.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Database
|
|
2
|
+
|
|
3
|
+
In **endpoints**, import the `Database` singleton:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Database } from "@palbase/backend";
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
In **workers, jobs, hooks, and webhooks**, use `ctx.db` — the same surface,
|
|
10
|
+
reached via the context object (see [background.md](./background.md) and
|
|
11
|
+
[events.md](./events.md)).
|
|
12
|
+
|
|
13
|
+
## Operations
|
|
14
|
+
|
|
15
|
+
| Method | Returns |
|
|
16
|
+
|--------|---------|
|
|
17
|
+
| `Database.insert(table, data)` | the inserted row (`Record<string, unknown>`) |
|
|
18
|
+
| `Database.update(table, id, data)` | the updated row |
|
|
19
|
+
| `Database.delete(table, id)` | `void` |
|
|
20
|
+
| `Database.findById(table, id)` | the row or `null` |
|
|
21
|
+
| `Database.findMany(table, query?)` | matching rows (array) |
|
|
22
|
+
| `Database.query(sql, params?)` | rows from a read-only SQL query (runs in a READ ONLY transaction) |
|
|
23
|
+
| `Database.transaction(fn)` | runs `fn(tx)` in a transaction |
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
const row = await Database.insert("todos", { title: "buy milk", done: false });
|
|
27
|
+
const one = await Database.findById("todos", row.id as string);
|
|
28
|
+
const open = await Database.findMany("todos", { done: false });
|
|
29
|
+
await Database.update("todos", row.id as string, { done: true });
|
|
30
|
+
await Database.delete("todos", row.id as string);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`findMany`'s `query` is an equality filter: keys are ANDed together. For
|
|
34
|
+
anything richer (ranges, ordering, joins) use `Database.query`.
|
|
35
|
+
|
|
36
|
+
`Database.query` is **read-only** — use it for selects/joins the helpers don't
|
|
37
|
+
cover. Writes must go through `insert`/`update`/`delete` or a transaction.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const rows = await Database.query(
|
|
41
|
+
"SELECT id, title FROM todos WHERE done = $1 ORDER BY created_at DESC LIMIT $2",
|
|
42
|
+
[false, 20],
|
|
43
|
+
);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Transactions
|
|
47
|
+
|
|
48
|
+
`transaction(fn)` gives you a `tx` with the same DB ops (no nested
|
|
49
|
+
transaction). Returning commits; throwing rolls back.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
await Database.transaction(async (tx) => {
|
|
53
|
+
const order = await tx.insert("orders", { amount: 1000, status: "pending" });
|
|
54
|
+
await tx.insert("order_items", { order_id: order.id, sku: "ABC" });
|
|
55
|
+
// throw here → both inserts roll back
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For a typed `.tables.*` API instead of string table names, see
|
|
60
|
+
[schema.md](./schema.md).
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Endpoints
|
|
2
|
+
|
|
3
|
+
An endpoint is `export default defineEndpoint({...})` in a method file. The
|
|
4
|
+
handler receives **one argument**, `req` (a `PBRequest`). Services are NOT on
|
|
5
|
+
`req` — import them as singletons (see [services.md](./services.md)).
|
|
6
|
+
|
|
7
|
+
## `defineEndpoint` config
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
defineEndpoint({
|
|
11
|
+
method: "POST", // required: GET | POST | PUT | PATCH | DELETE
|
|
12
|
+
auth: { required: true }, // optional; see Auth below. Omitted → public.
|
|
13
|
+
rateLimit: { max: 100, window: 60 }, // optional: max requests per window seconds
|
|
14
|
+
input: z.object({ ... }), // optional Zod schema → validates & types req.input
|
|
15
|
+
output: z.object({ ... }), // optional Zod schema → validates the return value
|
|
16
|
+
errors: { ... }, // optional declared errors (see errors.md)
|
|
17
|
+
middleware: [ ... ], // optional Middleware[] (see below)
|
|
18
|
+
handler: async (req) => { ... }, // required
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## `req` (PBRequest)
|
|
23
|
+
|
|
24
|
+
| Field | Type | Notes |
|
|
25
|
+
|-------|------|-------|
|
|
26
|
+
| `req.input` | inferred from `input` schema | request body for POST/PUT/PATCH; `{}` otherwise |
|
|
27
|
+
| `req.params` | `Record<string,string>` | route params, e.g. `req.params.id` |
|
|
28
|
+
| `req.query` | `Record<string,string>` | parsed query string |
|
|
29
|
+
| `req.headers` | `Record<string,string>` | lowercase keys |
|
|
30
|
+
| `req.user` | `User` when authenticated; `User \| null` when `auth` is omitted or `required: false` | see Auth below |
|
|
31
|
+
| `req.client` | `ClientInfo` | calling SDK/app/platform/os version (all nullable) |
|
|
32
|
+
| `req.file` | `FileContext \| null` | uploaded file, if any |
|
|
33
|
+
| `req.method` | `string` | the HTTP method |
|
|
34
|
+
| `req.requestId` / `req.traceId` / `req.spanId` | `string` | correlation ids |
|
|
35
|
+
| `req.errors` | typed throwers | present when `errors` is declared (see errors.md) |
|
|
36
|
+
|
|
37
|
+
`User` is `{ id: string; email: string; role: string; metadata: Record<string, unknown> }`.
|
|
38
|
+
|
|
39
|
+
## Auth
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
auth: { required: true } // require any authenticated user → req.user is non-null User
|
|
43
|
+
auth: { required: true, role: "admin" } // require a specific role
|
|
44
|
+
auth: { required: false } // public → req.user may be null
|
|
45
|
+
// auth omitted entirely // also public → req.user is User | null
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Whether `req.user` is non-null is computed from the `auth` config at the type
|
|
49
|
+
level:
|
|
50
|
+
|
|
51
|
+
| `auth` value | `req.user` type |
|
|
52
|
+
|--------------|-----------------|
|
|
53
|
+
| omitted | `User \| null` |
|
|
54
|
+
| `true` | `User` |
|
|
55
|
+
| `false` | `User \| null` |
|
|
56
|
+
| `{ required: true }` | `User` |
|
|
57
|
+
| `{ required: false }` | `User \| null` |
|
|
58
|
+
| `{ role: "admin" }` (object, no `required`) | `User` |
|
|
59
|
+
|
|
60
|
+
To enforce authentication, set `auth: { required: true }` (or `auth: true`). An
|
|
61
|
+
object with a `role` but no `required` key is treated as authenticated. When
|
|
62
|
+
`auth` is omitted, the endpoint is public and `req.user` may be null.
|
|
63
|
+
|
|
64
|
+
## Typed input/output
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { defineEndpoint, z, Database } from "@palbase/backend";
|
|
68
|
+
|
|
69
|
+
export default defineEndpoint({
|
|
70
|
+
method: "POST",
|
|
71
|
+
auth: { required: true },
|
|
72
|
+
input: z.object({ name: z.string().min(1).max(100), capacity: z.number().int().positive().optional() }),
|
|
73
|
+
output: z.object({ id: z.string(), name: z.string(), capacity: z.number().nullable() }),
|
|
74
|
+
handler: async (req) => {
|
|
75
|
+
const room = await Database.insert("rooms", {
|
|
76
|
+
name: req.input.name,
|
|
77
|
+
capacity: req.input.capacity ?? null,
|
|
78
|
+
});
|
|
79
|
+
return { id: room.id as string, name: room.name as string, capacity: (room.capacity as number) ?? null };
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Middleware
|
|
85
|
+
|
|
86
|
+
A middleware wraps a request. Define one in `middleware/<name>.ts`:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// middleware/logger.ts
|
|
90
|
+
import { defineMiddleware } from "@palbase/backend";
|
|
91
|
+
|
|
92
|
+
export default defineMiddleware(async (ctx, next) => {
|
|
93
|
+
ctx.log.info(`start ${ctx.requestId}`);
|
|
94
|
+
await next();
|
|
95
|
+
ctx.log.info(`done ${ctx.requestId}`);
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The middleware handler receives `(ctx, next)` — call `await next()` to run the
|
|
100
|
+
rest of the chain (other middleware, then the endpoint handler). Note this uses
|
|
101
|
+
the `ctx` model, not `req`.
|
|
102
|
+
|
|
103
|
+
To attach middleware to a specific endpoint, import it and list it in the
|
|
104
|
+
endpoint's `middleware` array:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { defineEndpoint, z } from "@palbase/backend";
|
|
108
|
+
import logger from "../../middleware/logger.js";
|
|
109
|
+
|
|
110
|
+
export default defineEndpoint({
|
|
111
|
+
method: "GET",
|
|
112
|
+
middleware: [logger],
|
|
113
|
+
output: z.object({ ok: z.boolean() }),
|
|
114
|
+
handler: async (req) => ({ ok: true }),
|
|
115
|
+
});
|
|
116
|
+
```
|
package/docs/errors.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Errors
|
|
2
|
+
|
|
3
|
+
Two ways to fail a request. Both serialize to the standard Palbase error
|
|
4
|
+
envelope:
|
|
5
|
+
|
|
6
|
+
```json
|
|
7
|
+
{ "error": "todo_not_found", "error_description": "No such todo", "status": 404, "request_id": "req_…" }
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## 1. `HttpError` (ad-hoc)
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { HttpError } from "@palbase/backend";
|
|
14
|
+
throw new HttpError(404, "todo_not_found", "No such todo");
|
|
15
|
+
// optional structured payload (4th arg) rides along under `data`:
|
|
16
|
+
throw new HttpError(423, "todo_locked", "Locked", { retryAfter: 30 });
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`new HttpError(status, code, description, data?)`.
|
|
20
|
+
|
|
21
|
+
## 2. Declared errors (typed)
|
|
22
|
+
|
|
23
|
+
Declare them on the endpoint; throw via `req.errors.<name>(...)`. Declared
|
|
24
|
+
errors are described in the endpoint's OpenAPI and codegen'd into a typed enum
|
|
25
|
+
for iOS callers.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { defineEndpoint, z, Database } from "@palbase/backend";
|
|
29
|
+
|
|
30
|
+
export default defineEndpoint({
|
|
31
|
+
method: "POST",
|
|
32
|
+
input: z.object({ id: z.string() }),
|
|
33
|
+
output: z.object({ ok: z.boolean() }),
|
|
34
|
+
errors: {
|
|
35
|
+
notFound: { status: 404, code: "todo_not_found", description: "No such todo" },
|
|
36
|
+
locked: { status: 423, code: "todo_locked", data: z.object({ retryAfter: z.number() }) },
|
|
37
|
+
},
|
|
38
|
+
handler: async (req) => {
|
|
39
|
+
const todo = await Database.findById("todos", req.input.id);
|
|
40
|
+
if (!todo) throw req.errors.notFound(); // no data → no args
|
|
41
|
+
if (todo.locked) throw req.errors.locked({ retryAfter: 30 }); // data schema → required arg
|
|
42
|
+
return { ok: true };
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A declared error with a `data` Zod schema requires that payload as an argument;
|
|
48
|
+
one without `data` takes no arguments. This is enforced by the types.
|
package/docs/events.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Hooks & Webhooks
|
|
2
|
+
|
|
3
|
+
Like workers/jobs, hooks and webhooks use the **`ctx` model** (`ctx.db`,
|
|
4
|
+
`ctx.log`, `ctx.cache`, `ctx.queue`, `ctx.env`) — not `req`, and not imported
|
|
5
|
+
singletons.
|
|
6
|
+
|
|
7
|
+
## Hooks (platform events)
|
|
8
|
+
|
|
9
|
+
React to auth, storage, and document events. Files live under `hooks/`. Builders
|
|
10
|
+
are imported from `@palbase/backend`: `auth`, `storage`, `documents`.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// hooks/auth.ts
|
|
14
|
+
import { auth } from "@palbase/backend";
|
|
15
|
+
|
|
16
|
+
export const onUserCreated = auth.onUserCreated(async (ctx, event) => {
|
|
17
|
+
ctx.log.info(`new user: ${event.user.email}`);
|
|
18
|
+
await ctx.db.insert("profiles", {
|
|
19
|
+
user_id: event.user.id,
|
|
20
|
+
email: event.user.email,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const onSignIn = auth.onSignIn(async (ctx, event) => {
|
|
25
|
+
ctx.log.info(`sign in: ${event.user.email} via ${event.provider}`);
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Available hook builders: `auth.onUserCreated`, `auth.onSignIn`, `auth.onSignOut`,
|
|
30
|
+
`auth.onPasswordReset`, `storage.onFileUploaded`, `storage.onFileDeleted`,
|
|
31
|
+
`documents.onDocumentCreated`, `documents.onDocumentUpdated`,
|
|
32
|
+
`documents.onDocumentDeleted`.
|
|
33
|
+
|
|
34
|
+
## Webhooks (inbound provider events)
|
|
35
|
+
|
|
36
|
+
Receive and verify webhooks from third-party providers. Files live under
|
|
37
|
+
`webhooks/`.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// webhooks/stripe.ts
|
|
41
|
+
import { defineWebhook } from "@palbase/backend";
|
|
42
|
+
|
|
43
|
+
export default defineWebhook({
|
|
44
|
+
provider: "stripe",
|
|
45
|
+
secret: { env: "STRIPE_WEBHOOK_SECRET" }, // signing secret resolved from env
|
|
46
|
+
events: {
|
|
47
|
+
"checkout.session.completed": async (ctx, event) => {
|
|
48
|
+
await ctx.db.insert("orders", { status: "paid", data: event });
|
|
49
|
+
},
|
|
50
|
+
"payment_intent.payment_failed": async (ctx, event) => {
|
|
51
|
+
ctx.log.error("payment failed");
|
|
52
|
+
await ctx.db.insert("payment_failures", { data: event });
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The signing secret is read from the project's env/secrets (`{ env: "NAME" }`);
|
|
59
|
+
the runtime verifies the signature before dispatching to your event handlers.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
There is no CLI init command. A starter project is created for you when your
|
|
4
|
+
Palbase project is provisioned. You then edit the files locally and deploy them.
|
|
5
|
+
|
|
6
|
+
## package.json
|
|
7
|
+
|
|
8
|
+
Your project depends on the SDK and uses the Palbase CLI for the dev loop:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"name": "my-backend",
|
|
13
|
+
"private": true,
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "palbase serve",
|
|
16
|
+
"deploy": "palbase push",
|
|
17
|
+
"typecheck": "tsc --noEmit"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": { "@palbase/backend": "latest" },
|
|
20
|
+
"devDependencies": { "@types/node": "^22", "typescript": "^5" }
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Local dev loop
|
|
25
|
+
|
|
26
|
+
- `palbase serve` — run your backend locally with hot reload.
|
|
27
|
+
- `palbase push` — deploy the current directory to your project's backend runtime.
|
|
28
|
+
- `palbase push --branch <name>` — deploy to a branch instead of `main`.
|
|
29
|
+
|
|
30
|
+
## Your first endpoint
|
|
31
|
+
|
|
32
|
+
Create `endpoints/hello/get.ts`:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { defineEndpoint, z } from "@palbase/backend";
|
|
36
|
+
|
|
37
|
+
export default defineEndpoint({
|
|
38
|
+
method: "GET",
|
|
39
|
+
input: z.object({ name: z.string().optional() }),
|
|
40
|
+
output: z.object({ message: z.string(), user: z.string().nullable() }),
|
|
41
|
+
handler: async (req) => ({
|
|
42
|
+
message: `hello, ${req.input.name ?? "world"}!`,
|
|
43
|
+
user: req.user?.id ?? null,
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This is served at `GET /hello`. The Zod `input` schema validates the request and
|
|
49
|
+
flows into `req.input`; the `output` schema validates your return value.
|