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