@getxflow/cli 0.10.0 → 0.10.2

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