@softr/sdk 0.0.26-alpha-0 → 0.0.26

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/package.json CHANGED
@@ -1,36 +1,16 @@
1
1
  {
2
2
  "name": "@softr/sdk",
3
- "version": "0.0.26-alpha-0",
3
+ "version": "0.0.26",
4
4
  "type": "module",
5
5
  "exports": {
6
- ".": {
7
- "types": "./dist/index.d.ts",
8
- "import": "./dist/index.js"
9
- },
10
- "./client": {
11
- "types": "./dist/client.d.ts",
12
- "import": "./dist/client.js"
13
- },
14
- "./server": {
15
- "types": "./dist/server.d.ts",
16
- "import": "./dist/server.js"
17
- },
18
- "./vite": {
19
- "types": "./dist/vite.d.ts",
20
- "import": "./dist/vite.js"
21
- }
22
- },
23
- "bin": {
24
- "softr-sdk": "./dist/cli.js"
25
- },
26
- "files": [
27
- "dist",
28
- "skills",
29
- "!skills/_artifacts"
30
- ],
31
- "publishConfig": {
32
- "access": "public"
33
- },
6
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
7
+ "./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" },
8
+ "./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" },
9
+ "./vite": { "types": "./dist/vite.d.ts", "import": "./dist/vite.js" }
10
+ },
11
+ "bin": { "softr-sdk": "./dist/cli.js" },
12
+ "files": ["dist", "skills", "!skills/_artifacts"],
13
+ "publishConfig": { "access": "public" },
34
14
  "scripts": {
35
15
  "build": "tsdown",
36
16
  "test": "node --import ./src/test-utils/setup.ts --test",
@@ -62,9 +42,7 @@
62
42
  "optional": true
63
43
  }
64
44
  },
65
- "keywords": [
66
- "tanstack-intent"
67
- ],
45
+ "keywords": ["tanstack-intent"],
68
46
  "intent": {
69
47
  "version": 1,
70
48
  "repo": "@softr/sdk",
@@ -5,13 +5,13 @@ description: >-
5
5
  four entrypoints (@softr/sdk, /server, /client, /vite) and the client-server
6
6
  boundary between them, softrAuthMiddleware, getSoftrContext,
7
7
  initialSoftrContext, SoftrContext, and which sub-skill to load for route
8
- visibility, the current user, reading records, charts and metrics, writing
9
- records, or auth. Load this when unsure which sub-skill applies, or when
10
- touching the wiring; each sub-skill stands alone for its own task.
8
+ visibility, the current user, reading records, writing records, or auth. Load
9
+ this when unsure which sub-skill applies, or when touching the wiring; each
10
+ sub-skill stands alone for its own task.
11
11
  metadata:
12
12
  type: core
13
13
  library: softr-sdk
14
- library_version: 0.0.25
14
+ library_version: 0.0.26
15
15
  ---
16
16
 
17
17
  # Softr SDK core
@@ -21,7 +21,7 @@ metadata:
21
21
  > **CRITICAL**: The SDK is split across four entrypoints and the split is enforced. `ds` and `auth` exist only on `@softr/sdk/server` and only run inside a `createServerFn` handler. Importing them anywhere else fails the build.
22
22
  > **CRITICAL**: Every route file must declare its audience with `defineVisibility`. There is no default and nothing is inherited from a sibling. A route file without a declaration is a lint error.
23
23
  > **CRITICAL**: The app is already wired. `src/start.ts`, `src/router.tsx` and `src/routes/__root.tsx` register the middleware and resolve the session. Do not add a second registration.
24
- > **CRITICAL**: `workflow.trigger` and `integration.fetch` are exploratory. They exist on the entrypoints below and they typecheck, but their endpoints are not final. Do not build a feature on either of them. Tell the user the capability is not ready and ask how they want to proceed.
24
+ > **CRITICAL**: `workflow.trigger` is exploratory. It exists on the entrypoints below and typechecks, but its endpoint is not final. Do not build a feature on it. Tell the user the capability is not ready and ask how they want to proceed.
25
25
 
26
26
  Each sub-skill stands alone for its task and repeats the boundary above, so most work needs **one** sub-skill and not this file. Load this one when you are unsure which sub-skill applies, or when the wiring below is what you are touching. Add a second sub-skill only when the work genuinely crosses that boundary: a page that lists records behind a group restriction needs `reading-records` plus `routes-and-visibility`.
27
27
 
@@ -29,7 +29,7 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
29
29
 
30
30
  | Import | Runs | Holds |
31
31
  | --- | --- | --- |
32
- | `@softr/sdk` | Both | `defineAction`, `defineVisibility`, `useCurrentUser`, `usePermissions`, `useUserTimezone`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
32
+ | `@softr/sdk` | Both | `defineVisibility`, `q`, `useCurrentUser`, `usePermissions`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
33
33
  | `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration` |
34
34
  | `@softr/sdk/client` | Browser only | Nothing yet, reserved for browser-only utilities |
35
35
  | `@softr/sdk/vite` | Build | `softr()`, already in `vite.config.ts` |
@@ -41,9 +41,9 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
41
41
  | Add a route, or change who can see one | [routes-and-visibility/SKILL.md](./routes-and-visibility/SKILL.md) |
42
42
  | Read the signed-in visitor or what they may do | [current-user/SKILL.md](./current-user/SKILL.md) |
43
43
  | Fetch records from a datasource | [reading-records/SKILL.md](./reading-records/SKILL.md) |
44
- | Chart, aggregate or count records | [charts-and-metrics/SKILL.md](./charts-and-metrics/SKILL.md) |
45
44
  | Create, update or delete a record | [data-mutations/SKILL.md](./data-mutations/SKILL.md) |
46
45
  | Sign a visitor in, out, or up | [auth/SKILL.md](./auth/SKILL.md) |
46
+ | Call a connected third-party API (Slack, Stripe, Gmail, …) | [integrations/SKILL.md](./integrations/SKILL.md) |
47
47
 
48
48
  ## Quick Decision Tree
49
49
 
@@ -57,15 +57,15 @@ Showing the visitor's name, or hiding UI they cannot use?
57
57
  Putting records on a page, in a table, a list or a detail view?
58
58
  load softr-sdk/reading-records
59
59
 
60
- Drawing a chart, a stat tile, or any aggregated number?
61
- load softr-sdk/charts-and-metrics
62
-
63
60
  Submitting a form, editing a record, or deleting one?
64
61
  load softr-sdk/data-mutations
65
62
 
66
63
  Wiring a login, signup, or sign-out?
67
64
  load softr-sdk/auth
68
65
 
66
+ Calling a connected third-party API (Slack, Stripe, Gmail, Google Calendar, …)?
67
+ load softr-sdk/integrations
68
+
69
69
  Writing a server function, loader, or middleware in general?
70
70
  not a Softr concern. Use the TanStack Start skills.
71
71
  ```
@@ -128,4 +128,4 @@ type User = {
128
128
 
129
129
  ## Version
130
130
 
131
- Targets `@softr/sdk` v0.0.25.
131
+ Targets `@softr/sdk` v0.0.26.
@@ -16,7 +16,7 @@ description: >-
16
16
  metadata:
17
17
  type: sub-skill
18
18
  library: softr-sdk
19
- library_version: 0.0.25
19
+ library_version: 0.0.26
20
20
  ---
21
21
 
22
22
  **Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineVisibility`, the hooks and every type come from `@softr/sdk`. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
@@ -10,7 +10,7 @@ description: >-
10
10
  metadata:
11
11
  type: sub-skill
12
12
  library: softr-sdk
13
- library_version: 0.0.25
13
+ library_version: 0.0.26
14
14
  ---
15
15
 
16
16
  **Boundary.** `useCurrentUser`, `usePermissions` and every type come from `@softr/sdk` and work anywhere; `ds` and `auth` live on `@softr/sdk/server` and run only inside a `createServerFn` handler. The app is already wired — `src/start.ts` registers `softrAuthMiddleware`, `src/routes/__root.tsx` publishes the visitor as route context — never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
@@ -2,14 +2,13 @@
2
2
  name: data-mutations
3
3
  description: >-
4
4
  Create, update and delete records in a Softr datasource from inside a server
5
- function: declaring a write with defineAction, ds.create, ds.update and
6
- ds.delete from @softr/sdk/server, the data payload keyed by the action's
7
- aliases, what each field type accepts on a write, and the record each call
8
- returns. Writes only. Reads, charts and metrics are not covered.
5
+ function: ds.create, ds.update and ds.delete from @softr/sdk/server, the data
6
+ payload key, select field aliasing on the way in and out, and what each call
7
+ returns or throws. Writes only. Reads, metrics and chart data are not covered.
9
8
  metadata:
10
9
  type: sub-skill
11
10
  library: softr-sdk
12
- library_version: 0.0.25
11
+ library_version: 0.0.26
13
12
  ---
14
13
 
15
14
  **Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `q` and every type come from `@softr/sdk` and can be imported anywhere. The app is already wired (`src/start.ts`, `src/router.tsx`, `src/routes/__root.tsx`); never add a second registration. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
@@ -23,101 +22,93 @@ metadata:
23
22
  ```ts
24
23
  import { createServerFn } from '@tanstack/react-start'
25
24
  import { z } from 'zod'
26
- import { defineAction } from '@softr/sdk'
27
25
  import { ds } from '@softr/sdk/server'
26
+ import { q } from '@softr/sdk'
28
27
  ```
29
28
 
30
- A write's input arrives from the client, so validate it. Pass a zod schema straight to `.validator`; Start accepts it and types `data` from it.
29
+ Pass a zod schema straight to `.validator`. Start accepts it and types `data` from it.
31
30
 
32
- ## Declare the write
31
+ Every `ds` call names its datasource by an opaque UUID from the table's registration on the application (Softr Studio's Data tab, or the `register_application_data_source` tool when an agent does it). It is never derived from the table's name. Field ids are opaque too (`fZk3Qm9pLw2Xa1`, not `fld_<name>`) and come from the table's schema.
33
32
 
34
- Every `ds` call takes an action, and an action declares one operation. A create action only works with `ds.create`; hand it to `ds.update` and the call throws `action "createProject" is declared as create, not update`.
35
-
36
- ```ts
37
- const createProject = defineAction({
38
- id: 'createProject',
39
- datasource: 'c9a51f22-3b8e-4d17-a6c0-5e92d84b7f31',
40
- operation: 'create',
41
- fields: {
42
- name: 'fZk3Qm9pLw2Xa1', // SINGLE_LINE_TEXT
43
- status: 'Ru7bVx2cNq0Ed9', // SELECT, choice ids 'draft' | 'active' | 'archived'
44
- dueDate: 'Xp2dLh6vKz9Fa3', // DATETIME
45
- },
46
- permission: { userGroup: 'LOGGED_IN_USERS' },
47
- })
48
- ```
49
-
50
- The declaration follows the same rules as a read action, spelled out in [reading-records](../reading-records/SKILL.md): `datasource` is the opaque UUID from the table's registration, never derived from its name; the keys of `fields` are names you choose and the values are opaque field ids; everything sits at module scope with every property a literal.
51
-
52
- The action's `fields` bound the write: `data` can only name them, and the returned record only carries them. A delete action declares no `fields`; giving it some is a type error.
33
+ **The datasource id must be a string literal at the call site.**
53
34
 
54
35
  ## Core Patterns
55
36
 
56
37
  ### Create a record
57
38
 
58
39
  ```ts
59
- const NewProject = z.object({ name: z.string(), dueDate: z.string() })
40
+ const NewProject = z.object({ name: z.string(), status: z.string() })
60
41
 
61
- const addProject = createServerFn({ method: 'POST' })
42
+ const createProject = createServerFn({ method: 'POST' })
62
43
  .validator(NewProject)
63
44
  .handler(async ({ data }) =>
64
- ds.create(createProject, {
65
- data: { name: data.name, dueDate: data.dueDate, status: { id: 'draft' } },
45
+ ds.create('8ffb4814-b028-4496-b679-7face2167ebc', {
46
+ data,
47
+ select: q.select({ name: 'fZk3Qm9pLw2Xa1', status: 'Ru7bVx2cNq0Ed9' }),
66
48
  }),
67
49
  )
68
50
  ```
69
51
 
70
- Returns the created record as `{ id, fields }`, with `fields` keyed by the action's names and holding the same value shapes a read returns. Every key in `data` is optional. A field left out, or written as `null`, gets the datasource's default.
52
+ Resolves to `{ id, fields }`, and on a Softr table `fields` holds **only the keys you wrote** — not the rest of the record. See "What a write answers with" below.
53
+
54
+ **The payload key is `data`, not `fields`.** A record answers with `fields`, which is where the confusion comes from, but the option you pass is `data`. `CreateOptions` requires it, so a call keyed by `fields` does not type-check.
55
+
56
+ **`data` speaks whatever `select` maps.** The keys above are the schema's own names because `select` maps them to field ids, which is why the validated input can go straight through. With no `select`, `data` has to be keyed by raw field ids instead.
71
57
 
72
58
  ### Update a record
73
59
 
74
60
  ```ts
75
- const updateProject = defineAction({
76
- id: 'updateProject',
77
- datasource: 'c9a51f22-3b8e-4d17-a6c0-5e92d84b7f31',
78
- operation: 'update',
79
- fields: { status: 'Ru7bVx2cNq0Ed9', dueDate: 'Xp2dLh6vKz9Fa3' },
80
- permission: { userGroup: 'LOGGED_IN_USERS' },
81
- })
82
-
83
- const archiveProject = createServerFn({ method: 'POST' })
84
- .validator(z.object({ id: z.string() }))
61
+ const Rename = z.object({ id: z.string(), name: z.string() })
62
+
63
+ const renameProject = createServerFn({ method: 'POST' })
64
+ .validator(Rename)
85
65
  .handler(async ({ data }) =>
86
- ds.update(updateProject, { id: data.id, data: { status: { id: 'archived' } } }),
66
+ ds.update('8ffb4814-b028-4496-b679-7face2167ebc', {
67
+ id: data.id,
68
+ data: { name: data.name },
69
+ select: q.select({ name: 'fZk3Qm9pLw2Xa1' }),
70
+ }),
87
71
  )
88
72
  ```
89
73
 
90
- The record id is its own option; keep it out of `data`. An empty `id` throws.
74
+ The record id is its own option, so keep it out of `data`.
75
+
76
+ `update` writes the keys in `data` and leaves every other field alone, so a field you leave out is a field you are not writing. An empty `id` throws.
77
+
78
+ ### What a write answers with
79
+
80
+ `create` and `update` resolve to `{ id, fields }`, but `fields` is **not the record**. On a Softr table it carries only the keys you sent, aliased through `select`; a status change answers with `{ id, fields: { status } }` and nothing else. (Other datasource types may answer with more. Rely on neither.)
91
81
 
92
- `update` writes the keys in `data` and leaves every other field alone, so a field you leave out is a field you are not writing. On an update, `null` clears the field, where on a create it reached the default. Returns the updated record.
82
+ So the answer is a receipt, not a replacement for what the page holds. Putting it where a loader-fetched record was blanks every column you did not write. Two honest ways to show the result:
83
+
84
+ - **A quick change to one field** — a status select, a card dragged to another column, an archive toggle — is optimistic: change that one field in your own state immediately, keep the rest of the record as the loader gave it, save in the background, and on a rejected write restore the old value with a clear message. Do not `router.invalidate()` or re-fetch after a successful write; the local change is already what the user sees.
85
+ - **A saved form, or anything that changes several fields**, refreshes the route's data — `router.invalidate()`, see `@tanstack/router-core#router-core/data-loading` — so the list or detail re-reads through its own `select`.
86
+
87
+ Do not re-read the one record with `ds.get` for either: it takes no `select`, so it answers with raw field ids.
88
+
89
+ ### Writing a choice field
90
+
91
+ A `SELECT` or `LINKED_RECORD` field reads back as `{ id, label }` (see [field-types](../reading-records/references/field-types.md)). Writing one accepts the option's `id`, its `label`, or `{ id, label }`, matched case-insensitively.
92
+
93
+ **A value that matches no option is not an error — it silently adds a new option to the field.** A typo'd label leaves a junk choice in the schema. So write `{ label }` (or the `{ id, label }` you read), copy labels from the table's schema verbatim, and never invent one.
93
94
 
94
95
  ### Delete a record
95
96
 
96
97
  ```ts
97
- const deleteProject = defineAction({
98
- id: 'deleteProject',
99
- datasource: 'c9a51f22-3b8e-4d17-a6c0-5e92d84b7f31',
100
- operation: 'delete',
101
- permission: { userGroup: 'LOGGED_IN_USERS' },
102
- })
103
-
104
- const removeProject = createServerFn({ method: 'POST' })
105
- .validator(z.object({ id: z.string() }))
106
- .handler(async ({ data }) => {
107
- await ds.delete(deleteProject, { id: data.id })
108
- })
98
+ const RecordId = z.object({ id: z.string() })
99
+
100
+ const deleteProject = createServerFn({ method: 'POST' })
101
+ .validator(RecordId)
102
+ .handler(async ({ data }) => ds.delete('8ffb4814-b028-4496-b679-7face2167ebc', data.id))
109
103
  ```
110
104
 
111
- Resolves to nothing. A record that cannot be deleted throws.
105
+ **`delete` takes the record id as a positional argument**, unlike `create` and `update`, which take an options object. It resolves to nothing.
112
106
 
113
- ### What a write accepts
107
+ ### Naming fields with `select`
114
108
 
115
- With `softr-env.d.ts` in place (see [reading-records](../reading-records/SKILL.md#typing)), each key of `data` accepts exactly what its field takes:
109
+ `select` works in both directions on a write. Going in, it turns your names into Softr field ids. Coming back, it turns the returned field ids into your names. Without it, `data` has to be keyed by raw field ids and the returned record answers with them too.
116
110
 
117
- - SELECT and LINKED_RECORD take `{ id }`. Not a bare string, and not the `{ id, label }` a read returns; the label is display data and a write does not accept it. A generated SELECT narrows the id to the field's own choices.
118
- - CHECKBOX takes a boolean. Dates take ISO strings, never `Date` instances.
119
- - ATTACHMENT takes `{ url }`, optionally with `filename`, `id` and `type`.
120
- - A multi-entry field takes an array of the value.
111
+ The map has to cover every key in `data`. A key it does not map is not translated, so the write does not land on the field you meant.
121
112
 
122
113
  ### Writing on behalf of the visitor
123
114
 
@@ -125,16 +116,6 @@ Stamping a record with who created it, or scoping an update to the person making
125
116
 
126
117
  ## Failure Modes
127
118
 
128
- **A write with the wrong action.** Each operation needs an action declared for it. `ds.update` with a read action throws `action "..." is declared as read, not update` at the first call, not at build time.
129
-
130
- **A payload keyed `fields`.** The option is `data`. A record answers with `fields`, which is where the confusion comes from, but the option you pass is `data`, and the options type requires it.
131
-
132
- **A SELECT or LINKED_RECORD written as a bare string.** The write shape is `{ id }`. Writing `status: 'archived'` where `status: { id: 'archived' }` belongs is a type error with codegen in place and a silent misfire without it.
133
-
134
- **Feeding a read value back into a write.** A read returns `{ id, label }`; a write takes `{ id }` only. Strip the label before writing the value back.
135
-
136
- **A field missing from the action.** `data` can only name the action's own `fields`. A field you need to write must be declared on the action, and a delete action declares none.
137
-
138
119
  **A datasource id built from a name.** `'ds_projects'`, the table id, or the table's name glued to a uuid are not datasource ids. The id is the opaque UUID assigned when the table was registered on the application; copy it from there. Anything else fails at request time as not found or forbidden.
139
120
 
140
121
  **A datasource id stored in a variable.** Pass the UUID as a string literal directly at the call site. The lint rule `@softr/ds-no-computed-id` catches this at development time.
@@ -153,6 +134,5 @@ Stamping a record with who created it, or scoping an update to the person making
153
134
 
154
135
  ## References
155
136
 
156
- - [reading-records/SKILL.md](../reading-records/SKILL.md): declaring actions, fetching the record a write is about to change, and the codegen the types come from
157
- - [reading-records/references/field-types.md](../reading-records/references/field-types.md): the value shape every field reads back as
137
+ - [reading-records/SKILL.md](../reading-records/SKILL.md): fetching the record a write is about to change
158
138
  - [current-user/SKILL.md](../current-user/SKILL.md): reading the visitor off `context` to stamp or scope a write
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: integrations
3
+ description: >-
4
+ Call a third-party service connected to the app as an integration: integration.fetch
5
+ from @softr/sdk/server, reading the datasource id and integration type from the
6
+ "Attached data sources" prompt section, looking up the base URL from the reference
7
+ table by integrationType, and constructing the full API URL from base URL plus the
8
+ path and query params the model knows from training. Check res.ok before res.json().
9
+ metadata:
10
+ type: sub-skill
11
+ library: softr-sdk
12
+ library_version: 0.0.26
13
+ ---
14
+
15
+ **Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. Load the core `softr-sdk` skill only if you are unsure this is the right sub-skill.
16
+
17
+ # Integrations
18
+
19
+ `integration.fetch` calls a third-party API through Softr's authenticated proxy. It handles authorization automatically — you never set API keys or auth headers yourself.
20
+
21
+ ## Setup
22
+
23
+ ```ts
24
+ import { createServerFn } from '@tanstack/react-start'
25
+ import { integration } from '@softr/sdk/server'
26
+ ```
27
+
28
+ ## Reading connected integrations from the prompt
29
+
30
+ When the user attaches integrations to the chat, they appear in the `## Attached data sources` section of the prompt in this form:
31
+
32
+ ```
33
+ - "Gmail" — database `<databaseId>`, integration `<integrationId>`, type `GMAIL`
34
+ ```
35
+
36
+ Use `integrationId` as the first argument to `integration.fetch`. Use `integrationType` (e.g. `GMAIL`) to look up the base URL from the reference table below — then construct the full URL by appending the path and query parameters your training knows for that API.
37
+
38
+ ## Core pattern
39
+
40
+ ```ts
41
+ const getCalendarEvents = createServerFn().handler(async () => {
42
+ const now = new Date().toISOString()
43
+ const res = await integration.fetch(
44
+ '25d60213-ef4f-4fa0-b094-a46931b2d120',
45
+ `https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=${encodeURIComponent(now)}&singleEvents=true&orderBy=startTime&maxResults=10`,
46
+ )
47
+ if (!res.ok) throw new Error(`Integration request failed: ${res.status} ${await res.text()}`)
48
+ return res.json()
49
+ })
50
+ ```
51
+
52
+ The integration id (`25d60213-…`) is the `integrationId` from the attached data sources section. The base URL (`https://www.googleapis.com`) is looked up from the reference table below by matching `integrationType` (`GOOGLE_CALENDAR`). The path and query params come from your knowledge of the Google Calendar REST API.
53
+
54
+ ## POST request
55
+
56
+ ```ts
57
+ const sendSlackMessage = createServerFn({ method: 'POST' })
58
+ .validator((data: { channel: string; text: string }) => data)
59
+ .handler(async ({ data }) => {
60
+ const res = await integration.fetch(
61
+ 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
62
+ 'https://slack.com/api/chat.postMessage',
63
+ {
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify({ channel: data.channel, text: data.text }),
67
+ },
68
+ )
69
+ if (!res.ok) throw new Error(`Slack error: ${res.status} ${await res.text()}`)
70
+ return res.json()
71
+ })
72
+ ```
73
+
74
+ `integration.fetch` accepts the same `RequestInit` options as the native `fetch`. Do not pass `Authorization` headers — Softr injects the integration's credentials automatically.
75
+
76
+ ## Base URLs by integration type
77
+
78
+ Match the `integrationType` from the attached data sources section to find the base URL:
79
+
80
+ | integrationType | Base URL |
81
+ |---|---|
82
+ | ANTHROPIC_AI | `https://api.anthropic.com` |
83
+ | APOLLO | `https://api.apollo.io` |
84
+ | APIFY | `https://api.apify.com` |
85
+ | ASANA | `https://app.asana.com` |
86
+ | ATTIO | `https://api.attio.com` |
87
+ | BASEROW | `https://api.baserow.io` |
88
+ | BITLY | `https://api-ssl.bitly.com` |
89
+ | BREVO | `https://api.brevo.com` |
90
+ | CALCOM | `https://api.cal.com` |
91
+ | CALENDLY | `https://api.calendly.com` |
92
+ | COHERE | `https://api.cohere.com` |
93
+ | DEEPL | `https://api.deepl.com` |
94
+ | DEEP_SEEK_AI | `https://api.deepseek.com` |
95
+ | DOCS_AUTOMATOR | `https://api.docsautomator.co` |
96
+ | DOCUMINT | `https://api.documint.me` |
97
+ | ELASTIC_EMAIL | `https://api.elasticemail.com` |
98
+ | ELEVEN_LABS | `https://api.elevenlabs.io` |
99
+ | FATHOM | `https://api.fathom.ai` |
100
+ | FIRECRAWL | `https://api.firecrawl.dev` |
101
+ | GEMINI_AI | `https://generativelanguage.googleapis.com` |
102
+ | GMAIL | `https://gmail.googleapis.com` |
103
+ | GOOGLE_CALENDAR | `https://www.googleapis.com` |
104
+ | GRANOLA | `https://public-api.granola.ai` |
105
+ | HUNTER | `https://api.hunter.io` |
106
+ | INSTANTLY | `https://api.instantly.ai` |
107
+ | LEMLIST | `https://api.lemlist.com` |
108
+ | LINEAR | `https://api.linear.app` |
109
+ | LINKUP | `https://api.linkup.so` |
110
+ | LOOPS | `https://app.loops.so` |
111
+ | MESSAGE_BIRD | `https://rest.messagebird.com` |
112
+ | MISTRAL_AI | `https://api.mistral.ai` |
113
+ | OPEN_AI | `https://api.openai.com` |
114
+ | OUTLOOK | `https://graph.microsoft.com` |
115
+ | PADDLE | `https://api.paddle.com` |
116
+ | PINECONE | `https://api.pinecone.io` |
117
+ | PIPELINE_CRM | `https://api.pipelinecrm.com` |
118
+ | POSTMARK | `https://api.postmarkapp.com` |
119
+ | REPLICATE | `https://api.replicate.com` |
120
+ | RESEND | `https://api.resend.com` |
121
+ | SLACK | `https://slack.com/api` |
122
+ | SMARTLEAD | `https://server.smartlead.ai` |
123
+ | STRIPE | `https://api.stripe.com` |
124
+ | TELEGRAM | `https://api.telegram.org` |
125
+ | TINYURL | `https://api.tinyurl.com` |
126
+ | TWILIO | `https://api.twilio.com` |
127
+ | XERO | `https://api.xero.com` |
128
+ | ZOOM | `https://api.zoom.us` |
129
+
130
+ ## When the service is not a Softr integration
131
+
132
+ If the API the user needs is not in the table above, call it directly from a server function using the native `fetch`. The user supplies their own credentials and the call lives entirely in the server function handler.
133
+
134
+ ## Failure modes
135
+
136
+ **Calling from client code.** `integration` is server-only. Importing it into a component or calling it outside a `createServerFn` handler fails the build or has no request context to act on.
137
+
138
+ **Setting Authorization headers.** Softr injects credentials automatically. A manually set `Authorization` header overrides the injected one and the request is rejected by the third-party API.
139
+
140
+ **Not checking `res.ok`.** `integration.fetch` returns the proxied response as-is. A 4xx or 5xx from the third-party API is not thrown — call `res.ok` or `res.status` before calling `res.json()`.