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