@getxflow/cli 0.34.2 → 1.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.
- package/README.md +1 -1
- package/dist/api.js +21 -6
- package/dist/args.js +1 -1
- package/dist/bin.js +15 -4
- package/dist/commands/connections.js +4 -4
- package/dist/commands/db.js +26 -63
- package/dist/commands/deploy.js +170 -216
- package/dist/commands/dev.js +15 -29
- package/dist/commands/env.js +6 -3
- package/dist/commands/functions.js +27 -119
- package/dist/commands/logs.js +19 -15
- package/dist/commands/pages.js +26 -0
- package/dist/commands/projects.js +134 -190
- package/dist/commands/sources.js +218 -120
- package/dist/commands/storage.js +2 -2
- package/dist/config.js +82 -26
- package/dist/flags.js +21 -12
- package/dist/help.js +220 -211
- package/dist/limits.js +6 -6
- package/dist/session.js +167 -52
- package/dist/template.js +17 -113
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/skills/xflow/SKILL.md +504 -546
package/skills/xflow/SKILL.md
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: xflow
|
|
3
|
-
description: Build, deploy and publish apps on the XFlow platform with the xflow CLI. Use whenever
|
|
3
|
+
description: Build, deploy and publish apps on the XFlow platform with the xflow CLI. Use whenever a folder has xflow.json (the project root or an app folder in apps/), and for any task that touches a project and its apps, deployment, publishing, rollback, source sync, who may open an app or call a function, sign-in inside a public app, the project database or SQL migrations, cloud functions, schedules, environment variables and secrets, file storage and uploads of photos, video or PDFs, connected accounts of external services, production errors and logs, UI built on the template components, or a public page on xflowpage.ru (a landing page, presentation or invoice for people outside the organization).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# XFlow
|
|
7
7
|
|
|
8
|
-
Hosting for web apps. The code lives in an ordinary repository on the developer machine,
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
Hosting for web apps. The code lives in an ordinary repository on the developer machine, the
|
|
9
|
+
CLI sends the sources, the platform builds them in a clean sandbox, serves each app at its own
|
|
10
|
+
address and runs its backend: one database and cloud functions per project.
|
|
11
11
|
|
|
12
|
-
No `xflow.json`
|
|
13
|
-
projects of the organization with their ids, `xflow link <id>`
|
|
14
|
-
folder
|
|
12
|
+
No `xflow.json` in the folder or above it means it is not bound to a project yet:
|
|
13
|
+
`xflow projects list` names the projects of the organization with their ids, `xflow link <id>`
|
|
14
|
+
binds the folder to one (an empty folder gets the whole project), and `xflow init` starts a new
|
|
15
|
+
project with its first app.
|
|
15
16
|
|
|
16
17
|
## First rule
|
|
17
18
|
|
|
@@ -22,7 +23,7 @@ contains the fix.
|
|
|
22
23
|
|
|
23
24
|
## Keeping these instructions current
|
|
24
25
|
|
|
25
|
-
These instructions ship with xflow CLI 0.
|
|
26
|
+
These instructions ship with xflow CLI 1.0.1, so the copy you are reading can be older than
|
|
26
27
|
the CLI answering your commands.
|
|
27
28
|
|
|
28
29
|
When the CLI says this file does not match it, run `xflow skills --refresh` and then **read
|
|
@@ -30,9 +31,8 @@ again the file it names**: refreshing only writes to disk, and until you re-read
|
|
|
30
31
|
still working from the old rules. Do it yourself, no need to ask.
|
|
31
32
|
|
|
32
33
|
Updating the CLI is not yours. `xflow update` replaces a program shared by every project on
|
|
33
|
-
the machine
|
|
34
|
-
|
|
35
|
-
is updated, so say what you are doing and run it.
|
|
34
|
+
the machine: offer the update in your summary. The exception is a platform refusing the CLI as too old (`outdated_cli`):
|
|
35
|
+
nothing runs until it is updated, so say what you are doing and run it.
|
|
36
36
|
|
|
37
37
|
## Hard rules
|
|
38
38
|
|
|
@@ -44,359 +44,424 @@ the error points away from the cause. The sections below carry the details.
|
|
|
44
44
|
spreading the environment all read as no mention of the variable at all. The
|
|
45
45
|
build rejects them, and `xflow env check` names them before that.
|
|
46
46
|
2. A value written with `xflow env set` reaches the functions on the next
|
|
47
|
-
`xflow deploy`, not at the moment it is written.
|
|
47
|
+
`xflow deploy functions`, not at the moment it is written.
|
|
48
48
|
3. Never delete a `functions/<name>/` directory unless the user asked for that
|
|
49
|
-
function to go. The next deploy removes it from the cloud together with its
|
|
50
|
-
schedules, and
|
|
51
|
-
4.
|
|
49
|
+
function to go. The next deploy removes it from the cloud together with its keys and
|
|
50
|
+
schedules, and that is final.
|
|
51
|
+
4. A function an app or a page calls needs that caller in its `callers` in the root
|
|
52
|
+
`xflow.json`. Without it the call answers 404 `not_bound`, and a new function is not
|
|
53
|
+
reachable from anywhere.
|
|
54
|
+
5. Every function with a public caller (a page, an app open to anyone) can be called by
|
|
55
|
+
anybody on the internet with curl. Write it as such, see **Your function is open to the
|
|
56
|
+
whole internet**.
|
|
57
|
+
6. An already applied migration is never re-run, and `xflow db migrate` refuses a
|
|
52
58
|
name it already holds with different content. A schema change is always a new file.
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
7. When `xflow db migrate` answers `db_write_locked`, or an INSERT or CREATE TABLE in a
|
|
60
|
+
function fails with `permission denied` (42501), the database is over its plan size and
|
|
61
|
+
Postgres itself refuses. Reads, updates and deletes keep working, so the fix is a migration
|
|
55
62
|
that deletes data, never a rewrite of the SQL. Write comes back within the hour.
|
|
56
|
-
|
|
57
|
-
the real size from storage, so an early call answers that the file is not there.
|
|
58
|
-
7. Heavy media does not travel with the sources. Photos, video and PDFs go up with
|
|
63
|
+
8. Heavy media does not travel with the sources. Photos, video and PDFs go up with
|
|
59
64
|
`xflow storage push`, not into the repository: the archive is capped at 10 MB.
|
|
60
65
|
|
|
61
|
-
##
|
|
66
|
+
## Projects and apps
|
|
67
|
+
|
|
68
|
+
- A project is a box: one database with its migrations, the cloud functions, secrets and
|
|
69
|
+
members. It has no code of its own.
|
|
70
|
+
- An app is code the platform builds and serves at `https://<id>--<org>.xflowpage.ru`
|
|
71
|
+
(`<id>` is six random characters the platform gives the app, `<org>` the name of the
|
|
72
|
+
organization): its own versions, its own access level. A project holds one or more apps,
|
|
73
|
+
and all of them use the same database and the same functions.
|
|
74
|
+
- A project without apps is fine (`xflow init --no-app`): its functions are workflows, woken
|
|
75
|
+
by a schedule, by an outside service holding a key, or by `xflow functions invoke`.
|
|
76
|
+
- `xflow projects get` shows the apps with their folders, addresses and access levels, and
|
|
77
|
+
who may call each function.
|
|
78
|
+
|
|
79
|
+
## Folder layout
|
|
80
|
+
|
|
81
|
+
One folder per project, apps in subfolders:
|
|
82
|
+
|
|
83
|
+
clinic/
|
|
84
|
+
xflow.json projectId, and who may call each function
|
|
85
|
+
package.json dependencies of the functions
|
|
86
|
+
functions/<name>/index.ts cloud functions, shared by every app
|
|
87
|
+
migrations/0001_init.sql the one database of the project
|
|
88
|
+
apps/
|
|
89
|
+
priem/ an app: a Vite project with its own xflow.json (appId, build)
|
|
90
|
+
zapis/
|
|
91
|
+
|
|
92
|
+
- Two kinds of history: the project root (functions, migrations, `xflow.json`) is one, each
|
|
93
|
+
app is another. `apps/**` never travels with the root, the root never travels with an app.
|
|
94
|
+
- An app sees only its own folder when it is built. What two apps share is data and logic,
|
|
95
|
+
and that lives in functions. Functions never go inside an app folder: the build refuses
|
|
96
|
+
`functions/` there (`functions-in-app`).
|
|
97
|
+
- `xflow apps add <folder>` creates an app, lays the template into `apps/<folder>` and prints
|
|
98
|
+
its address. Never create an app folder by hand: without its `appId` it is nobody's.
|
|
99
|
+
`--color <color>` sets the tile the user sees in the project; a wrong one is answered with
|
|
100
|
+
the list of colors. Its icon and a later color change belong to the person in the web
|
|
101
|
+
interface.
|
|
102
|
+
- Rename an app folder freely: the app is its `appId`.
|
|
103
|
+
- Commands follow the folder you stand in: inside `apps/zapis` they mean that app, anywhere
|
|
104
|
+
else in the project they mean the project. `--project <id>` and `--app <id>` name them
|
|
105
|
+
without a folder.
|
|
106
|
+
- Neither `xflow.json` is edited by hand except `functions.<name>.callers` in the root one.
|
|
62
107
|
|
|
63
|
-
|
|
64
|
-
connections, seats, database and file storage, function minutes, builds per month and at
|
|
65
|
-
once, schedules and how often they may run. Most are finite; a plan may leave one uncapped,
|
|
66
|
-
and then `whoami` prints `no limit` instead of a ceiling. `xflow whoami` prints every one of
|
|
67
|
-
them next to what is used, and the rights of your own key with them: destroying data in a migration,
|
|
68
|
-
deleting files from storage and linking a connected account each need a right that is off by
|
|
69
|
-
default. Reading it before a long task turns a refusal halfway through the work into a
|
|
70
|
-
sentence said before it starts.
|
|
108
|
+
## Plan limits
|
|
71
109
|
|
|
72
|
-
|
|
110
|
+
The organization runs on a plan with limits: apps, functions, pages, active connections,
|
|
111
|
+
seats, database and file storage, function minutes, builds per month and at once, schedules
|
|
112
|
+
and how often they may run, and which access levels an app may have. Most are finite; a plan
|
|
113
|
+
may leave one uncapped, and then `whoami` prints `no limit` instead of a ceiling.
|
|
114
|
+
`xflow whoami` prints every one of them next to what is used, and the rights of your own key
|
|
115
|
+
with them: destroying data in a migration, deleting files from storage, moving a database and
|
|
116
|
+
linking a connected account each need a right. Reading it before a long task turns a refusal
|
|
117
|
+
halfway through the work into a sentence said before it starts.
|
|
73
118
|
|
|
74
119
|
- Connections are counted while active: switching an unused one off frees the seat without
|
|
75
120
|
deleting it.
|
|
121
|
+
- Function minutes are shared by the whole organization: every app, page and schedule spends
|
|
122
|
+
them. When they run out, every function of the organization stops (`functions_stopped`).
|
|
76
123
|
- The cap on how long one function call may run is written into the function when it is
|
|
77
|
-
deployed, so a plan change reaches the cloud only with the next `xflow deploy` of
|
|
78
|
-
project, in both directions.
|
|
124
|
+
deployed, so a plan change reaches the cloud only with the next `xflow deploy functions` of
|
|
125
|
+
each project, in both directions.
|
|
79
126
|
|
|
80
127
|
A limit refusal is not a bad request. The CLI prints a line starting with `Plan limit:`, the
|
|
81
128
|
API answers `code: "forbidden"` with a `limit` object (`code`, `used`, `limit`), MCP tools
|
|
82
129
|
carry the same field. Retrying, renaming or rewriting the code changes nothing: tell the user
|
|
83
|
-
what ran out and stop, only the owner
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
2. `
|
|
91
|
-
|
|
92
|
-
3. `
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
5.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
function, which has no pages to return, and it needs no folder at all: `--project <id>` names the
|
|
127
|
-
project. Functions that exist now but not in that revision are removed, with the usual question.
|
|
128
|
-
|
|
129
|
-
Pages are kept for the **three latest successful builds** plus whatever dev and live show;
|
|
130
|
-
older versions stay in the history with `pages_kept` false, readable but not openable. The
|
|
131
|
-
history holds the **last 30 builds**, each keeping the revision it was built from; any other
|
|
132
|
-
revision is kept only while it is among the **10 latest**. Three builds is the whole rollback
|
|
133
|
-
depth, hours on an active project: keep anything you must not lose in your own git repository.
|
|
134
|
-
|
|
135
|
-
**The only link you give a person is the project page**, `https://app.getxflow.com/projects/<id>`,
|
|
136
|
-
which the CLI prints for you. Refer to builds by their number ("version 481203 is built, 092399
|
|
137
|
-
is what visitors see"), never by address: a build address has the version baked into it and
|
|
138
|
-
after the next publish keeps answering with the old copy, so whoever holds it stares at a
|
|
139
|
-
frozen app and concludes nothing shipped. The project page always shows the current state.
|
|
130
|
+
what ran out and stop, only the owner lifts it. A schedule refused for running too often is one
|
|
131
|
+
of these too, not a bad cron string: the minimum interval comes from the plan, never guess it.
|
|
132
|
+
|
|
133
|
+
## Deploying
|
|
134
|
+
|
|
135
|
+
1. Change the code. In an app folder `npm run typecheck` is a two-second check, `npm run build`
|
|
136
|
+
a fast way to see build errors early: the platform builds again in its own sandbox.
|
|
137
|
+
2. `xflow db migrate` when the schema changes. It stores the project root first (migrations
|
|
138
|
+
live in its history), then applies what is new.
|
|
139
|
+
3. `xflow deploy functions -m "what changed"` sends the project root, checks it, ships the
|
|
140
|
+
functions and their callers. Every app runs the new code the moment it finishes: there is
|
|
141
|
+
no staging and no publish step for server code. Say so to the user.
|
|
142
|
+
4. `xflow deploy apps/zapis -m "what changed"` sends and builds one app; inside `apps/zapis` a
|
|
143
|
+
plain `xflow deploy` does the same. In the root a plain `xflow deploy` refuses and names the
|
|
144
|
+
targets. The comment is required: one short line in the language the user speaks.
|
|
145
|
+
5. `xflow publish` in the app folder (or with `--app <id>`) shows that build to visitors.
|
|
146
|
+
|
|
147
|
+
A change that touches data and screens goes in this order: the migration, the functions, then
|
|
148
|
+
every app that shows it. `xflow deploy apps/...` warns when the root holds changes nobody
|
|
149
|
+
deployed.
|
|
150
|
+
|
|
151
|
+
- A build of an app is not shown to visitors until `xflow publish`. Publishing points the
|
|
152
|
+
address at a build and takes a second; `xflow publish <number>` (or `xflow rollback <number>`)
|
|
153
|
+
points it at an older kept build, and that is the rollback. The very first build of a new
|
|
154
|
+
app publishes itself.
|
|
155
|
+
- Only the app moves on publish and rollback. Functions come back with
|
|
156
|
+
`xflow deploy functions --revision N -m "why"` (`xflow deployments functions` lists the revisions);
|
|
157
|
+
`--project <id>` names the project without a folder.
|
|
158
|
+
- Files of the **three latest successful builds** of each app are kept, plus the live one;
|
|
159
|
+
`xflow deployments` marks the rest `no pages`. The history keeps 30 builds, a revision stays
|
|
160
|
+
while its build is in it or it is among the 10 latest. Keep anything you must not lose in
|
|
161
|
+
your own git repository.
|
|
162
|
+
- Give a person the app address the CLI printed, or the project page it printed. Name builds
|
|
163
|
+
by their number.
|
|
164
|
+
- `xflow status` says how the local copies differ from the server. A deploy rejected before
|
|
165
|
+
the build starts means the server holds a revision this folder has never seen: fetch it next
|
|
166
|
+
to yours (`xflow pull --into ./server-copy`), merge in git, deploy again. `--force` destroys
|
|
167
|
+
that work. A folder with no record of the server revision (a fresh clone, a CI checkout) is
|
|
168
|
+
not rejected: its first deploy goes on top and says so.
|
|
169
|
+
- A deploy that would delete functions gone from the sources refuses until somebody agrees;
|
|
170
|
+
`--allow-removals` is the only way past it without a terminal. Reaching for it to get a
|
|
171
|
+
build running means you deleted something the user never asked you to: put the directories
|
|
172
|
+
back. If the removal is intended, name the functions and let the user answer.
|
|
140
173
|
|
|
141
174
|
## Build gate
|
|
142
175
|
|
|
143
|
-
Before the sandbox starts, the platform checks the sources
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
-
|
|
152
|
-
|
|
153
|
-
- `
|
|
154
|
-
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
and
|
|
162
|
-
contents: interface text stays in whatever language the application speaks.
|
|
163
|
-
|
|
164
|
-
Platform contract, checked across all of `src/`:
|
|
165
|
-
|
|
166
|
-
- Call cloud functions through `xflow.functions.invoke`, never through a hardcoded
|
|
167
|
-
`*.yandexcloud.net` URL: the address changes and the app breaks silently.
|
|
168
|
-
- No API keys or tokens in the source: they end up in the bundle. Put the call in a
|
|
169
|
-
cloud function and the key in project secrets.
|
|
170
|
-
- No server modules (`fs`, `express`, `http`, `child_process`): there is no server runtime.
|
|
171
|
-
|
|
172
|
-
Interface rules, checked outside `src/components/ui` and `src/components/blocks`:
|
|
173
|
-
|
|
174
|
-
- No `alert()`, `confirm()`, `prompt()`. Use the Dialog and Toast components.
|
|
175
|
-
- No `console.log`. Deployed apps have a public console, and forgotten debugging prints
|
|
176
|
-
user data into it. `console.error` and `console.warn` are fine, they reach the project
|
|
177
|
-
logs.
|
|
178
|
-
- No inline styles with literal values (`style={{ color: '#fff' }}`). Computed styles
|
|
179
|
-
(a drag transform, a progress width) are fine, Tailwind cannot express them.
|
|
180
|
-
- No hex colors or Tailwind palette classes (`text-gray-500`): use the theme tokens.
|
|
181
|
-
Charts are exempt, they need real colors.
|
|
182
|
-
- No importing a `@/components/ui/*` component that does not exist in the project.
|
|
176
|
+
Before the sandbox starts, the platform checks the sources. Everything below blocks the build,
|
|
177
|
+
all violations come back together with files and lines, and a rejected attempt costs nothing.
|
|
178
|
+
|
|
179
|
+
An app:
|
|
180
|
+
|
|
181
|
+
- `package.json` with the build script named in its `xflow.json` (`npm run build` by default),
|
|
182
|
+
a `vite.config.*` and `vite` in dependencies. No server frameworks: `next`, `nuxt`, Remix,
|
|
183
|
+
`@sveltejs/kit`, `astro`.
|
|
184
|
+
- `index.html` in the app root with `id="root"` and the script that loads `src/main.tsx`
|
|
185
|
+
(`.ts`, `.jsx`, `.js` count too). Code under `src/`; root `app/`, `pages/`, `next/` are rejected.
|
|
186
|
+
- Keep `base` in `vite.config.js` as the template has it: the platform serves each build from
|
|
187
|
+
`/_b/<key>/`, and a build whose `index.html` points elsewhere is refused.
|
|
188
|
+
- The built app at most 15 MB unpacked, the sources at most 10 MB.
|
|
189
|
+
- File names in latin letters, digits, spaces and `. - _ ~` only: `логотип.png` breaks the build.
|
|
190
|
+
- Call functions through `xflow.functions.invoke`, never a hardcoded function address.
|
|
191
|
+
- No keys or tokens in the sources, no server modules (`fs`, `express`, `http`, `child_process`).
|
|
192
|
+
- Outside `src/components/ui` and `src/components/blocks`: no `alert()`, `confirm()`,
|
|
193
|
+
`prompt()` (use Dialog and Toast), no `console.log` (visitors read the console;
|
|
194
|
+
`console.error` and `console.warn` reach the logs).
|
|
183
195
|
- A library that needs a provider (`@tanstack/react-query`, `react-redux`, `sonner`,
|
|
184
|
-
`react-hot-toast`, `react-dnd`) must have it mounted
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
`
|
|
196
|
+
`react-hot-toast`, `react-dnd`) must have it mounted. A missing provider builds fine and
|
|
197
|
+
gives visitors a white screen.
|
|
198
|
+
- The platform files stay byte for byte as they arrived: `src/lib/platform-auth.ts`,
|
|
199
|
+
`src/contexts/platform-auth-context.tsx`, `src/hooks/use-platform-auth.ts`,
|
|
200
|
+
`src/utils/error-logger.ts`, `src/lib/xflow.ts`. The code keeps calling `initPlatformAuth()`
|
|
201
|
+
and `initErrorLogger()` and importing `index.css`, and the Tailwind
|
|
202
|
+
`content` globs keep covering `src/**`. A rejection names the revision to take the original
|
|
203
|
+
from: `xflow pull --revision N --into ./original`, then copy the file back.
|
|
204
|
+
|
|
205
|
+
The project root: function names are lowercase latin letters, digits and hyphens, up to 41
|
|
206
|
+
characters (`send-mail`, not `sendMail`); every name under `functions` in `xflow.json` has its
|
|
207
|
+
`functions/<name>/index.ts`; every caller is an app folder of this project or an existing page
|
|
208
|
+
of the organization (`unknown-caller`).
|
|
209
|
+
|
|
210
|
+
Passes with a line `Unfinished:` instead of blocking: the template's page title, no
|
|
211
|
+
`<link rel="icon">` in `index.html`, an `exports.access` left in a function (it does nothing).
|
|
212
|
+
Fix the first two before you tell the person the app is done.
|
|
213
|
+
|
|
214
|
+
## The template is a starter kit
|
|
215
|
+
|
|
216
|
+
- `src/components/ui` and `src/components/blocks` are a starting set: change them, restyle them
|
|
217
|
+
or delete what you do not use. Any colors, your own color scheme, inline styles, a single
|
|
218
|
+
theme without dark mode: all allowed.
|
|
219
|
+
- The template looks like the xflow interface: warm grays, hairline controls (`ctl-solid`,
|
|
220
|
+
`ctl-raised`, `ctl-inset`, `ctl-select`), segments (`seg-track`, `seg-on`) and surfaces
|
|
221
|
+
(`surface-raised`, `surface-pop`, `surface-modal`) in `src/index.css`.
|
|
222
|
+
- While any template component is left, the theme token names in `:root` of `src/index.css`
|
|
223
|
+
and their `hsl(var(--name))` mapping in the Tailwind config stay: those components paint with
|
|
224
|
+
them. Values change freely; they are HSL components (`216 95% 40%`), not `#hex`. Your own
|
|
225
|
+
colors go under new names.
|
|
226
|
+
- Images and other assets: import them from `src/` (`import logo from './assets/logo.png'`)
|
|
227
|
+
so the build puts them next to the code. An absolute path into `public/` (`/logo.png`) goes
|
|
228
|
+
through the access check of the app and fails inside the xflow frame in Safari.
|
|
229
|
+
|
|
230
|
+
## Who opens an app
|
|
231
|
+
|
|
232
|
+
A person sets this in the web interface; you cannot, and you should not ask for a way around.
|
|
233
|
+
|
|
234
|
+
| Level | Who gets in |
|
|
235
|
+
|---|---|
|
|
236
|
+
| Project members | people who have access to the project: the team through xflow, staff with a login |
|
|
237
|
+
| By list | the members named for this app |
|
|
238
|
+
| Anyone with the link | anybody, no sign-in at all |
|
|
239
|
+
|
|
240
|
+
- The plan decides which levels exist: on Free an app opens only inside xflow, Start adds the
|
|
241
|
+
direct address and "By list", Business adds "Anyone with the link".
|
|
242
|
+
- At the two closed levels the platform signs people in, and a function receives the person in
|
|
243
|
+
`event.xflow.user` (`id`, `role` in the organization, `name`, `login`).
|
|
244
|
+
- Staff (a doctor, a picker) never sign in to xflow: an admin gives them a login and a password,
|
|
245
|
+
they enter at the address of the app, `login` is set for them and null for the team. Keep
|
|
246
|
+
roles inside the app (doctor, receptionist) in your own table keyed by `user.id`.
|
|
247
|
+
- A "Sign out" button: `const r = await fetch('/_xflow/logout', { method: 'POST' })`, then
|
|
248
|
+
`location.href = (await r.json()).next`. It ends every sign-in of the person.
|
|
249
|
+
- At "Anyone with the link" the platform signs nobody in: `event.xflow.user` is null for
|
|
250
|
+
visitors. Accounts, sign-in and who sees which rows are the code of your app, see below.
|
|
251
|
+
- `usePlatformAuth()` in the frontend knows the person only inside the xflow frame, and even
|
|
252
|
+
there it is a hint for drawing the interface, edited from the console by anyone. On the
|
|
253
|
+
direct address it answers `anonymous`. Guard data in the function, by `event.xflow`.
|
|
254
|
+
|
|
255
|
+
## Which functions an app may call
|
|
256
|
+
|
|
257
|
+
The root `xflow.json` names the callers of each function:
|
|
258
|
+
|
|
259
|
+
{
|
|
260
|
+
"projectId": "…",
|
|
261
|
+
"functions": {
|
|
262
|
+
"list-slots": { "callers": ["zapis", "priem"] },
|
|
263
|
+
"book-slot": { "callers": ["zapis", "clinic-landing"] }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
- A caller is an app folder of this project (`apps/zapis` is `"zapis"`) or the address name of
|
|
268
|
+
a page of the organization (`clinic` for `clinic.xflowpage.ru`). A function without callers is
|
|
269
|
+
left out of `functions` or given `"callers": []`, the same thing.
|
|
270
|
+
- A function nobody lists has no callers: no app and no page reaches it. That is a workflow,
|
|
271
|
+
run by a schedule, a key or `xflow functions invoke`.
|
|
272
|
+
- Callers ship with `xflow deploy functions` and come back with its `--revision N`.
|
|
273
|
+
- The app calls `xflow.functions.invoke('book-slot', { body })` from `src/lib/xflow.ts`. The call
|
|
274
|
+
goes to `/_fn/book-slot` on the app's own address: no function addresses, tokens or keys in
|
|
275
|
+
the bundle, nothing to rebuild when a function changes.
|
|
276
|
+
- A refusal throws `XFlowError` with `status` and `code`: `not_bound` (404, add the caller and
|
|
277
|
+
deploy functions), `not_found` (404, the function is not deployed), `login_required` and
|
|
278
|
+
`invalid_credential` (401, the visitor has to sign in again), `no_access`, `role_too_low`,
|
|
279
|
+
`functions_stopped` (403, minutes ran out or the plan is unpaid: a person settles it),
|
|
280
|
+
`rate_limited` (429), `unavailable` (503, retry later). Branch on `code`, never on the message.
|
|
281
|
+
- A page has to exist before a function can name it: create the page, then deploy functions.
|
|
282
|
+
|
|
283
|
+
## Your function is open to the whole internet
|
|
284
|
+
|
|
285
|
+
A function is open when any of its callers is public: a page (always) or an app at "Anyone
|
|
286
|
+
with the link". `xflow deploy functions` says so in a line of its own, `xflow functions list`
|
|
287
|
+
prints `open`, the web interface marks it.
|
|
288
|
+
|
|
289
|
+
Open means anybody can call it with curl, without your page: the browser is not a gate.
|
|
290
|
+
Hiding the address, checking `Origin` or `Referer`, a token baked into the bundle: all of it is
|
|
291
|
+
readable or forgeable and protects nothing. Write every open function as if the request came
|
|
292
|
+
from a stranger, because it may:
|
|
293
|
+
|
|
294
|
+
- Check every input: types, lengths, allowed values; refuse fields you do not expect. Put input
|
|
295
|
+
into SQL as parameters only (`$1`), never into the text.
|
|
296
|
+
- Return only what this caller needs: no `select *` into a response, no rows of other people,
|
|
297
|
+
no internal fields, no stack traces.
|
|
298
|
+
- Make writes safe to repeat: take a request id from the client and put a unique index on it,
|
|
299
|
+
so a retry or a replay changes nothing.
|
|
300
|
+
- Deduplicate and limit per visitor where it costs: `event.xflow.ip` is the visitor's address,
|
|
301
|
+
set by the platform proxy and not by the caller. The proxy caps requests per address; the
|
|
302
|
+
organization pays for every minute a function runs.
|
|
303
|
+
- A form open to strangers needs a captcha: Cloudflare Turnstile, with the token checked in the
|
|
304
|
+
function against `https://challenges.cloudflare.com/turnstile/v0/siteverify` and the secret
|
|
305
|
+
from `xflow env set`. The site key is public and stays in the source. A check in the browser
|
|
306
|
+
checks nothing.
|
|
307
|
+
- `minRole` on an open function refuses every anonymous visitor (`login_required`): use it only
|
|
308
|
+
where members call.
|
|
309
|
+
|
|
310
|
+
## Sign-in in a public app
|
|
311
|
+
|
|
312
|
+
At the closed levels do none of this: the platform already knows the person.
|
|
313
|
+
|
|
314
|
+
- Passwords: store only a hash, `crypto.scrypt` from Node with a random salt per user, compare
|
|
315
|
+
with `crypto.timingSafeEqual`. Never log a password, never return it, never keep it in the
|
|
316
|
+
browser.
|
|
317
|
+
- Session: after sign-in the function makes a random token (`crypto.randomBytes(32)`), stores
|
|
318
|
+
its SHA-256 and an expiry in a table and returns it as a cookie in its response headers:
|
|
319
|
+
`Set-Cookie: __Host-sid=…; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=…`. The `__Host-`
|
|
320
|
+
prefix stops a neighbouring `*.xflowpage.ru` site from planting its own `sid`. `/_fn` is on
|
|
321
|
+
the same address, so the browser sends it back by itself. Not `localStorage`: every script on
|
|
322
|
+
the page reads it.
|
|
323
|
+
- Not the `Authorization` header: `/_fn` replaces it with the platform's own. Nor headers named
|
|
324
|
+
`X-Xflow-*`, `X-Project-Pass`, `X-Server-Key`, `X-Function-Key`: the platform strips them.
|
|
325
|
+
Cookies reach the function as they are.
|
|
326
|
+
- Every function that serves a signed-in visitor reads the cookie from `event.headers`, finds
|
|
327
|
+
the session and filters rows by that user in SQL. A hidden button is not access control.
|
|
328
|
+
- Count failed sign-ins per email and per `event.xflow.ip` in a table and slow them down.
|
|
329
|
+
- Never in the frontend: keys, secrets, the list of users, hashes, a role check that is the only
|
|
330
|
+
guard.
|
|
331
|
+
- The platform sends no email for your app: confirmation and reset letters need a mail provider
|
|
332
|
+
whose key goes into `xflow env set`.
|
|
333
|
+
- `xflow dev` carries neither cookies nor `Set-Cookie`, and calls functions as you (a member).
|
|
334
|
+
Try the sign-in on the app address.
|
|
335
|
+
|
|
336
|
+
## Page or app
|
|
337
|
+
|
|
338
|
+
- A page is finished static files at `https://<name>.xflowpage.ru`: public, live the moment it
|
|
339
|
+
is uploaded, no build, no publish step, no database. A landing page, a presentation, an invoice.
|
|
340
|
+
- An app is source the platform builds: versions and publish, access levels, a database through
|
|
341
|
+
functions. Anything people sign in to, enter data into or come back to.
|
|
342
|
+
- A form open to strangers on a plan without "Anyone with the link" is a page: pages are public
|
|
343
|
+
on every plan.
|
|
344
|
+
- Keep the sources of a page outside the project folder, or list them in `.xflowignore`: the
|
|
345
|
+
root travels with `deploy functions`.
|
|
346
|
+
- A page that needs a backend (a form, a lookup) calls `/_fn/<name>` on its own address with the
|
|
347
|
+
header `X-Xflow-Fn: 1` (without it 403 `bad_request`), and a function of a project answers only
|
|
348
|
+
when its callers name the page. The function is then open. A refusal carries its code in
|
|
349
|
+
`reason` of the JSON body, the same codes as above.
|
|
210
350
|
|
|
211
351
|
## Cloud functions
|
|
212
352
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
`
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
its
|
|
239
|
-
|
|
240
|
-
Inside a standalone function, remember who can reach it: any member of the organization,
|
|
241
|
-
including someone whose access to projects is narrowed to one folder. The role in `minRole` is
|
|
242
|
-
the role in the organization, so a function holding data that is not for everyone checks the
|
|
243
|
-
caller itself (`event.xflow.user`).
|
|
244
|
-
|
|
245
|
-
Keep one shape inside that string across the whole project: `{ success: true, data }`
|
|
246
|
-
when it worked, `{ success: false, error: { message, code } }` when it did not. Nothing
|
|
247
|
-
enforces this, but a project where every function answers its own way costs an adapter
|
|
248
|
-
on every call. Branch the frontend on `error.code`, never on `error.message`: wording
|
|
249
|
-
gets rewritten on any edit, a code does not.
|
|
250
|
-
|
|
251
|
-
How a non-2xx answer reaches the page depends on the generation of `src/lib/xflow.ts`, so read
|
|
252
|
-
that file before writing the catch: a copy declaring `XFlowError` throws it with `code` and
|
|
253
|
-
`status`, an older copy throws a plain `Error` whose message is all there is. In an older
|
|
254
|
-
project, a function whose error text must be seen answers `200` with `{ success: false, error }`.
|
|
255
|
-
|
|
256
|
-
The sources are the whole truth about which functions exist: a deleted directory takes the
|
|
257
|
-
function and its schedules out of the cloud, undoably, and a function created again later gets
|
|
258
|
-
a different address. Such a deploy refuses until somebody agrees, and `--allow-removals` is the
|
|
259
|
-
only way past it. Reaching for that flag to get a build running means you deleted something the
|
|
260
|
-
user never asked you to delete: put the directories back. If the removal is intended, name the
|
|
261
|
-
functions about to go and let the user answer.
|
|
262
|
-
|
|
263
|
-
Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it the way
|
|
264
|
-
the app does and prints status, timing and body (`--data '{"a":1}'`, `--data-file body.json`,
|
|
265
|
-
`--json` for the body alone), and `xflow functions logs <name>` shows the failures with stacks
|
|
266
|
-
and console output. Only failed calls are logged, so empty output means it never crashed.
|
|
353
|
+
`functions/<name>/index.ts` in the project root exports `handler` (`index.js` and `index.mjs`
|
|
354
|
+
are read too), `export const handler` and `exports.handler` both work. The request arrives as
|
|
355
|
+
`event.httpMethod`, `event.headers`, `event.queryStringParameters` and `event.body`, a string
|
|
356
|
+
(`JSON.parse` it; `event.isBase64Encoded` marks a binary body). The handler returns
|
|
357
|
+
`{ statusCode, body }` where `body` is a JSON string, and may add `headers`.
|
|
358
|
+
`xflow functions list` shows status, callers, `open`, keys, last call and last error.
|
|
359
|
+
|
|
360
|
+
- Dependencies (`pg`, a mail client) go into the root `package.json`; the platform installs them
|
|
361
|
+
on every deploy, and a lock file next to it makes that exact.
|
|
362
|
+
- Code shared by functions lives in a root folder such as `lib/`, imported by relative path: each
|
|
363
|
+
function is bundled with what it imports. A folder in `functions/` without `index.*` is not a
|
|
364
|
+
function. Read `process.env` in `functions/<name>/` itself: `xflow env check` looks only there.
|
|
365
|
+
|
|
366
|
+
Keep one shape inside that string across the project: `{ success: true, data }` when it worked,
|
|
367
|
+
`{ success: false, error: { message, code } }` when it did not. Branch the frontend on
|
|
368
|
+
`error.code`, never on `error.message`.
|
|
369
|
+
|
|
370
|
+
A function whose code and variables did not change is not shipped again, and one that fails to
|
|
371
|
+
ship fails the whole deploy.
|
|
372
|
+
|
|
373
|
+
Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it as you,
|
|
374
|
+
through the platform, and prints status, timing and body (`--data '{"a":1}'`, `--data-file
|
|
375
|
+
body.json`, `--method GET`, `--json` for the body alone); `xflow functions logs <name>` shows
|
|
376
|
+
the crashes and 5xx answers with stacks and console output, the latest 10 (`--limit N` for more).
|
|
377
|
+
Only failed calls are logged. Empty output does not clear a function that fails at module start,
|
|
378
|
+
runs over its time cap or out of memory: those never reach the logs, `invoke` shows them.
|
|
267
379
|
|
|
268
380
|
The body has to be JSON and the CLI checks it before calling. In PowerShell the double quotes
|
|
269
381
|
never reach the CLI (`--data '{"a":1}'` arrives as `{a:1}`): escape them (`--data '{\"a\":1}'`)
|
|
270
382
|
or pass a file. It bites `--payload` of a schedule the same way. A command that worked writes
|
|
271
383
|
nothing to stderr, so a red line there means it really failed.
|
|
272
384
|
|
|
273
|
-
From the app, call a function through `src/lib/xflow.ts`:
|
|
274
|
-
`await xflow.functions.invoke('send-mail', { body: { to } })`, which carries the credentials for
|
|
275
|
-
you. Addresses are baked into the build, which is why functions go out first: by the time the
|
|
276
|
-
bundle is built they already exist.
|
|
277
|
-
|
|
278
385
|
### Who is calling
|
|
279
386
|
|
|
280
|
-
|
|
281
|
-
service holding a key of that function, and, when the code opens it, anyone at all. The
|
|
282
|
-
platform issues a short-lived pass when it opens the application, the wrapper checks it
|
|
283
|
-
with the platform on every call, and the handler receives the answer in `event.xflow`:
|
|
387
|
+
The platform checks every call before your code runs and hands the answer over in `event.xflow`:
|
|
284
388
|
|
|
285
389
|
```js
|
|
286
390
|
exports.handler = async (event) => {
|
|
287
|
-
const { caller, user } = event.xflow
|
|
288
|
-
// caller: '
|
|
289
|
-
// 'external' (an outside service with a key), 'schedule'
|
|
290
|
-
// '
|
|
291
|
-
//
|
|
391
|
+
const { caller, slug, user, ip } = event.xflow
|
|
392
|
+
// caller: 'app' (an app of this project), 'page', 'service' (another function of this
|
|
393
|
+
// project), 'external' (an outside service with a key), 'schedule',
|
|
394
|
+
// 'platform' (a person through xflow functions invoke or the web interface)
|
|
395
|
+
// slug: the address name of the calling app (<id>--<org>) or page, null otherwise
|
|
396
|
+
// user: { id, role, name, login } of a signed-in person (login only for staff),
|
|
397
|
+
// null for anonymous visitors, pages, keys
|
|
398
|
+
// ip: the visitor's address, null for a schedule
|
|
292
399
|
}
|
|
293
400
|
```
|
|
294
401
|
|
|
295
|
-
Never trust an identity that arrives in the body or in a header of the request:
|
|
296
|
-
|
|
297
|
-
identity the platform stands behind, and `usePlatformAuth()` in the frontend is a hint for
|
|
298
|
-
the interface, not a check.
|
|
402
|
+
Never trust an identity that arrives in the body or in a header of the request: the page lives
|
|
403
|
+
on someone else's computer. `event.xflow` is the only identity the platform stands behind.
|
|
299
404
|
|
|
300
|
-
A function that changes data should say
|
|
405
|
+
A function that changes data should say who may call it instead of checking the role by hand:
|
|
301
406
|
|
|
302
407
|
```js
|
|
303
408
|
exports.minRole = 'developer' // 'member' | 'developer' | 'owner'
|
|
304
409
|
```
|
|
305
410
|
|
|
306
|
-
The
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
it in the project settings, «Облачные функции» → the function → «Настройки»; a standalone
|
|
322
|
-
function has a screen of its own, «Функции» → the function → «Настройки». Ask the user to do
|
|
323
|
-
that — never invent another way in. The service sends the key in the `X-Function-Key` header
|
|
324
|
-
to the address `xflow functions list` prints. A function holds at most two keys, the second
|
|
325
|
-
one to replace the first without downtime, not to serve a second consumer.
|
|
326
|
-
|
|
327
|
-
A function behind a public page (a form on a landing, a lookup, a calculator on
|
|
328
|
-
`xflowpage.ru`) has neither a person nor a key: every visitor calls it. Declare that in the
|
|
329
|
-
code and nothing else is needed, no key, no setting, nobody to ask:
|
|
330
|
-
|
|
331
|
-
```js
|
|
332
|
-
exports.access = 'public'
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
The wrapper then lets a call without any credential through, with `caller: 'public'` and
|
|
336
|
-
`user: null`, and never asks the platform. The page calls the address from `xflow functions
|
|
337
|
-
list` directly (CORS is open) and bakes no key into its files. Everything else stays as it
|
|
338
|
-
is: a call that does carry a pass or a key is still checked. Use it only for a function whose
|
|
339
|
-
work is fine for anyone on the internet to trigger: it cannot hold `minRole`, and it must not
|
|
340
|
-
write or delete data on the caller's word, there is no caller to trust. The platform marks
|
|
341
|
-
such a function «Открыта всем»
|
|
342
|
-
in the interface, and removing the line and deploying closes it again.
|
|
343
|
-
|
|
344
|
-
`xflow functions list` shows who can reach each function: `in-app only` (answers only inside
|
|
345
|
-
the application), `external (N keys)` (a human issued external access), `public` (the code
|
|
346
|
-
opened it), and the address of every deployed function. Key values are never shown there.
|
|
347
|
-
Under the table it names the standalone functions of the organization this project calls by
|
|
348
|
-
the same name, each with the `--project` id its code lives in: that list and the table
|
|
349
|
-
together are everything `xflow.functions.invoke` reaches from here. A deploy prints the
|
|
350
|
-
address of every function it left open, so a page needs no second command to find it.
|
|
411
|
+
The platform refuses anything below that role before your code runs; it applies to people
|
|
412
|
+
(apps, pages, `functions invoke`), not to keys and schedules. `member` is the people who only
|
|
413
|
+
use the apps (staff of a clinic, managers), `developer` and `owner` build them. Without the line
|
|
414
|
+
every member of the project can call the function. Losing access
|
|
415
|
+
closes the function within a minute.
|
|
416
|
+
|
|
417
|
+
- Another function of the project: `fetch(`${process.env.XFLOW_FN_URL}/<name>`, { headers:
|
|
418
|
+
{ 'X-Server-Key': process.env.XFLOW_SERVER_KEY } })`. The key opens the functions of its own
|
|
419
|
+
project only, whatever their callers.
|
|
420
|
+
- An outside service (a payment webhook, a bot, a CRM) calls
|
|
421
|
+
`https://fn.xflowpage.ru/<projectId>/<name>` (`xflow functions list` prints it) with a key of
|
|
422
|
+
that function in `X-Function-Key` or `?key=`. Keys are issued by a person on the function's
|
|
423
|
+
screen in the web interface, never by you: ask the user. A function holds at most two keys,
|
|
424
|
+
the second one to replace the first without downtime.
|
|
425
|
+
- `exports.access` does nothing any more: who may call is `callers`.
|
|
351
426
|
|
|
352
427
|
## Secrets and connected accounts
|
|
353
428
|
|
|
354
429
|
Keys and passwords live on the platform, not in the repository: `xflow env set SMTP_PASSWORD=…`
|
|
355
430
|
writes one, `xflow env` lists the names, `xflow env check` names the variables your functions
|
|
356
431
|
read but the platform does not have. Values never come back out: the only place they exist is
|
|
357
|
-
inside the running function.
|
|
432
|
+
inside the running function. Apps get no variables at all.
|
|
358
433
|
|
|
359
434
|
A variable goes to the whole organization unless you say otherwise: `--scope project` keeps it
|
|
360
|
-
to this project alone, `--scope organization` is the default, and a third word is refused
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
These commands see only what this project can see: organization-wide variables and the ones
|
|
365
|
-
bound to this project. One bound to another project is invisible here, so `env rm` reports it
|
|
366
|
-
missing even though names are unique within the organization.
|
|
435
|
+
to this project alone, `--scope organization` is the default, and a third word is refused.
|
|
436
|
+
Pick the narrow one for anything that belongs to one project. These commands see only what
|
|
437
|
+
this project can see, so `env rm` reports a variable of another project missing even though
|
|
438
|
+
names are unique within the organization.
|
|
367
439
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
whose secret never arrives. A value written today rides the next `xflow deploy`.
|
|
440
|
+
`DATABASE_URL` and every name starting with `XFLOW` are filled in by the platform: writing them
|
|
441
|
+
is refused.
|
|
371
442
|
|
|
372
443
|
Some variables come from a connected account instead of from you. Credentials of an account
|
|
373
444
|
linked to the project show up in `xflow env` marked read-only, under a prefix chosen at link
|
|
374
|
-
time (`YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_CLIENT_LOGIN`): read them like any other variable
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
445
|
+
time (`YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_CLIENT_LOGIN`): read them like any other variable.
|
|
446
|
+
If calls to that service start failing on authorisation, `xflow status` says whether the token
|
|
447
|
+
is merely expiring (any deploy of the functions renews it) or the account was disconnected on
|
|
448
|
+
the provider's side, which only a human can reconnect.
|
|
378
449
|
|
|
379
|
-
`xflow connections` lists those accounts: the ones
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
unavailable: the account you need is often connected already, one link away.
|
|
450
|
+
`xflow connections` lists those accounts: the ones linked to this project, with the alias and
|
|
451
|
+
the state of the access, and the ones the organization has but this project does not use yet,
|
|
452
|
+
marked `available, not linked`. Check it before telling anyone a service is unavailable.
|
|
383
453
|
|
|
384
454
|
`xflow connections link "Яндекс Метрика" --as YANDEX_METRIKA` is that link and
|
|
385
|
-
`xflow connections unlink YANDEX_METRIKA` undoes it; `xflow help connections` has the
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
list before reaching for `--force`. Under `--project` there is no folder to read: it
|
|
389
|
-
prints the variables that go and removes the link.
|
|
455
|
+
`xflow connections unlink YANDEX_METRIKA` undoes it; `xflow help connections` has the naming
|
|
456
|
+
rules. In the project folder unlink refuses while a function still reads one of the variables,
|
|
457
|
+
naming those functions, so read that list before reaching for `--force`.
|
|
390
458
|
|
|
391
459
|
Linking needs the `connections:link` right on the key, and only accounts granted to the owner
|
|
392
|
-
of the key personally can be linked at all
|
|
393
|
-
|
|
394
|
-
live. Connecting a new account and switching one off stay with a person too.
|
|
460
|
+
of the key personally can be linked at all. A key cannot grant it to itself: ask the person.
|
|
461
|
+
Connecting a new account and switching one off stay with a person too.
|
|
395
462
|
|
|
396
463
|
### Asking an API what it returns
|
|
397
464
|
|
|
398
|
-
Before writing a function against an unfamiliar API, find out what it actually answers:
|
|
399
|
-
|
|
400
465
|
```
|
|
401
466
|
xflow connections call WB /api/v1/supplier/stocks?dateFrom=2026-09-01
|
|
402
467
|
xflow connections call "Яндекс Метрика" /management/v1/counters
|
|
@@ -404,58 +469,41 @@ xflow connections call OZON /v1/product/list --method POST --data '{}' \
|
|
|
404
469
|
--header '{"Client-Id":"{CLIENT_ID}"}'
|
|
405
470
|
```
|
|
406
471
|
|
|
407
|
-
The platform attaches the credentials and makes the request itself
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
**Calling is looking, not wiring**: no variable reaches the functions because you called
|
|
412
|
-
something. To use the account in code, link it and deploy.
|
|
413
|
-
|
|
414
|
-
Two refusals do need a person, and different ones: `connections:link` right — the owner of the
|
|
415
|
-
key grants it where the key permissions live; `not granted to you` — ask whoever connected the account.
|
|
416
|
-
|
|
417
|
-
`{NAME}` in the path or in a header value is filled in from the fields of the connection.
|
|
418
|
-
**Take the names from `xflow connections`**, which prints them in braces for every account,
|
|
419
|
-
linked or not: an invented name is left in the text as you typed it and the request goes out
|
|
420
|
-
anyway, into somebody's live account. `xflow env` is not that list. Only the hosts of that
|
|
421
|
-
connector are reachable and a refusal lists them.
|
|
472
|
+
The platform attaches the credentials and makes the request itself: no deploy, and the key never
|
|
473
|
+
reaches you. A linked account is called by its alias, an `available, not linked` one by its
|
|
474
|
+
name: linking first is not needed to look. Calling is looking, not wiring: to use the account
|
|
475
|
+
in code, link it and deploy functions.
|
|
422
476
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
person first when the endpoint is not clearly a read.
|
|
477
|
+
`{NAME}` in the path or in a header value is filled in from the fields of the connection. Take
|
|
478
|
+
the names from `xflow connections`, which prints them in braces: an invented name is left in the
|
|
479
|
+
text and the request goes out anyway, into somebody's live account. Ask for the shape, not the
|
|
480
|
+
volume: one day and `limit=1`; `--json | jq '.response.body | keys'` gives the field names
|
|
481
|
+
without the body entering your context. `POST` and `DELETE` change things in a live account and
|
|
482
|
+
nothing undoes that: ask the person first when the endpoint is not clearly a read.
|
|
430
483
|
|
|
431
484
|
## Schedules
|
|
432
485
|
|
|
433
486
|
`xflow schedules set report "0 3 ? * * *"` runs a function daily at 03:00. Six fields, UTC,
|
|
434
|
-
and exactly one of day-of-month / day-of-week must be
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
is refused when the schedule is set, not at three in the morning with nobody watching. How often
|
|
438
|
-
a schedule may run is a plan limit, see **Plan limits**.
|
|
439
|
-
|
|
440
|
-
The pieces line up in one pass. From a new function to a verified schedule:
|
|
487
|
+
and exactly one of day-of-month / day-of-week must be `?`. A run reaches the handler as a POST
|
|
488
|
+
with `event.xflow.caller === 'schedule'`, and `--payload '{"mode":"full"}'` (or
|
|
489
|
+
`--payload-file body.json`) is its body. The function has to be deployed first.
|
|
441
490
|
|
|
442
491
|
```
|
|
443
|
-
xflow env set SMTP_PASSWORD=...
|
|
492
|
+
xflow env set SMTP_PASSWORD=... # secrets first: values ride the next deploy
|
|
444
493
|
# write functions/report/index.ts, reading process.env.SMTP_PASSWORD literally
|
|
445
|
-
xflow deploy -m "Nightly report
|
|
446
|
-
xflow schedules set report "0 3 ? * * *" --payload '{"mode":"full"}'
|
|
447
|
-
xflow functions invoke report
|
|
448
|
-
xflow functions logs report
|
|
494
|
+
xflow deploy functions -m "Nightly report"
|
|
495
|
+
xflow schedules set report "0 3 ? * * *" --payload '{"mode":"full"}'
|
|
496
|
+
xflow functions invoke report # run it once
|
|
497
|
+
xflow functions logs report # crashes of real runs
|
|
449
498
|
```
|
|
450
499
|
|
|
451
500
|
## Database
|
|
452
501
|
|
|
453
|
-
Schema changes are files: `migrations/0001_init.sql`, `migrations/0002_orders.sql
|
|
454
|
-
in filename order by `xflow db migrate
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
write a new migration instead.
|
|
502
|
+
Schema changes are files: `migrations/0001_init.sql`, `migrations/0002_orders.sql` in the
|
|
503
|
+
project root, applied in filename order by `xflow db migrate` from any folder of the project.
|
|
504
|
+
`xflow db status` shows what is applied and what waits. Never edit an applied file, write a new
|
|
505
|
+
migration instead. Every project has exactly one database of its own, shared by all its apps and
|
|
506
|
+
functions.
|
|
459
507
|
|
|
460
508
|
The browser never reaches the database directly. The app reads and writes through a cloud
|
|
461
509
|
function, and inside the handler the connection string is already there:
|
|
@@ -465,102 +513,71 @@ const { Client } = require('pg')
|
|
|
465
513
|
const db = new Client({ connectionString: process.env.DATABASE_URL })
|
|
466
514
|
```
|
|
467
515
|
|
|
468
|
-
The platform passes `DATABASE_URL` only to functions that mention it
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
`
|
|
472
|
-
|
|
516
|
+
The platform passes `DATABASE_URL` only to functions that mention it and sets the project schema
|
|
517
|
+
on every connection, so plain table names (`select * from tasks`) hit your project.
|
|
518
|
+
|
|
519
|
+
`migrations/` says what you did; `xflow db schema [table]` says what is in the database now, and
|
|
520
|
+
`xflow db query 'select ...'` reads it inside a READ ONLY transaction (50 rows by default,
|
|
521
|
+
`--limit` up to 300; `--json` prints long values whole). Single quotes hold in PowerShell, bash
|
|
522
|
+
and zsh, cmd.exe wants double ones; a query with quotes of its own goes in a `.sql` file outside
|
|
523
|
+
the project, `xflow db query ../q.sql`.
|
|
473
524
|
|
|
474
525
|
The platform keeps no database history and no backups, so anything that destroys data
|
|
475
526
|
(`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE FROM` without a condition) needs both the
|
|
476
527
|
`--allow-destructive` flag and the right to destroy data on the key, which is off by default and
|
|
477
528
|
only its owner turns on. When the refusal is about the right, the flag changes nothing: say what
|
|
478
529
|
needs deleting and why, and let the person decide. With both in place the affected tables are
|
|
479
|
-
dumped first and kept for 7 days
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
database: projects sharing a database share its server.
|
|
495
|
-
- A new project with its database on a chosen server: `xflow init --server <name>` (`platform`
|
|
496
|
-
for the common one). Without it the first migration creates the database on `platform`.
|
|
497
|
-
- An existing database: `xflow db move --to <name> --confirm <database name>`. Writes to every
|
|
498
|
-
project on it close while it copies, usually a couple of minutes; the platform switches the
|
|
499
|
-
functions itself, no deploy. It needs the right to move databases, off on the key by default:
|
|
500
|
-
refused for the right, say what should move where and why, and the person turns it on or
|
|
501
|
-
moves it in the "Data" section.
|
|
502
|
-
- Buying, paying for and deleting servers are the person's, in the web interface. There is no
|
|
503
|
-
limit on how many. If a database has outgrown its server, say so and suggest buying one.
|
|
504
|
-
|
|
505
|
-
Code stays the same: `DATABASE_URL` arrives as before, pass it as is, without an `ssl` option (an
|
|
506
|
-
own server is reached over a closed network, and `ssl` makes the connection fail). While an own
|
|
507
|
-
server is not running, migrations and queries say so and functions that use the database time
|
|
508
|
-
out. That is not your code and not yours to fix: tell the person, who sees the server state in
|
|
509
|
-
the "Data" section of the web interface, instead of retrying.
|
|
530
|
+
dumped first (up to 50 000 rows each) and kept for 7 days; getting a dump back is a request to
|
|
531
|
+
the person, no command does it. Check with `--dry-run`.
|
|
532
|
+
|
|
533
|
+
An organization can have own database servers. `xflow db list` names the server of each project
|
|
534
|
+
database (`platform` is the common one) and the own servers with the room used.
|
|
535
|
+
- A new project with its database on a chosen server: `xflow init --server <name>`.
|
|
536
|
+
- An existing one: `xflow db move --to <name> --confirm <project name>`. Writes close while it
|
|
537
|
+
copies, usually a couple of minutes; the platform switches the functions itself. It needs the
|
|
538
|
+
right to move databases, off on the key by default: refused for the right, say what should move
|
|
539
|
+
where and why, and the person turns it on or moves it in the «Базы данных» (Databases) section.
|
|
540
|
+
- Buying, paying for and deleting servers are the person's, in the web interface.
|
|
541
|
+
|
|
542
|
+
Code stays the same on an own server: pass `DATABASE_URL` as is, without an `ssl` option. While
|
|
543
|
+
an own server is not running, migrations and queries say so and functions that use the database
|
|
544
|
+
time out: tell the person instead of retrying.
|
|
510
545
|
|
|
511
546
|
An own server comes tuned and guarded, so do not tune it from migrations:
|
|
512
|
-
- Installed
|
|
513
|
-
`
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
and nowhere else. A database that used `citext` or another extension type cannot move back to
|
|
523
|
-
`platform`: the move refuses, the common server has no such extensions.
|
|
524
|
-
- A restart happens on a plan change and takes about a minute. Create the `Client` inside the
|
|
525
|
-
handler, or keep a `Pool` at module level: a `Client` created at module level dies with the
|
|
526
|
-
restart and stays dead until the function cold-starts.
|
|
547
|
+
- Installed: `pgcrypto`, `pg_trgm`, `unaccent`, `citext`, `btree_gin`, `btree_gist`,
|
|
548
|
+
`pg_stat_statements`. `gen_random_uuid()` is built in everywhere, `uuidv7()` on an own server.
|
|
549
|
+
Never `uuid-ossp`. Anything else (`postgis`, `pgvector`) is a request to the person.
|
|
550
|
+
- A statement is cut after 30 seconds, waiting for a lock after 3, a transaction left open after
|
|
551
|
+
30. A query that needs longer is a wrong query (a missing index, or a report that belongs in a
|
|
552
|
+
scheduled function writing a summary table). A scheduled job that genuinely needs more runs
|
|
553
|
+
`SET statement_timeout = '5min'` on its own connection. A database that used `citext` or
|
|
554
|
+
another extension type cannot move back to `platform`.
|
|
555
|
+
- A restart on a plan change takes about a minute. Create the `Client` inside the handler, or
|
|
556
|
+
keep a `Pool` at module level: a `Client` created at module level dies with the restart.
|
|
527
557
|
|
|
528
558
|
## File storage
|
|
529
559
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
**Static assets of the application you are building** — product photos, video, PDFs, big
|
|
533
|
-
artwork — you upload yourself, from the terminal:
|
|
560
|
+
**Static assets of an app** (product photos, video, PDFs, big artwork) you upload yourself:
|
|
534
561
|
|
|
535
562
|
```bash
|
|
536
563
|
xflow storage push ./media --to media # a folder, its structure kept
|
|
537
564
|
xflow storage ls # what is there, with addresses and sizes
|
|
538
|
-
xflow storage ls --json # the same list, machine-readable
|
|
539
565
|
```
|
|
540
566
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
567
|
+
`push` prints the address of every file (`/_files/<id>`) and skips what is already stored under
|
|
568
|
+
the same path and size; `--replace` overwrites a file whose size differs and keeps its address.
|
|
569
|
+
Use the address as is inside an app: the app's own host serves it to whoever may open the app,
|
|
570
|
+
including anonymous visitors of a public one. Icons, fonts and small artwork stay in the
|
|
571
|
+
repository.
|
|
544
572
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
fetches files back to a machine.
|
|
573
|
+
Deleting needs a right of its own, off by default, so `xflow storage rm` may answer that the key
|
|
574
|
+
was not granted it. Ask the person and do not look for a way around: whatever the users of the
|
|
575
|
+
app uploaded lives in the same folders, and there is no undo. A folder is
|
|
576
|
+
`xflow storage rm --folder <path> --yes`.
|
|
550
577
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
the same folders, and there is no undo. A folder first says how many files it holds and deletes
|
|
555
|
-
them only with `--yes`.
|
|
556
|
-
|
|
557
|
-
**Files that users of the application upload** go through a cloud function of your own. The
|
|
558
|
-
browser cannot reach storage directly: those endpoints take only the server key of the project,
|
|
559
|
-
which the platform puts into the environment of your functions as `XFLOW_SERVER_KEY` and
|
|
560
|
-
nothing else holds — not the bundle, not `.env`, not `xflow env`.
|
|
561
|
-
|
|
562
|
-
So uploading is a function of your own: it asks the platform for a one-time link, the browser
|
|
563
|
-
sends the bytes straight to storage, and a second call records the file:
|
|
578
|
+
**Files that users of the app upload** go through a cloud function of your own: it asks the
|
|
579
|
+
platform for a one-time link, the browser sends the bytes straight to storage, and a second call
|
|
580
|
+
records the file:
|
|
564
581
|
|
|
565
582
|
```js
|
|
566
583
|
const link = await fetch(`${process.env.XFLOW_API_URL}/api/storage/project/upload-url`, {
|
|
@@ -573,168 +590,109 @@ const link = await fetch(`${process.env.XFLOW_API_URL}/api/storage/project/uploa
|
|
|
573
590
|
}).then((r) => r.json())
|
|
574
591
|
```
|
|
575
592
|
|
|
576
|
-
`confirm` takes the returned `s3Key` and answers with the file and its address; `delete`
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
- **The limits are 200 MB per file and the storage quota of the organization.** The quota is
|
|
592
|
-
checked again on `confirm`, by the real size, which means a refusal can land after the
|
|
593
|
-
bytes are already up; the platform then removes the object and your table stays clean.
|
|
594
|
-
- **Confirm only after the PUT has finished.** The platform looks the object up in storage
|
|
595
|
-
and takes its real size and content type from there, not from what you declared, so an
|
|
596
|
-
early `confirm` answers that the file is not there. Send on the PUT exactly the
|
|
597
|
-
`Content-Type` you named when asking for the link: the link is signed for that header, a
|
|
598
|
-
different or missing type gets 403, and storage serves the file under it.
|
|
599
|
-
- **Storage takes application assets, not code.** Images, documents, data files, audio,
|
|
600
|
-
video, fonts and archives pass; pages, scripts and executables (`html`, `js`, `exe`, …)
|
|
601
|
-
are refused with `forbidden_type` when the link is requested. Both the extension and the
|
|
602
|
-
declared `content_type` are checked. Application code travels through `xflow deploy`, not
|
|
603
|
-
through storage.
|
|
604
|
-
|
|
605
|
-
A refusal comes back as `{ error, code }`. Branch on `code` (`invalid_name`, `file_too_large`,
|
|
606
|
-
`quota_exceeded`, `not_uploaded`, `duplicate_name`, `forbidden_type`, `not_found`, …) and never on the text:
|
|
607
|
-
the wording is free to change, the code is not.
|
|
608
|
-
|
|
609
|
-
What the app may do with files is decided inside that function, because the page can be edited
|
|
610
|
-
by whoever opened it. Never send the key to the frontend: the build gate stops on a key found in
|
|
611
|
-
the sources, and a key that reached a visitor deletes every file of the project.
|
|
593
|
+
`confirm` takes the returned `s3Key` and answers with the file and its address; `delete` takes
|
|
594
|
+
that same `url`. No endpoint lists files back, so write the address into a table of your own in
|
|
595
|
+
the same call. What a visitor may do with files is decided inside that function; never send the
|
|
596
|
+
key to the frontend.
|
|
597
|
+
|
|
598
|
+
- A name already taken in that folder is refused before the link is issued; `overwrite: true`
|
|
599
|
+
replaces the bytes and keeps the address.
|
|
600
|
+
- 200 MB per file and the storage quota of the organization, checked again on `confirm`.
|
|
601
|
+
- Confirm only after the PUT has finished, and send on the PUT exactly the `Content-Type` you
|
|
602
|
+
named: the link is signed for it, a different or missing type gets 403.
|
|
603
|
+
- Assets only: pages, scripts and executables (`html`, `js`, `exe`, …) are refused with
|
|
604
|
+
`forbidden_type`.
|
|
605
|
+
|
|
606
|
+
A refusal comes back as `{ error, code }` (`invalid_name`, `file_too_large`, `quota_exceeded`,
|
|
607
|
+
`not_uploaded`, `duplicate_name`, `forbidden_type`, `not_found`, …): branch on `code`.
|
|
612
608
|
|
|
613
609
|
## Pages
|
|
614
610
|
|
|
615
|
-
A page is a public static site at `https://<name>.xflowpage.ru
|
|
616
|
-
|
|
617
|
-
without signing in and outside the platform. A project is the opposite, it opens only inside the
|
|
618
|
-
platform: pick a page when the audience is outside the organization.
|
|
611
|
+
A page is a public static site at `https://<name>.xflowpage.ru`, outside projects. Anyone with
|
|
612
|
+
the address opens it, without signing in.
|
|
619
613
|
|
|
620
614
|
- `xflow pages create "Price list" ./dist` makes a page with the built site on it and prints its
|
|
621
615
|
address: there are no empty pages. `--slug price-list` picks the name, otherwise it comes from
|
|
622
616
|
the title with a random tail.
|
|
623
617
|
- `xflow pages deploy <page> <folder|.zip|.html> -m "what changed"` uploads a new version.
|
|
624
|
-
- **Both are public the moment the command finishes**: no draft, no publish step.
|
|
625
|
-
user wants the page out, and give them the address the CLI printed.
|
|
618
|
+
- **Both are public the moment the command finishes**: no draft, no publish step.
|
|
626
619
|
- `xflow pages status <page>` lists the versions, `xflow pages rollback <page> <N>` puts one back
|
|
627
620
|
in seconds; the files of the last 5 are kept.
|
|
628
|
-
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
-
|
|
642
|
-
- The tab icon is the platform's on every page: the upload drops any `<link rel="…icon…">` the
|
|
643
|
-
files carry and puts the XFlow one in. Do not spend a request on a favicon of your own.
|
|
621
|
+
- `xflow pages rm <page> --yes` deletes the page: the address goes empty and every version is
|
|
622
|
+
gone for good, without `--yes` the command names what goes and stops. The name stays reserved
|
|
623
|
+
for the organization. A page put up to try something out is yours to take down when you are
|
|
624
|
+
done: do not leave it standing.
|
|
625
|
+
- Names are 3-40 characters. At most 100 pages per organization, 20 new and 200 uploads a day,
|
|
626
|
+
10 seconds between uploads of one page: batch changes into one upload.
|
|
627
|
+
- Static files only, what the build writes (`dist`), never a folder with `package.json`. At most
|
|
628
|
+
5 MB zipped, 25 MB unpacked, 1000 files. Everything a page shows travels inside it: project
|
|
629
|
+
file storage does not serve pages.
|
|
630
|
+
- A backend for a page: see **Page or app**.
|
|
631
|
+
- Everything in a page is readable by every visitor. A known key format gets the upload refused,
|
|
632
|
+
anything else simply leaks.
|
|
633
|
+
- Search engines are asked not to index pages or apps: do not promise SEO.
|
|
634
|
+
- The tab icon is the platform's on every page: do not spend a request on a favicon of your own.
|
|
644
635
|
- The MCP `pages` tool uploads one self-contained HTML document up to 150 KB.
|
|
645
|
-
-
|
|
646
|
-
|
|
647
|
-
are for a person, in the platform interface.
|
|
636
|
+
- A page is deleted by whoever made it, or by the owner of the organization. Switching a page
|
|
637
|
+
off for a while is for a person, in the platform interface.
|
|
648
638
|
|
|
649
639
|
## Organizations and keys
|
|
650
640
|
|
|
651
|
-
One key per organization, stored side by side
|
|
652
|
-
|
|
653
|
-
precedence rules.
|
|
641
|
+
One key per organization, stored side by side: `xflow org` lists them, `xflow org switch
|
|
642
|
+
<name|id>` changes the active one, `xflow help org` has the precedence rules.
|
|
654
643
|
|
|
655
|
-
Inside a project folder
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
argument instead.
|
|
661
|
-
|
|
662
|
-
A "not found" on a project you know exists usually means the key belongs to another
|
|
663
|
-
organization: ids are unique across the platform, so a wrong key can never reach somebody
|
|
664
|
-
else's project. Check `xflow org`; signing in to a missing organization is `xflow login`, and
|
|
665
|
-
that needs a person with a browser.
|
|
644
|
+
Inside a project folder commands follow the organization the folder is bound to, whatever the
|
|
645
|
+
active one is. `--project <id>` and `--app <id>` ignore the folder whole (`xflow help project`
|
|
646
|
+
lists the commands that take them). A "not found" on a project you know exists usually means the
|
|
647
|
+
key belongs to another organization: check `xflow org`; signing in to a missing organization is
|
|
648
|
+
`xflow login`, and that needs a person with a browser.
|
|
666
649
|
|
|
667
650
|
## Direct access without the terminal
|
|
668
651
|
|
|
669
|
-
The platform also exposes an MCP server, connected with `xflow mcp install`. When its tools
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
when a result is truncated, which parsing terminal output does not.
|
|
652
|
+
The platform also exposes an MCP server, connected with `xflow mcp install`. When its tools are
|
|
653
|
+
available, prefer them for control-plane work: project state, database schema and read-only
|
|
654
|
+
queries, migrations, function logs and invocations, schedules, variables, app builds and
|
|
655
|
+
publish. They say explicitly when a result is truncated.
|
|
674
656
|
|
|
675
|
-
Anything that depends on the working copy stays in the CLI: sending sources, building and
|
|
676
|
-
|
|
677
|
-
tools cannot see
|
|
678
|
-
|
|
679
|
-
through tool calls also burns the user's tokens for nothing.
|
|
657
|
+
Anything that depends on the working copy stays in the CLI: sending sources, building apps and
|
|
658
|
+
deploying functions (`xflow deploy`), creating projects and apps (`xflow init`,
|
|
659
|
+
`xflow apps add`). The tools cannot see your folder. Access levels, function keys, money and
|
|
660
|
+
people are set by a person in the web interface.
|
|
680
661
|
|
|
681
662
|
## Do not
|
|
682
663
|
|
|
683
|
-
- Edit `xflow.json` by hand: the CLI writes it.
|
|
684
|
-
- Commit `.env`: it holds the project token.
|
|
685
664
|
- Deploy with `--force` without checking `xflow status` first.
|
|
686
665
|
- Invent platform commands: what is not in `xflow help` does not exist.
|
|
666
|
+
- Ask for a way to change an app's access level or issue a function key: that is the person's.
|
|
687
667
|
|
|
688
668
|
## App design
|
|
689
669
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
-
|
|
694
|
-
|
|
695
|
-
filters, list, empty state, stats, detail card, form rows, card grid, timeline, table,
|
|
696
|
-
charts. Props are typed next to each block
|
|
697
|
-
- `src/index.css` — colour tokens
|
|
698
|
-
|
|
699
|
-
Take a block before writing your own layout, and colours from tokens (`bg-card`,
|
|
700
|
-
`text-muted-foreground`, `bg-success-soft`): a custom hex palette makes the app look
|
|
701
|
-
foreign inside the platform.
|
|
702
|
-
|
|
703
|
-
Screens that read as a product, not as a form:
|
|
704
|
-
|
|
705
|
-
- Every screen starts with `Page` and `PageHeader`; header, filters and content in one column.
|
|
706
|
-
- Draw all four states: loading, error, empty, data. An empty screen says what appears here and offers an action.
|
|
670
|
+
- Take a block from `src/components/blocks` or a primitive from `src/components/ui` before
|
|
671
|
+
writing your own layout; replace or delete them when the design needs something else.
|
|
672
|
+
- Every screen starts with a clear header; header, filters and content in one column.
|
|
673
|
+
- Draw all four states: loading, error, empty, data. An empty screen says what appears here and
|
|
674
|
+
offers an action.
|
|
707
675
|
- Colour only when it communicates: status, action, error, success. Grey carries the structure.
|
|
708
676
|
- Density over air: people scan and compare. Numbers in a column, `tabular-nums`.
|
|
709
677
|
- Name the shape first: list, list with detail, dashboard, form, board.
|
|
710
678
|
- Seed 5-10 believable rows in the first migration: a screen with zero rows looks unfinished.
|
|
711
|
-
-
|
|
712
|
-
|
|
713
|
-
The app runs inside the platform in an iframe and receives the theme and the current
|
|
714
|
-
user from it. The `usePlatformAuth()` hook gives the name, role, permissions and the
|
|
715
|
-
list of organization members. Use it to draw the interface, never to guard data: the
|
|
716
|
-
value lives on the page and is edited from the console. Guard data in the function, by
|
|
717
|
-
`event.xflow`.
|
|
679
|
+
- Give the app its own page title and icon.
|
|
718
680
|
|
|
719
681
|
## Errors from a deployed app
|
|
720
682
|
|
|
721
|
-
`xflow logs` prints what broke in the browser on
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
A local run does not report anything: these logs exist for what you cannot open in your
|
|
727
|
-
own devtools.
|
|
683
|
+
`xflow logs` prints what broke in the browser on the app addresses: unhandled errors, rejected
|
|
684
|
+
promises and 5xx responses; in an app folder that app, in the project root every app.
|
|
685
|
+
`xflow functions logs` prints function crashes. A person sees the same in the web interface:
|
|
686
|
+
browser errors in the «Ошибки» (Errors) panel of the app, function crashes on the «Ошибки» tab
|
|
687
|
+
of the function's screen. The last 200 records of each stream are kept.
|
|
728
688
|
|
|
729
|
-
Run
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
and neither can anybody else on that network.
|
|
689
|
+
Run an app locally with `xflow dev` in its folder, not a bare `npm run dev`: every `/_fn/<name>`
|
|
690
|
+
call then goes to the platform as this app, with the same callers and access as on its address,
|
|
691
|
+
and a function someone else deployed answers at once. A bare `npm run dev` has no key, so its
|
|
692
|
+
function calls fail. The server answers `127.0.0.1` alone, because it holds that key. A local
|
|
693
|
+
run reports nothing to the logs.
|
|
735
694
|
|
|
736
695
|
## Environment
|
|
737
696
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
`XFLOW_TOKEN` replaces `xflow login`, `XFLOW_API_URL` points at another platform host.
|
|
697
|
+
For CI and agent runs: `XFLOW_TOKEN` replaces `xflow login`, `XFLOW_API_URL` points at another
|
|
698
|
+
platform host. There is no `.env` from the platform: the bundle carries no tokens or addresses.
|