@getxflow/cli 0.9.1 → 0.10.0

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