@getxflow/cli 0.6.5 → 0.6.6
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/dist/api.js +28 -4
- package/dist/bin.js +9 -1
- package/dist/commands/connections.js +89 -2
- package/dist/commands/env.js +1 -0
- package/dist/help.js +348 -328
- package/dist/version.js +1 -1
- package/package.json +23 -23
- package/skills/xflow/SKILL.md +463 -451
package/skills/xflow/SKILL.md
CHANGED
|
@@ -1,451 +1,463 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: xflow
|
|
3
|
-
description: Build, deploy and publish apps on the XFlow platform with the xflow CLI. Use whenever the project root has xflow.json or VITE_XFLOW_* variables, and for any task that touches deployment, publishing, rollback, source sync, the project database or SQL migrations, cloud functions, schedules, environment variables and secrets, production errors and logs, or UI built on the platform design system.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# XFlow
|
|
7
|
-
|
|
8
|
-
Hosting for web apps. The code lives in an ordinary repository on the developer
|
|
9
|
-
machine, `xflow deploy` sends the sources, and the platform builds them in a clean
|
|
10
|
-
sandbox and serves the result as static files. A platform project is recognized by
|
|
11
|
-
the `xflow.json` file in its root.
|
|
12
|
-
|
|
13
|
-
## First rule
|
|
14
|
-
|
|
15
|
-
Check commands and flags against `xflow help` and `xflow help <command>`, not against
|
|
16
|
-
memory. If a command is not in the help output, it does not exist: guessing flags is
|
|
17
|
-
pointless. The CLI prints a hint with almost every error, read it in full, it usually
|
|
18
|
-
contains the fix.
|
|
19
|
-
|
|
20
|
-
When the CLI says a newer version is out, or refuses to work because the platform wants
|
|
21
|
-
a newer one, run `xflow update`. It also rewrites these instructions, which ship inside
|
|
22
|
-
the package: what you are reading may be older than the platform you are working on.
|
|
23
|
-
|
|
24
|
-
## Hard rules
|
|
25
|
-
|
|
26
|
-
These mistakes cost the most because nothing fails at the moment they are made, or
|
|
27
|
-
the error points away from the cause. The sections below carry the details.
|
|
28
|
-
|
|
29
|
-
1. Read environment variables literally: `process.env.API_KEY`. Destructuring
|
|
30
|
-
(`const { API_KEY } = process.env`) reads as no mention of the variable at all,
|
|
31
|
-
and it arrives empty.
|
|
32
|
-
2. A value written with `xflow env set` reaches the functions on the next
|
|
33
|
-
`xflow deploy`, not at the moment it is written.
|
|
34
|
-
3. Never delete a `functions/<name>/` directory unless the user asked for that
|
|
35
|
-
function to go. The next deploy removes it from the cloud together with its
|
|
36
|
-
schedules, and a function created again later gets a different address.
|
|
37
|
-
4. An already applied migration is never re-run, so editing its file changes
|
|
38
|
-
nothing. A schema change is always a new file.
|
|
39
|
-
5. When inserts start failing with `db_write_locked`, the database is over its plan
|
|
40
|
-
size. The fix is a migration that deletes data, never a rewrite of the failing SQL.
|
|
41
|
-
6. In file storage, call `confirm` only after the PUT has finished, and send the same
|
|
42
|
-
`Content-Type` in both calls. Both mistakes answer 200 and break later.
|
|
43
|
-
|
|
44
|
-
## Plan limits
|
|
45
|
-
|
|
46
|
-
The organization runs on a plan with finite limits: projects, cloud functions, developer
|
|
47
|
-
and staff seats, database and file storage, function minutes per month, plus the right to
|
|
48
|
-
use schedules. `xflow whoami` prints every one of them next to what is already used, and
|
|
49
|
-
reading it before a long task is cheaper than hitting a wall mid-way.
|
|
50
|
-
|
|
51
|
-
A limit refusal is not a bad request. The CLI prints a line starting with `Plan limit:`,
|
|
52
|
-
the API answers `code: "forbidden"` with a `limit` object (`code`, `used`, `limit`), and
|
|
53
|
-
MCP tools carry the same field. Retrying the command, renaming things or rewriting the
|
|
54
|
-
code changes nothing: tell the user what ran out and stop. Only the owner or an admin
|
|
55
|
-
lifts it, in the web interface, by freeing the resource or moving to a bigger plan.
|
|
56
|
-
|
|
57
|
-
One refusal looks like a code error but is not: when the database is over its plan size,
|
|
58
|
-
Postgres itself rejects inserts (`db_write_locked`). Reads and deletes still work, so the
|
|
59
|
-
fix is a migration that deletes data, never a rewrite of the failing SQL. Write is
|
|
60
|
-
restored within an hour of the data going back under the limit.
|
|
61
|
-
|
|
62
|
-
## Workflow
|
|
63
|
-
|
|
64
|
-
1. Change the code.
|
|
65
|
-
2. `npm run typecheck` for a two-second type check (older projects may not have the
|
|
66
|
-
script, then `npx tsc --noEmit`).
|
|
67
|
-
3. `npm run build` if the change is substantial, before deploying.
|
|
68
|
-
4. `xflow deploy` sends the sources, ships the cloud functions and builds the application
|
|
69
|
-
on the platform, printing each phase and the six-digit number of the version it built.
|
|
70
|
-
5. Give the user the project link the CLI printed and let them look. Do not open a
|
|
71
|
-
browser for them.
|
|
72
|
-
6. `xflow publish` makes that same version visible to visitors.
|
|
73
|
-
|
|
74
|
-
The split is deliberate: shipping a build and showing it are two separate decisions.
|
|
75
|
-
Until `publish` runs, visitors keep seeing the previous version. The one exception is
|
|
76
|
-
the very first version of a project: it publishes automatically, since there is no
|
|
77
|
-
live version to protect yet.
|
|
78
|
-
|
|
79
|
-
Rolling back: `xflow deployments` lists the version history, `xflow rollback <id>`
|
|
80
|
-
points the project back at an earlier build. Sources stay on their own revision.
|
|
81
|
-
|
|
82
|
-
**The only link you give a person is the project page**, `https://app.getxflow.com/projects/<id>`,
|
|
83
|
-
which the CLI prints for you. Refer to builds by their number ("version 481203 is built,
|
|
84
|
-
092399 is what visitors see"), never by address. The platform does not hand out build
|
|
85
|
-
addresses and neither should you: a build address has the version number baked into it,
|
|
86
|
-
and after the next publish it does not break, it keeps answering with the old copy. Anyone
|
|
87
|
-
holding that link then stares at a frozen app and concludes the changes never shipped. The
|
|
88
|
-
project page always shows the current state, and every version is reachable from it.
|
|
89
|
-
|
|
90
|
-
The platform builds the project itself, in a clean sandbox with one Node version for
|
|
91
|
-
everyone, and serves the result as static files. Nothing is built on your machine for
|
|
92
|
-
deployment, so a local `npm run build` is only a fast way to see errors early.
|
|
93
|
-
|
|
94
|
-
## Build gate
|
|
95
|
-
|
|
96
|
-
Before the sandbox starts, the platform checks the sources against the template. Every
|
|
97
|
-
rule below blocks the build, and the whole list of violations comes back at once, with
|
|
98
|
-
files and line numbers. Nothing is charged for a rejected attempt: the sandbox never
|
|
99
|
-
starts. Write code that already satisfies these rules instead of learning them from
|
|
100
|
-
rejections.
|
|
101
|
-
|
|
102
|
-
Build setup:
|
|
103
|
-
|
|
104
|
-
- `package.json` with the build script named in `xflow.json` (`npm run build` by default).
|
|
105
|
-
- Vite: a `vite.config.*` and `vite` in dependencies.
|
|
106
|
-
- No server frameworks: `next`, `nuxt`, `remix`, `@sveltejs/kit`, `astro`.
|
|
107
|
-
- `index.html` in the root, `src/main.tsx` as the entry point.
|
|
108
|
-
- Application code under `src/`. Root `app/`, `pages/`, `next/` are rejected.
|
|
109
|
-
|
|
110
|
-
Platform contract, checked across all of `src/`:
|
|
111
|
-
|
|
112
|
-
- Call cloud functions through `xflow.functions.invoke`, never through a hardcoded
|
|
113
|
-
`*.yandexcloud.net` URL: the address changes and the app breaks silently.
|
|
114
|
-
- No API keys or tokens in the source: they end up in the bundle. Put the call in a
|
|
115
|
-
cloud function and the key in project secrets.
|
|
116
|
-
- No server modules (`fs`, `express`, `http`, `child_process`): there is no server runtime.
|
|
117
|
-
|
|
118
|
-
Interface rules, checked outside `src/components/ui` and `src/components/blocks`:
|
|
119
|
-
|
|
120
|
-
- No `alert()`, `confirm()`, `prompt()`. Use the Dialog and Toast components.
|
|
121
|
-
- No `console.log`. Deployed apps have a public console, and forgotten debugging prints
|
|
122
|
-
user data into it. `console.error` and `console.warn` are fine, they reach the project
|
|
123
|
-
logs.
|
|
124
|
-
- No inline styles with literal values (`style={{ color: '#fff' }}`). Computed styles
|
|
125
|
-
(a drag transform, a progress width) are fine, Tailwind cannot express them.
|
|
126
|
-
- No hex colors or Tailwind palette classes (`text-gray-500`): use the theme tokens.
|
|
127
|
-
Charts are exempt, they need real colors.
|
|
128
|
-
- No importing a `@/components/ui/*` component that does not exist in the project.
|
|
129
|
-
- A library that needs a provider (`@tanstack/react-query`, `react-redux`, `sonner`,
|
|
130
|
-
`react-hot-toast`, `react-dnd`) must have it mounted in `App.tsx`. Missing providers
|
|
131
|
-
build fine and give visitors a white screen.
|
|
132
|
-
|
|
133
|
-
Template integrity. The app grows out of the platform template, and part of that template
|
|
134
|
-
is not yours to change. The reference is a snapshot of the project itself, taken when the
|
|
135
|
-
platform first looked at it, so these rules never argue with work that was already there:
|
|
136
|
-
|
|
137
|
-
- Platform files must stay byte for byte as they arrived: `src/lib/theme-sync.ts`,
|
|
138
|
-
`src/lib/platform-auth.ts`, `src/contexts/platform-auth-context.tsx`,
|
|
139
|
-
`src/hooks/use-platform-auth.ts`, `src/utils/error-logger.ts`, `src/lib/xflow.ts`.
|
|
140
|
-
They wire the app to the platform, and every way they break is a silent one. Build what
|
|
141
|
-
you need around them, never inside them.
|
|
142
|
-
- The entry point keeps calling `initThemeSync()`, `initPlatformAuth()` and
|
|
143
|
-
`initErrorLogger()`, keeps importing `index.css` and keeps mounting `ThemeProvider`.
|
|
144
|
-
How the file is written is up to you.
|
|
145
|
-
- `index.html` keeps the element with `id="root"` and the script that loads `src/main`.
|
|
146
|
-
- Theme token names in `src/index.css` stay declared, in `:root` and in `.dark` alike, and
|
|
147
|
-
the Tailwind config keeps mapping them. Change the values as much as the design needs:
|
|
148
|
-
it is the names that components paint with.
|
|
149
|
-
- The Tailwind `content` globs keep covering `src/**`. Narrow them and Tailwind strips
|
|
150
|
-
every class the app uses.
|
|
151
|
-
- Files under `src/components/ui` and `src/components/blocks` may be edited freely but
|
|
152
|
-
not deleted.
|
|
153
|
-
|
|
154
|
-
A rejection names the file and the revision to take the original from:
|
|
155
|
-
`xflow pull --revision N --into ./original`, then copy the file back.
|
|
156
|
-
|
|
157
|
-
## Cloud functions
|
|
158
|
-
|
|
159
|
-
Server-side code lives in `functions/<name>/index.ts` and exports `handler`. There is no
|
|
160
|
-
separate deploy command: `xflow deploy` ships the functions and then builds the application,
|
|
161
|
-
in that order. List what is live with `xflow functions list`. The handler returns
|
|
162
|
-
`{ statusCode, body }` where `body` is a JSON string.
|
|
163
|
-
|
|
164
|
-
Keep one shape inside that string across the whole project: `{ success: true, data }`
|
|
165
|
-
when it worked, `{ success: false, error: { message, code } }` when it did not. Nothing
|
|
166
|
-
enforces this, but a project where every function answers its own way costs an adapter
|
|
167
|
-
on every call. Branch the frontend on `error.code`, never on `error.message`: wording
|
|
168
|
-
gets rewritten on any edit, a code does not.
|
|
169
|
-
|
|
170
|
-
The sources are the whole truth about which functions exist. Delete the directory and the
|
|
171
|
-
next deploy would delete the function from the cloud, schedules included, and that cannot be
|
|
172
|
-
undone: a function created again later gets a different address. So never remove a function
|
|
173
|
-
directory to "clean up" unless the user asked for the function to go.
|
|
174
|
-
|
|
175
|
-
Such a deploy does not start on its own: the platform names the functions it would remove and
|
|
176
|
-
refuses until somebody agrees. Under an agent there is no terminal to ask in, so the refusal
|
|
177
|
-
reaches you, and `--allow-removals` is the only way past it. Adding that flag to get the
|
|
178
|
-
build running is exactly the wrong move: it means you deleted something the user did not ask
|
|
179
|
-
you to delete. Put the directories back instead, and if the removal really is intended, say
|
|
180
|
-
which functions are about to go and let the user answer.
|
|
181
|
-
|
|
182
|
-
Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it
|
|
183
|
-
the way the app does and prints status, timing and body (`--data '{"a":1}'` sends a body),
|
|
184
|
-
and `xflow functions logs <name>` shows the failures, each with its stack and the console
|
|
185
|
-
output of that call. Only failed calls are logged, so an empty output means the function
|
|
186
|
-
never crashed, not that logging is broken.
|
|
187
|
-
|
|
188
|
-
From the app, call a function through `src/lib/xflow.ts`:
|
|
189
|
-
`await xflow.functions.invoke('send-mail', { body: { to } })`. It carries the credentials
|
|
190
|
-
for you. Addresses are baked into the build, which is why the functions go out first:
|
|
191
|
-
by the time the bundle is built they already exist, and a new function is never missing
|
|
192
|
-
from the application that calls it.
|
|
193
|
-
|
|
194
|
-
### Who is calling
|
|
195
|
-
|
|
196
|
-
A function answers only to a member of the organization who has access to that project.
|
|
197
|
-
The platform issues a short-lived pass when it opens the application, the wrapper checks it
|
|
198
|
-
with the platform on every call, and the handler receives the answer in `event.xflow`:
|
|
199
|
-
|
|
200
|
-
```js
|
|
201
|
-
exports.handler = async (event) => {
|
|
202
|
-
const { caller, user } = event.xflow
|
|
203
|
-
// caller: 'visitor' (a person), 'service' (another function of this project),
|
|
204
|
-
// 'external' (an outside service with a key), 'schedule' (a timer run)
|
|
205
|
-
// user: { id, role } for a visitor, null for everything else
|
|
206
|
-
}
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Never trust an identity that arrives in the body or in a header of the request: those are
|
|
210
|
-
written by the page, which lives on someone else's computer. `event.xflow` is the only
|
|
211
|
-
identity the platform stands behind, and `usePlatformAuth()` in the frontend is a hint for
|
|
212
|
-
the interface, not a check.
|
|
213
|
-
|
|
214
|
-
A function that changes data should say so instead of checking the role by hand:
|
|
215
|
-
|
|
216
|
-
```js
|
|
217
|
-
exports.minRole = 'admin' // 'member' | 'developer' | 'admin' | 'owner'
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
The wrapper refuses anything below that role before your code runs. Without the line every
|
|
221
|
-
member of the project can call the function, including the ones who may only look at apps.
|
|
222
|
-
|
|
223
|
-
Losing access closes the function within five minutes, so a removed member cannot keep calling it.
|
|
224
|
-
Opening the deployed address directly does not work either: there is no pass outside the
|
|
225
|
-
platform.
|
|
226
|
-
|
|
227
|
-
Calling a function from another function is a server call. Send two headers, both from the
|
|
228
|
-
environment the platform fills in: `X-Project-Token` with `process.env.XFLOW_PROJECT_TOKEN`
|
|
229
|
-
and `X-Server-Key` with `process.env.XFLOW_SERVER_KEY`. The token is the ticket into the
|
|
230
|
-
project and the key is the identity; the wrapper checks the ticket first, so the key alone
|
|
231
|
-
answers 401.
|
|
232
|
-
|
|
233
|
-
An outside service (a webhook from a payment provider, a bot, a CRM) has no person behind it
|
|
234
|
-
and needs a key of that one function. Keys are not issued by default and the CLI cannot
|
|
235
|
-
create one: a human issues it in the project settings: «Облачные функции» → the function →
|
|
236
|
-
«Настройки». Ask the user to do that and to paste the address back to you — never invent
|
|
237
|
-
another way in. A function holds at most two keys, and the second one exists to replace the
|
|
238
|
-
first without downtime, not to serve a second consumer.
|
|
239
|
-
|
|
240
|
-
`xflow functions list` shows who can reach each function: `in-app only` (no keys, answers
|
|
241
|
-
only inside the application) or `external (N keys)` (a human issued external access). Key
|
|
242
|
-
values are never shown there.
|
|
243
|
-
|
|
244
|
-
Keys and passwords live on the platform, not in the repository: `xflow env set SMTP_PASSWORD=…`
|
|
245
|
-
writes one, `xflow env` lists the names, `xflow env check` tells you which variables your
|
|
246
|
-
functions read but the platform does not have. Values never come back out — the only place
|
|
247
|
-
they exist is inside the running function.
|
|
248
|
-
|
|
249
|
-
A function receives only the variables it mentions by name via `process.env.NAME`, so never
|
|
250
|
-
assemble a variable name from an expression and never destructure the environment
|
|
251
|
-
(`const { API_KEY } = process.env` reads as no mention at all, and the variable arrives
|
|
252
|
-
empty). New values arrive on the next `xflow deploy`, not at the moment they are written.
|
|
253
|
-
|
|
254
|
-
Some variables come from a connected account instead of from you. When someone connects an
|
|
255
|
-
advertising cabinet or another external service in the platform settings and links it to the
|
|
256
|
-
project, its credentials show up in `xflow env` marked read-only, under a prefix chosen at
|
|
257
|
-
link time: `YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_CLIENT_LOGIN`. Read them like any other
|
|
258
|
-
variable. Do not try to `xflow env set` those names: the platform keeps the values in sync
|
|
259
|
-
and refuses. If a call to that service starts failing with an authorisation error, run
|
|
260
|
-
`xflow status`: it says whether the token is merely expiring (any build renews it) or the
|
|
261
|
-
account was disconnected on the provider's side, which only a human can fix by reconnecting
|
|
262
|
-
it in the platform settings.
|
|
263
|
-
|
|
264
|
-
`xflow connections` lists those accounts: the ones already linked to this project, with the
|
|
265
|
-
alias and the state of the access, and the ones the organization has but this project does
|
|
266
|
-
not use yet, marked `available, not linked`. Check it before telling anyone a service is
|
|
267
|
-
unavailable: the account you need is often connected already, one link away.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
xflow
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
the
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
the
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
- `
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
`
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
`
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
1
|
+
---
|
|
2
|
+
name: xflow
|
|
3
|
+
description: Build, deploy and publish apps on the XFlow platform with the xflow CLI. Use whenever the project root has xflow.json or VITE_XFLOW_* variables, and for any task that touches deployment, publishing, rollback, source sync, the project database or SQL migrations, cloud functions, schedules, environment variables and secrets, production errors and logs, or UI built on the platform design system.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# XFlow
|
|
7
|
+
|
|
8
|
+
Hosting for web apps. The code lives in an ordinary repository on the developer
|
|
9
|
+
machine, `xflow deploy` sends the sources, and the platform builds them in a clean
|
|
10
|
+
sandbox and serves the result as static files. A platform project is recognized by
|
|
11
|
+
the `xflow.json` file in its root.
|
|
12
|
+
|
|
13
|
+
## First rule
|
|
14
|
+
|
|
15
|
+
Check commands and flags against `xflow help` and `xflow help <command>`, not against
|
|
16
|
+
memory. If a command is not in the help output, it does not exist: guessing flags is
|
|
17
|
+
pointless. The CLI prints a hint with almost every error, read it in full, it usually
|
|
18
|
+
contains the fix.
|
|
19
|
+
|
|
20
|
+
When the CLI says a newer version is out, or refuses to work because the platform wants
|
|
21
|
+
a newer one, run `xflow update`. It also rewrites these instructions, which ship inside
|
|
22
|
+
the package: what you are reading may be older than the platform you are working on.
|
|
23
|
+
|
|
24
|
+
## Hard rules
|
|
25
|
+
|
|
26
|
+
These mistakes cost the most because nothing fails at the moment they are made, or
|
|
27
|
+
the error points away from the cause. The sections below carry the details.
|
|
28
|
+
|
|
29
|
+
1. Read environment variables literally: `process.env.API_KEY`. Destructuring
|
|
30
|
+
(`const { API_KEY } = process.env`) reads as no mention of the variable at all,
|
|
31
|
+
and it arrives empty.
|
|
32
|
+
2. A value written with `xflow env set` reaches the functions on the next
|
|
33
|
+
`xflow deploy`, not at the moment it is written.
|
|
34
|
+
3. Never delete a `functions/<name>/` directory unless the user asked for that
|
|
35
|
+
function to go. The next deploy removes it from the cloud together with its
|
|
36
|
+
schedules, and a function created again later gets a different address.
|
|
37
|
+
4. An already applied migration is never re-run, so editing its file changes
|
|
38
|
+
nothing. A schema change is always a new file.
|
|
39
|
+
5. When inserts start failing with `db_write_locked`, the database is over its plan
|
|
40
|
+
size. The fix is a migration that deletes data, never a rewrite of the failing SQL.
|
|
41
|
+
6. In file storage, call `confirm` only after the PUT has finished, and send the same
|
|
42
|
+
`Content-Type` in both calls. Both mistakes answer 200 and break later.
|
|
43
|
+
|
|
44
|
+
## Plan limits
|
|
45
|
+
|
|
46
|
+
The organization runs on a plan with finite limits: projects, cloud functions, developer
|
|
47
|
+
and staff seats, database and file storage, function minutes per month, plus the right to
|
|
48
|
+
use schedules. `xflow whoami` prints every one of them next to what is already used, and
|
|
49
|
+
reading it before a long task is cheaper than hitting a wall mid-way.
|
|
50
|
+
|
|
51
|
+
A limit refusal is not a bad request. The CLI prints a line starting with `Plan limit:`,
|
|
52
|
+
the API answers `code: "forbidden"` with a `limit` object (`code`, `used`, `limit`), and
|
|
53
|
+
MCP tools carry the same field. Retrying the command, renaming things or rewriting the
|
|
54
|
+
code changes nothing: tell the user what ran out and stop. Only the owner or an admin
|
|
55
|
+
lifts it, in the web interface, by freeing the resource or moving to a bigger plan.
|
|
56
|
+
|
|
57
|
+
One refusal looks like a code error but is not: when the database is over its plan size,
|
|
58
|
+
Postgres itself rejects inserts (`db_write_locked`). Reads and deletes still work, so the
|
|
59
|
+
fix is a migration that deletes data, never a rewrite of the failing SQL. Write is
|
|
60
|
+
restored within an hour of the data going back under the limit.
|
|
61
|
+
|
|
62
|
+
## Workflow
|
|
63
|
+
|
|
64
|
+
1. Change the code.
|
|
65
|
+
2. `npm run typecheck` for a two-second type check (older projects may not have the
|
|
66
|
+
script, then `npx tsc --noEmit`).
|
|
67
|
+
3. `npm run build` if the change is substantial, before deploying.
|
|
68
|
+
4. `xflow deploy` sends the sources, ships the cloud functions and builds the application
|
|
69
|
+
on the platform, printing each phase and the six-digit number of the version it built.
|
|
70
|
+
5. Give the user the project link the CLI printed and let them look. Do not open a
|
|
71
|
+
browser for them.
|
|
72
|
+
6. `xflow publish` makes that same version visible to visitors.
|
|
73
|
+
|
|
74
|
+
The split is deliberate: shipping a build and showing it are two separate decisions.
|
|
75
|
+
Until `publish` runs, visitors keep seeing the previous version. The one exception is
|
|
76
|
+
the very first version of a project: it publishes automatically, since there is no
|
|
77
|
+
live version to protect yet.
|
|
78
|
+
|
|
79
|
+
Rolling back: `xflow deployments` lists the version history, `xflow rollback <id>`
|
|
80
|
+
points the project back at an earlier build. Sources stay on their own revision.
|
|
81
|
+
|
|
82
|
+
**The only link you give a person is the project page**, `https://app.getxflow.com/projects/<id>`,
|
|
83
|
+
which the CLI prints for you. Refer to builds by their number ("version 481203 is built,
|
|
84
|
+
092399 is what visitors see"), never by address. The platform does not hand out build
|
|
85
|
+
addresses and neither should you: a build address has the version number baked into it,
|
|
86
|
+
and after the next publish it does not break, it keeps answering with the old copy. Anyone
|
|
87
|
+
holding that link then stares at a frozen app and concludes the changes never shipped. The
|
|
88
|
+
project page always shows the current state, and every version is reachable from it.
|
|
89
|
+
|
|
90
|
+
The platform builds the project itself, in a clean sandbox with one Node version for
|
|
91
|
+
everyone, and serves the result as static files. Nothing is built on your machine for
|
|
92
|
+
deployment, so a local `npm run build` is only a fast way to see errors early.
|
|
93
|
+
|
|
94
|
+
## Build gate
|
|
95
|
+
|
|
96
|
+
Before the sandbox starts, the platform checks the sources against the template. Every
|
|
97
|
+
rule below blocks the build, and the whole list of violations comes back at once, with
|
|
98
|
+
files and line numbers. Nothing is charged for a rejected attempt: the sandbox never
|
|
99
|
+
starts. Write code that already satisfies these rules instead of learning them from
|
|
100
|
+
rejections.
|
|
101
|
+
|
|
102
|
+
Build setup:
|
|
103
|
+
|
|
104
|
+
- `package.json` with the build script named in `xflow.json` (`npm run build` by default).
|
|
105
|
+
- Vite: a `vite.config.*` and `vite` in dependencies.
|
|
106
|
+
- No server frameworks: `next`, `nuxt`, `remix`, `@sveltejs/kit`, `astro`.
|
|
107
|
+
- `index.html` in the root, `src/main.tsx` as the entry point.
|
|
108
|
+
- Application code under `src/`. Root `app/`, `pages/`, `next/` are rejected.
|
|
109
|
+
|
|
110
|
+
Platform contract, checked across all of `src/`:
|
|
111
|
+
|
|
112
|
+
- Call cloud functions through `xflow.functions.invoke`, never through a hardcoded
|
|
113
|
+
`*.yandexcloud.net` URL: the address changes and the app breaks silently.
|
|
114
|
+
- No API keys or tokens in the source: they end up in the bundle. Put the call in a
|
|
115
|
+
cloud function and the key in project secrets.
|
|
116
|
+
- No server modules (`fs`, `express`, `http`, `child_process`): there is no server runtime.
|
|
117
|
+
|
|
118
|
+
Interface rules, checked outside `src/components/ui` and `src/components/blocks`:
|
|
119
|
+
|
|
120
|
+
- No `alert()`, `confirm()`, `prompt()`. Use the Dialog and Toast components.
|
|
121
|
+
- No `console.log`. Deployed apps have a public console, and forgotten debugging prints
|
|
122
|
+
user data into it. `console.error` and `console.warn` are fine, they reach the project
|
|
123
|
+
logs.
|
|
124
|
+
- No inline styles with literal values (`style={{ color: '#fff' }}`). Computed styles
|
|
125
|
+
(a drag transform, a progress width) are fine, Tailwind cannot express them.
|
|
126
|
+
- No hex colors or Tailwind palette classes (`text-gray-500`): use the theme tokens.
|
|
127
|
+
Charts are exempt, they need real colors.
|
|
128
|
+
- No importing a `@/components/ui/*` component that does not exist in the project.
|
|
129
|
+
- A library that needs a provider (`@tanstack/react-query`, `react-redux`, `sonner`,
|
|
130
|
+
`react-hot-toast`, `react-dnd`) must have it mounted in `App.tsx`. Missing providers
|
|
131
|
+
build fine and give visitors a white screen.
|
|
132
|
+
|
|
133
|
+
Template integrity. The app grows out of the platform template, and part of that template
|
|
134
|
+
is not yours to change. The reference is a snapshot of the project itself, taken when the
|
|
135
|
+
platform first looked at it, so these rules never argue with work that was already there:
|
|
136
|
+
|
|
137
|
+
- Platform files must stay byte for byte as they arrived: `src/lib/theme-sync.ts`,
|
|
138
|
+
`src/lib/platform-auth.ts`, `src/contexts/platform-auth-context.tsx`,
|
|
139
|
+
`src/hooks/use-platform-auth.ts`, `src/utils/error-logger.ts`, `src/lib/xflow.ts`.
|
|
140
|
+
They wire the app to the platform, and every way they break is a silent one. Build what
|
|
141
|
+
you need around them, never inside them.
|
|
142
|
+
- The entry point keeps calling `initThemeSync()`, `initPlatformAuth()` and
|
|
143
|
+
`initErrorLogger()`, keeps importing `index.css` and keeps mounting `ThemeProvider`.
|
|
144
|
+
How the file is written is up to you.
|
|
145
|
+
- `index.html` keeps the element with `id="root"` and the script that loads `src/main`.
|
|
146
|
+
- Theme token names in `src/index.css` stay declared, in `:root` and in `.dark` alike, and
|
|
147
|
+
the Tailwind config keeps mapping them. Change the values as much as the design needs:
|
|
148
|
+
it is the names that components paint with.
|
|
149
|
+
- The Tailwind `content` globs keep covering `src/**`. Narrow them and Tailwind strips
|
|
150
|
+
every class the app uses.
|
|
151
|
+
- Files under `src/components/ui` and `src/components/blocks` may be edited freely but
|
|
152
|
+
not deleted.
|
|
153
|
+
|
|
154
|
+
A rejection names the file and the revision to take the original from:
|
|
155
|
+
`xflow pull --revision N --into ./original`, then copy the file back.
|
|
156
|
+
|
|
157
|
+
## Cloud functions
|
|
158
|
+
|
|
159
|
+
Server-side code lives in `functions/<name>/index.ts` and exports `handler`. There is no
|
|
160
|
+
separate deploy command: `xflow deploy` ships the functions and then builds the application,
|
|
161
|
+
in that order. List what is live with `xflow functions list`. The handler returns
|
|
162
|
+
`{ statusCode, body }` where `body` is a JSON string.
|
|
163
|
+
|
|
164
|
+
Keep one shape inside that string across the whole project: `{ success: true, data }`
|
|
165
|
+
when it worked, `{ success: false, error: { message, code } }` when it did not. Nothing
|
|
166
|
+
enforces this, but a project where every function answers its own way costs an adapter
|
|
167
|
+
on every call. Branch the frontend on `error.code`, never on `error.message`: wording
|
|
168
|
+
gets rewritten on any edit, a code does not.
|
|
169
|
+
|
|
170
|
+
The sources are the whole truth about which functions exist. Delete the directory and the
|
|
171
|
+
next deploy would delete the function from the cloud, schedules included, and that cannot be
|
|
172
|
+
undone: a function created again later gets a different address. So never remove a function
|
|
173
|
+
directory to "clean up" unless the user asked for the function to go.
|
|
174
|
+
|
|
175
|
+
Such a deploy does not start on its own: the platform names the functions it would remove and
|
|
176
|
+
refuses until somebody agrees. Under an agent there is no terminal to ask in, so the refusal
|
|
177
|
+
reaches you, and `--allow-removals` is the only way past it. Adding that flag to get the
|
|
178
|
+
build running is exactly the wrong move: it means you deleted something the user did not ask
|
|
179
|
+
you to delete. Put the directories back instead, and if the removal really is intended, say
|
|
180
|
+
which functions are about to go and let the user answer.
|
|
181
|
+
|
|
182
|
+
Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it
|
|
183
|
+
the way the app does and prints status, timing and body (`--data '{"a":1}'` sends a body),
|
|
184
|
+
and `xflow functions logs <name>` shows the failures, each with its stack and the console
|
|
185
|
+
output of that call. Only failed calls are logged, so an empty output means the function
|
|
186
|
+
never crashed, not that logging is broken.
|
|
187
|
+
|
|
188
|
+
From the app, call a function through `src/lib/xflow.ts`:
|
|
189
|
+
`await xflow.functions.invoke('send-mail', { body: { to } })`. It carries the credentials
|
|
190
|
+
for you. Addresses are baked into the build, which is why the functions go out first:
|
|
191
|
+
by the time the bundle is built they already exist, and a new function is never missing
|
|
192
|
+
from the application that calls it.
|
|
193
|
+
|
|
194
|
+
### Who is calling
|
|
195
|
+
|
|
196
|
+
A function answers only to a member of the organization who has access to that project.
|
|
197
|
+
The platform issues a short-lived pass when it opens the application, the wrapper checks it
|
|
198
|
+
with the platform on every call, and the handler receives the answer in `event.xflow`:
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
exports.handler = async (event) => {
|
|
202
|
+
const { caller, user } = event.xflow
|
|
203
|
+
// caller: 'visitor' (a person), 'service' (another function of this project),
|
|
204
|
+
// 'external' (an outside service with a key), 'schedule' (a timer run)
|
|
205
|
+
// user: { id, role } for a visitor, null for everything else
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Never trust an identity that arrives in the body or in a header of the request: those are
|
|
210
|
+
written by the page, which lives on someone else's computer. `event.xflow` is the only
|
|
211
|
+
identity the platform stands behind, and `usePlatformAuth()` in the frontend is a hint for
|
|
212
|
+
the interface, not a check.
|
|
213
|
+
|
|
214
|
+
A function that changes data should say so instead of checking the role by hand:
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
exports.minRole = 'admin' // 'member' | 'developer' | 'admin' | 'owner'
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The wrapper refuses anything below that role before your code runs. Without the line every
|
|
221
|
+
member of the project can call the function, including the ones who may only look at apps.
|
|
222
|
+
|
|
223
|
+
Losing access closes the function within five minutes, so a removed member cannot keep calling it.
|
|
224
|
+
Opening the deployed address directly does not work either: there is no pass outside the
|
|
225
|
+
platform.
|
|
226
|
+
|
|
227
|
+
Calling a function from another function is a server call. Send two headers, both from the
|
|
228
|
+
environment the platform fills in: `X-Project-Token` with `process.env.XFLOW_PROJECT_TOKEN`
|
|
229
|
+
and `X-Server-Key` with `process.env.XFLOW_SERVER_KEY`. The token is the ticket into the
|
|
230
|
+
project and the key is the identity; the wrapper checks the ticket first, so the key alone
|
|
231
|
+
answers 401.
|
|
232
|
+
|
|
233
|
+
An outside service (a webhook from a payment provider, a bot, a CRM) has no person behind it
|
|
234
|
+
and needs a key of that one function. Keys are not issued by default and the CLI cannot
|
|
235
|
+
create one: a human issues it in the project settings: «Облачные функции» → the function →
|
|
236
|
+
«Настройки». Ask the user to do that and to paste the address back to you — never invent
|
|
237
|
+
another way in. A function holds at most two keys, and the second one exists to replace the
|
|
238
|
+
first without downtime, not to serve a second consumer.
|
|
239
|
+
|
|
240
|
+
`xflow functions list` shows who can reach each function: `in-app only` (no keys, answers
|
|
241
|
+
only inside the application) or `external (N keys)` (a human issued external access). Key
|
|
242
|
+
values are never shown there.
|
|
243
|
+
|
|
244
|
+
Keys and passwords live on the platform, not in the repository: `xflow env set SMTP_PASSWORD=…`
|
|
245
|
+
writes one, `xflow env` lists the names, `xflow env check` tells you which variables your
|
|
246
|
+
functions read but the platform does not have. Values never come back out — the only place
|
|
247
|
+
they exist is inside the running function.
|
|
248
|
+
|
|
249
|
+
A function receives only the variables it mentions by name via `process.env.NAME`, so never
|
|
250
|
+
assemble a variable name from an expression and never destructure the environment
|
|
251
|
+
(`const { API_KEY } = process.env` reads as no mention at all, and the variable arrives
|
|
252
|
+
empty). New values arrive on the next `xflow deploy`, not at the moment they are written.
|
|
253
|
+
|
|
254
|
+
Some variables come from a connected account instead of from you. When someone connects an
|
|
255
|
+
advertising cabinet or another external service in the platform settings and links it to the
|
|
256
|
+
project, its credentials show up in `xflow env` marked read-only, under a prefix chosen at
|
|
257
|
+
link time: `YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_CLIENT_LOGIN`. Read them like any other
|
|
258
|
+
variable. Do not try to `xflow env set` those names: the platform keeps the values in sync
|
|
259
|
+
and refuses. If a call to that service starts failing with an authorisation error, run
|
|
260
|
+
`xflow status`: it says whether the token is merely expiring (any build renews it) or the
|
|
261
|
+
account was disconnected on the provider's side, which only a human can fix by reconnecting
|
|
262
|
+
it in the platform settings.
|
|
263
|
+
|
|
264
|
+
`xflow connections` lists those accounts: the ones already linked to this project, with the
|
|
265
|
+
alias and the state of the access, and the ones the organization has but this project does
|
|
266
|
+
not use yet, marked `available, not linked`. Check it before telling anyone a service is
|
|
267
|
+
unavailable: the account you need is often connected already, one link away.
|
|
268
|
+
|
|
269
|
+
`xflow connections link "Яндекс Метрика" --as YANDEX_METRIKA` is that link, and
|
|
270
|
+
`xflow connections unlink YANDEX_METRIKA` undoes it. Name the connection the way the list
|
|
271
|
+
does, in its first column, or by its identifier; unlink also takes the alias, which your
|
|
272
|
+
own code already knows. If two accounts of the same service share a name, the command
|
|
273
|
+
prints their identifiers instead of guessing. Unlink refuses while a function still reads
|
|
274
|
+
one of the variables and names those functions, so read that list before reaching for
|
|
275
|
+
`--force`.
|
|
276
|
+
|
|
277
|
+
Linking needs the `connections:link` right, which keys are issued with. If it was taken
|
|
278
|
+
away, say so and ask the person to turn it back on in the platform settings under
|
|
279
|
+
Developers: a key cannot grant it to itself. Only accounts granted to the owner of the key
|
|
280
|
+
personally can be linked at all. Connecting a new account and switching one off stay with a
|
|
281
|
+
person too.
|
|
282
|
+
|
|
283
|
+
To run a function on a timer: `xflow schedules set report "0 3 ? * * *"` (daily at 03:00).
|
|
284
|
+
Six fields, UTC, and exactly one of day-of-month / day-of-week must be `?` — that is
|
|
285
|
+
how Yandex wants it. A scheduled run reaches the handler as a POST with no headers.
|
|
286
|
+
|
|
287
|
+
The pieces line up in one pass. From a new function to a verified schedule:
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
xflow env set SMTP_PASSWORD=... # secrets first: values ride the next deploy
|
|
291
|
+
# write functions/report/index.ts, reading process.env.SMTP_PASSWORD literally
|
|
292
|
+
xflow deploy # ships the function, then builds the app
|
|
293
|
+
xflow schedules set report "0 3 ? * * *" # after the deploy: a schedule needs a deployed function
|
|
294
|
+
xflow functions invoke report # run it once, the way the app would
|
|
295
|
+
xflow functions logs report # empty output means it never crashed
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Database
|
|
299
|
+
|
|
300
|
+
Schema changes are files: `migrations/0001_init.sql`, `migrations/0002_orders.sql`, applied
|
|
301
|
+
in filename order by `xflow db migrate`. `xflow db status` shows what is applied and what
|
|
302
|
+
waits. History lives in the database itself, so an already applied file is never re-run and
|
|
303
|
+
editing it changes nothing: write a new migration instead.
|
|
304
|
+
|
|
305
|
+
The browser never reaches the database directly. The app reads and writes through a cloud
|
|
306
|
+
function, and inside the handler the connection string is already there:
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
const { Client } = require('pg')
|
|
310
|
+
const db = new Client({ connectionString: process.env.DATABASE_URL })
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
The platform passes `DATABASE_URL` only to functions that mention it, and sets the project
|
|
314
|
+
schema on every connection, so plain table names (`select * from tasks`) hit your project.
|
|
315
|
+
You never write that variable yourself: `xflow env set DATABASE_URL=...` is refused. The same
|
|
316
|
+
goes for every name starting with `XFLOW`: the platform fills those in itself, and your value
|
|
317
|
+
under one of them would shadow the real one.
|
|
318
|
+
|
|
319
|
+
The platform keeps no database history and no backups. Anything that destroys data
|
|
320
|
+
(`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE FROM` without a condition) is refused
|
|
321
|
+
unless two things hold at once: you pass `--allow-destructive`, and the access key carries
|
|
322
|
+
the right to destroy data. That right is off by default and only its owner can turn it on,
|
|
323
|
+
in the platform settings, under Developers. So when a destructive migration is refused for
|
|
324
|
+
the right rather than the flag, adding the flag changes nothing: say what needs deleting and
|
|
325
|
+
why, and let the person decide. With both in place the affected tables are dumped first and
|
|
326
|
+
kept for 7 days. Check with `--dry-run` before applying.
|
|
327
|
+
|
|
328
|
+
One logical database can be shared by several projects, so your migration can break an app
|
|
329
|
+
you do not see. `xflow db status` lists applied migrations that have no file in your
|
|
330
|
+
repository: that is what someone else's project did.
|
|
331
|
+
|
|
332
|
+
For the same reason the `migrations/` directory is not the schema. It says what you did;
|
|
333
|
+
`xflow db schema` says what is in the database right now, and `xflow db schema <table>` gives
|
|
334
|
+
the columns of one table. `xflow db query "select ..."` reads data, inside a READ ONLY
|
|
335
|
+
transaction, so a write there fails by design rather than by accident. Look before you write
|
|
336
|
+
a migration against a shared database.
|
|
337
|
+
|
|
338
|
+
## File storage
|
|
339
|
+
|
|
340
|
+
The project has file storage, and the browser cannot reach it. Those endpoints take only the
|
|
341
|
+
server key of the project, and the platform puts it into the environment of your cloud
|
|
342
|
+
functions as `XFLOW_SERVER_KEY`. Nothing else holds it: not the bundle, not `.env`, not
|
|
343
|
+
`xflow env`.
|
|
344
|
+
|
|
345
|
+
So uploading is a function of your own. It asks the platform for a one-time link, the browser
|
|
346
|
+
then sends the bytes straight to storage, and a second call records the file:
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
const link = await fetch(`${process.env.XFLOW_API_URL}/api/storage/project/upload-url`, {
|
|
350
|
+
method: 'POST',
|
|
351
|
+
headers: {
|
|
352
|
+
'Content-Type': 'application/json',
|
|
353
|
+
'X-Server-Key': process.env.XFLOW_SERVER_KEY,
|
|
354
|
+
},
|
|
355
|
+
body: JSON.stringify({ fileName, fileSize, contentType, folderPath: 'invoices' }),
|
|
356
|
+
}).then((r) => r.json())
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
`confirm` takes the same fields plus the returned `s3Key` and answers with the file and its
|
|
360
|
+
public address; `delete` takes the file `url`. Never pipe the bytes through the function itself.
|
|
361
|
+
|
|
362
|
+
There is no endpoint that lists the files back, so the address that `confirm` returns is the
|
|
363
|
+
only copy you get: write it into a table of your own in the same call, and the application
|
|
364
|
+
reads its files from there.
|
|
365
|
+
|
|
366
|
+
Four things bite an upload that otherwise looks right, and none of them is obvious from the
|
|
367
|
+
answers you get:
|
|
368
|
+
|
|
369
|
+
- **The content type can quietly split in two.** Nothing checks that the `Content-Type` the
|
|
370
|
+
browser sends on the PUT matches the one you named in `upload-url`: both calls answer 200.
|
|
371
|
+
But storage keeps the header the browser sent and serves the file under it, while the
|
|
372
|
+
record keeps the one you named, so a card can say `image/png` about a file every browser
|
|
373
|
+
treats as HTML. Send the same string in both calls and they cannot drift.
|
|
374
|
+
- **The same name in the same folder is refused.** Names are unique per folder, so a second
|
|
375
|
+
`avatar.png` fails instead of replacing the first. Give the name a suffix of your own, or
|
|
376
|
+
delete the old file before confirming the new one.
|
|
377
|
+
- **The limits are 200 MB per file and the storage quota of the organization.** The quota is
|
|
378
|
+
checked again on `confirm`, by the real size, which means a refusal can land after the
|
|
379
|
+
bytes are already up; the platform then removes the object and your table stays clean.
|
|
380
|
+
- **Confirm only after the PUT has finished.** The platform looks the object up in storage
|
|
381
|
+
and takes its real size from there, not from what you declared, so an early `confirm`
|
|
382
|
+
answers that the file is not there.
|
|
383
|
+
|
|
384
|
+
A refusal comes back as `{ error, code }`. Branch on `code` (`invalid_name`, `file_too_large`,
|
|
385
|
+
`quota_exceeded`, `not_uploaded`, `duplicate_name`, `not_found`, …) and never on the text:
|
|
386
|
+
the wording is free to change, the code is not.
|
|
387
|
+
|
|
388
|
+
What the app may do with files is decided inside that function, because the page in the
|
|
389
|
+
browser can be edited by whoever opened it. Never write the key into the sources and never
|
|
390
|
+
send it to the frontend: the build gate stops on a key found in the application code, and a
|
|
391
|
+
key that reached a visitor lets them delete every file of the project.
|
|
392
|
+
|
|
393
|
+
## Syncing code
|
|
394
|
+
|
|
395
|
+
`xflow status` shows how the local copy differs from the server revision.
|
|
396
|
+
`xflow push` sends sources, `xflow pull` fetches them.
|
|
397
|
+
|
|
398
|
+
If a push is rejected, the server revision is newer, meaning someone pushed first.
|
|
399
|
+
Fetch their work next to yours (`xflow pull --into ./server-copy`), merge it locally,
|
|
400
|
+
then push again. `--force` destroys their work: a last resort, not a way around the
|
|
401
|
+
error.
|
|
402
|
+
|
|
403
|
+
## Direct access without the terminal
|
|
404
|
+
|
|
405
|
+
The platform also exposes an MCP server, connected with `xflow mcp install`. When its tools
|
|
406
|
+
are available, prefer them for control-plane work: project state, database schema and
|
|
407
|
+
read-only queries, migrations, function logs and invocations, schedules, environment
|
|
408
|
+
variables, versions, publish and rollback. They answer with aggregates and say explicitly
|
|
409
|
+
when a result is truncated, which parsing terminal output does not.
|
|
410
|
+
|
|
411
|
+
Anything that depends on the working copy stays in the CLI: sending sources (`xflow push`),
|
|
412
|
+
building and shipping the functions (`xflow deploy`), creating a project (`xflow init`). The
|
|
413
|
+
tools cannot see the folder you are working in, so a build started from there would release
|
|
414
|
+
whatever revision the server happens to hold, not what you have on disk. Pulling a repository
|
|
415
|
+
through tool calls also burns the user's tokens for nothing.
|
|
416
|
+
|
|
417
|
+
## Do not
|
|
418
|
+
|
|
419
|
+
- Edit `xflow.json` by hand: the CLI writes it.
|
|
420
|
+
- Commit `.env`: it holds the project token.
|
|
421
|
+
- Push with `--force` without checking `xflow status` first.
|
|
422
|
+
- Invent platform commands: what is not in `xflow help` does not exist.
|
|
423
|
+
|
|
424
|
+
## App design
|
|
425
|
+
|
|
426
|
+
The platform design system already ships inside the project, and the app is supposed
|
|
427
|
+
to look like a part of the platform:
|
|
428
|
+
|
|
429
|
+
- `src/components/ui` for primitives: buttons, inputs, dialogs, tables, menus
|
|
430
|
+
- `src/components/blocks` for composed blocks: `data-table` and `charts`
|
|
431
|
+
- `src/index.css` for color tokens
|
|
432
|
+
|
|
433
|
+
Before writing your own component, check whether a block already covers it: props are
|
|
434
|
+
typed next to each block, larger ones keep a separate `types.ts`. Take colors from
|
|
435
|
+
tokens (`bg-card`, `text-muted-foreground`, `bg-success-soft` and the like). A custom
|
|
436
|
+
hex palette makes the app look foreign inside the platform, which is the whole reason
|
|
437
|
+
the design system sits in the project.
|
|
438
|
+
|
|
439
|
+
The app runs inside the platform in an iframe and receives the theme and the current
|
|
440
|
+
user from it. The `usePlatformAuth()` hook gives the name, role, permissions and the
|
|
441
|
+
list of organization members. Use it to draw the interface, never to guard data: the
|
|
442
|
+
value lives on the page and is edited from the console. Guard data in the function, by
|
|
443
|
+
`event.xflow`.
|
|
444
|
+
|
|
445
|
+
## Errors from a deployed app
|
|
446
|
+
|
|
447
|
+
`xflow logs` prints what broke in the browser on deployed addresses: unhandled errors,
|
|
448
|
+
rejected promises and 5xx responses. The same stream, functions included, is in the
|
|
449
|
+
platform UI: open the project, section «Облако» (Cloud), tab «Логи» (Logs). The last 200
|
|
450
|
+
records per project are kept.
|
|
451
|
+
|
|
452
|
+
Local `npm run dev` does not report anything: these logs exist for what you cannot open
|
|
453
|
+
in your own devtools.
|
|
454
|
+
|
|
455
|
+
Cloud functions do work under `npm run dev`, and nothing has to be configured for that:
|
|
456
|
+
the dev server swaps the access key of the logged-in developer for the same narrow pass and
|
|
457
|
+
forwards the call. If it answers that the key is missing, the fix is `xflow login`.
|
|
458
|
+
|
|
459
|
+
## Environment
|
|
460
|
+
|
|
461
|
+
`VITE_XFLOW_PROJECT_TOKEN` and `VITE_XFLOW_API_URL` are written by the CLI when the
|
|
462
|
+
project is created, there is no need to edit them by hand. For CI and agent runs:
|
|
463
|
+
`XFLOW_TOKEN` replaces `xflow login`, `XFLOW_API_URL` points at another platform host.
|