@getxflow/cli 1.8.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,689 +1,704 @@
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 site for people outside the organization (a landing page, presentation or invoice).
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. A person signed in to several organizations names the one that gets it:
16
- `xflow init <dir> --org <name>` (ask them which one if unsure; `xflow org` lists them).
17
-
18
- ## First rule
19
-
20
- Check commands and flags against `xflow help` and `xflow help <command>`, not against
21
- memory. If a command is not in the help output, it does not exist: guessing flags is
22
- pointless. The CLI prints a hint with almost every error, read it in full, it usually
23
- contains the fix.
24
-
25
- The public documentation, in Russian, goes deeper than this file: its table of contents is
26
- https://docs.getxflow.com/llms.txt, every page has a Markdown copy. Read the page you need
27
- instead of guessing; give the person https://docs.getxflow.com when they ask how something works.
28
-
29
- ## Keeping these instructions current
30
-
31
- These instructions ship with xflow CLI 1.8.1, so the copy you are reading can be older than
32
- the CLI answering your commands.
33
-
34
- When the CLI says this file does not match it, run `xflow skills --refresh` and then **read
35
- again the file it names**: refreshing only writes to disk, and until you re-read it you are
36
- still working from the old rules. Do it yourself, no need to ask.
37
-
38
- Updating the CLI is not yours. `xflow update` replaces a program shared by every project on
39
- the machine: offer the update in your summary. The exception is a platform refusing the CLI as too old (`outdated_cli`):
40
- nothing runs until it is updated, so say what you are doing and run it.
41
-
42
- ## Hard rules
43
-
44
- These mistakes cost the most because nothing fails at the moment they are made, or
45
- the error points away from the cause. The sections below carry the details.
46
-
47
- 1. Read environment variables literally: `process.env.API_KEY`. Destructuring
48
- (`const { API_KEY } = process.env`), a name built from an expression, `?.` and
49
- spreading the environment all read as no mention of the variable at all. The
50
- build rejects them, and `xflow env check` names them before that.
51
- 2. A value written with `xflow env set` reaches the functions on the next
52
- `xflow deploy functions`, not at the moment it is written.
53
- 3. Never delete a `functions/<name>/` directory unless the user asked for that
54
- function to go. The next deploy removes it from the cloud together with its keys and
55
- schedules, and that is final.
56
- 4. A function an app calls needs that app in its `callers` in the root
57
- `xflow.json`. Without it the call answers 404 `not_bound`, and a new function is not
58
- reachable from anywhere.
59
- 5. Every function called by an app open to anyone can be called by
60
- anybody on the internet with curl. Write it as such, see **Your function is open to the
61
- whole internet**.
62
- 6. An already applied migration is never re-run, and `xflow db migrate` refuses a
63
- name it already holds with different content. A schema change is always a new file.
64
- 7. When `xflow db migrate` answers `db_write_locked`, or an INSERT or CREATE TABLE in a
65
- function fails with `database size limit ... reached` (53100), the organization's space
66
- (databases and files together) is full and Postgres itself refuses. Reads, updates and deletes keep working, so the fix is deleting data
67
- (`xflow db exec` or a migration), never a rewrite of the SQL. Write comes back within the hour. The limit
68
- is shared by the organization: `xflow db list` shows the size of each project database.
69
- 8. Heavy media does not travel with the sources. Photos, video and PDFs go up with
70
- `xflow storage push`, not into the repository: the archive is capped at 10 MB.
71
-
72
- ## Projects and apps
73
-
74
- - A project is a box: one database with its migrations, the cloud functions, secrets and
75
- members. It has no code of its own.
76
- - An app is code the platform builds and serves at `https://<id>--<org>.xflowpage.ru`
77
- (`<id>` is six random characters the platform gives the app, `<org>` the name of the
78
- organization): its own versions, its own access level. A project holds one or more apps,
79
- and all of them use the same database and the same functions.
80
- - A project without apps is fine (`xflow init --no-app`): its functions are workflows, woken
81
- by a schedule, by an outside service holding a key, or by `xflow functions invoke`.
82
- - `xflow projects get` shows the apps with their folders, addresses and access levels, and
83
- who may call each function.
84
-
85
- ## Folder layout
86
-
87
- One folder per project, apps in subfolders:
88
-
89
- clinic/
90
- xflow.json projectId, and who may call each function
91
- package.json dependencies of the functions
92
- functions/<name>/index.ts cloud functions, shared by every app
93
- migrations/0001_init.sql the one database of the project
94
- apps/
95
- priem/ an app: a Vite project with its own xflow.json (appId, build)
96
- zapis/
97
-
98
- - Two kinds of history: the project root (functions, migrations, `xflow.json`) is one, each
99
- app is another. `apps/**` never travels with the root, the root never travels with an app.
100
- - An app sees only its own folder when it is built. What two apps share is data and logic,
101
- and that lives in functions. Functions never go inside an app folder: the build refuses
102
- `functions/` there (`functions-in-app`).
103
- - `xflow apps add <folder>` creates an app, lays the template into `apps/<folder>` and prints
104
- its address. Never create an app folder by hand: without its `appId` it is nobody's.
105
- `--color <color>` sets the tile the user sees in the project; a wrong one is answered with
106
- the list of colors. Its icon and a later color change belong to the person in the web
107
- interface.
108
- - Rename an app folder freely: the app is its `appId`.
109
- - Commands follow the folder you stand in: inside `apps/zapis` they mean that app, anywhere
110
- else in the project they mean the project. `--project <id>` and `--app <id>` name them
111
- without a folder.
112
- - Neither `xflow.json` is edited by hand except `functions.<name>.callers` in the root one.
113
-
114
- ## Plan limits
115
-
116
- The organization runs on a plan with limits: apps, active connections, people in the
117
- organization, space shared by databases and files, function minutes, builds per month and at once, how often
118
- schedules may run, and which access levels an app may have. The number of functions, schedules
119
- and projects is not capped; at most 15 new projects a day (UTC) per organization, deleted ones count. Most are finite; a plan
120
- may leave one uncapped, and then `whoami` prints `no limit` instead of a ceiling.
121
- A paid plan comes in packages (Start S/M/L, Business M/L/XL): the package sets the space,
122
- database hours and function minutes, the plan sets the rest. More space is a bigger package
123
- chosen by a person in «Тариф» (Plan); there is no separate purchase of space.
124
- `xflow whoami` prints every one of them next to what is used, and the rights of your own key
125
- with them: destroying data (a migration or `db exec`), deleting files from storage, moving a database and
126
- linking a connected account each need a right. Reading it before a long task turns a refusal
127
- halfway through the work into a sentence said before it starts.
128
-
129
- - Connections are counted while active: switching an unused one off frees the seat without
130
- deleting it.
131
- - Function minutes are shared by the whole organization: every app and schedule spends
132
- them. When they run out, every function of the organization stops (`functions_stopped`).
133
- - An unpaid plan stops the functions at once, and five days later freezes the organization:
134
- every command except `xflow whoami` answers `subscription_blocked`,
135
- apps show a "suspended" page. Nothing is deleted and no code fixes it: tell the
136
- person that the owner pays the invoice in the organization settings, section «Тариф» (Plan).
137
- - The cap on how long one function call may run is written into the function when it is
138
- deployed, so a plan change reaches the cloud only with the next `xflow deploy functions` of
139
- each project, in both directions.
140
- - The functions of one project take a plan-set rate: Free 5 calls a second (a burst of 60),
141
- Start 20 (300), Business 50 (750). A call over the rate gets 429 at once. Calls at the same time:
142
- Free 4, Start 10, Business 25, the rest wait in a queue. Fan out in batches of that size, never
143
- all items of a list in parallel.
144
-
145
- A limit refusal is not a bad request. The CLI prints a line starting with `Plan limit:`, the
146
- API answers `code: "forbidden"` with a `limit` object (`code`, `used`, `limit`). Retrying, renaming or rewriting the code changes nothing: tell the user
147
- what ran out and stop, only the owner lifts it. A schedule refused for running too often is one
148
- of these too, not a bad cron string: the minimum interval comes from the plan, never guess it.
149
-
150
- ## Deploying
151
-
152
- 1. Change the code. In an app folder `npm run typecheck` is a two-second check, `npm run build`
153
- a fast way to see build errors early: the platform builds again in its own sandbox.
154
- 2. `xflow deploy functions -m "what changed"` sends the project root, checks it, applies the
155
- new migrations, ships the functions and their callers. A migration that fails or would
156
- destroy data stops it before any function ships: settle it with `xflow db migrate`
157
- (`--allow-destructive`), then deploy again. `xflow db migrate` alone changes the schema
158
- without shipping code. Every app runs the new code the moment it finishes: there is
159
- no staging and no publish step for server code. Say so to the user.
160
- 3. `xflow deploy apps/zapis -m "what changed"` sends and builds one app; inside `apps/zapis` a
161
- plain `xflow deploy` does the same. In the root a plain `xflow deploy` refuses and names the
162
- targets. The comment is required: one short line in the language the user speaks.
163
- 4. `xflow publish` in the app folder (or with `--app <id>`) shows that build to visitors.
164
-
165
- A change that touches data and screens goes in this order: the functions (with the migration),
166
- then every app that shows it. `xflow deploy apps/...` warns when the root holds changes nobody
167
- deployed.
168
-
169
- - A build of an app is not shown to visitors until `xflow publish`. Publishing points the
170
- address at a build and takes a second; `xflow publish <number>` (or `xflow rollback <number>`)
171
- points it at an older kept build, and that is the rollback. The very first build of a new
172
- app publishes itself.
173
- - Only the app moves on publish and rollback. Functions come back with
174
- `xflow deploy functions --revision N -m "why"` (`xflow deployments functions` lists the revisions);
175
- `--project <id>` names the project without a folder.
176
- - Files of the **three latest successful builds** of each app are kept, plus the live one;
177
- `xflow deployments` marks the rest `no pages`. The history keeps 30 builds, a revision stays
178
- while its build is in it or it is among the 10 latest. Keep anything you must not lose in
179
- your own git repository.
180
- - Give a person the app address the CLI printed, or the project page it printed. Name builds
181
- by their number.
182
- - `xflow status` says how the local copies differ from the server. A deploy rejected before
183
- the build starts means the server holds a revision this folder has never seen: fetch it next
184
- to yours (`xflow pull --into ./server-copy`), merge in git, deploy again. `--force` sends this
185
- folder over it anyway; the overwritten revision is kept while it is among the last 10, and the
186
- deploy prints the `xflow pull --revision <N>` line that brings it back. A folder with no record of the server revision (a fresh clone, a CI checkout) is
187
- not rejected: its first deploy goes on top and says so.
188
- - A deploy that would delete functions gone from the sources refuses until somebody agrees;
189
- `--allow-removals` is the only way past it without a terminal. Reaching for it to get a
190
- build running means you deleted something the user never asked you to: put the directories
191
- back. If the removal is intended, name the functions and let the user answer.
192
-
193
- ## Build gate
194
-
195
- Before the sandbox starts, the platform checks the sources. Everything below blocks the build,
196
- all violations come back together with files and lines, and a rejected attempt costs nothing.
197
-
198
- An app:
199
-
200
- - `package.json` with the build script named in its `xflow.json` (`npm run build` by default),
201
- a `vite.config.*` and `vite` in dependencies. No server frameworks: `next`, `nuxt`, Remix,
202
- `@sveltejs/kit`, `astro`.
203
- - `index.html` in the app root with `id="root"` and the script that loads `src/main.tsx`
204
- (`.ts`, `.jsx`, `.js` count too). Code under `src/`; root `app/`, `pages/`, `next/` are rejected.
205
- - Keep `base` in `vite.config.js` as the template has it: the platform serves each build from
206
- `/_b/<key>/`, and a build whose `index.html` points elsewhere is refused.
207
- - The built app at most 15 MB unpacked, the sources at most 10 MB.
208
- - File names in latin letters, digits, spaces and `. - _ ~` only: `логотип.png` breaks the build.
209
- - Call functions through `xflow.functions.invoke`, never a hardcoded function address.
210
- - No keys or tokens in the sources, no server modules (`fs`, `express`, `http`, `child_process`).
211
- - Outside `src/components/ui` and `src/components/blocks`: no `alert()`, `confirm()`,
212
- `prompt()` (use Dialog and Toast), no `console.log` (visitors read the console;
213
- only crashes and 5xx reach the logs).
214
- - A library that needs a provider (`@tanstack/react-query`, `react-redux`, `sonner`,
215
- `react-hot-toast`, `react-dnd`) must have it mounted. A missing provider builds fine and
216
- gives visitors a white screen.
217
- - The platform files stay byte for byte as they arrived: `src/lib/platform-auth.ts`,
218
- `src/contexts/platform-auth-context.tsx`, `src/hooks/use-platform-auth.ts`,
219
- `src/utils/error-logger.ts`, `src/lib/xflow.ts`. The code keeps calling `initPlatformAuth()`
220
- and `initErrorLogger()` and importing `index.css`, and the Tailwind
221
- `content` globs keep covering `src/**`. A rejection names the revision to take the original
222
- from: `xflow pull --revision N --into ./original`, then copy the file back.
223
-
224
- The project root: function names are lowercase latin letters, digits and hyphens, up to 41
225
- characters (`send-mail`, not `sendMail`); every name under `functions` in `xflow.json` has its
226
- `functions/<name>/index.ts`; every caller is an app folder of this project (`unknown-caller`).
227
-
228
- Passes with a line `Unfinished:` instead of blocking: the template's page title, no
229
- `<link rel="icon">` in `index.html`, an `exports.access` left in a function (it does nothing).
230
- Fix the first two before you tell the person the app is done.
231
-
232
- ## The template is a starter kit
233
-
234
- - `src/components/ui` and `src/components/blocks` are a starting set: change them, restyle them
235
- or delete what you do not use. Any colors, your own color scheme, inline styles, a single
236
- theme without dark mode: all allowed.
237
- - The template looks like the xflow interface: warm grays, hairline controls (`ctl-solid`,
238
- `ctl-raised`, `ctl-inset`, `ctl-select`), segments (`seg-track`, `seg-on`) and surfaces
239
- (`surface-raised`, `surface-pop`, `surface-modal`) in `src/index.css`.
240
- - While any template component is left, the theme token names in `:root` of `src/index.css`
241
- and their `hsl(var(--name))` mapping in the Tailwind config stay: those components paint with
242
- them. Values change freely; they are HSL components (`216 95% 40%`), not `#hex`. Your own
243
- colors go under new names.
244
- - Images and other assets: import them from `src/` (`import logo from './assets/logo.png'`)
245
- so the build puts them next to the code. An absolute path into `public/` (`/logo.png`) goes
246
- through the access check of the app and fails inside the xflow frame in Safari.
247
-
248
- ## Who opens an app
249
-
250
- A person sets this in the web interface; you cannot, and you should not ask for a way around.
251
-
252
- | Level | Who gets in |
253
- |---|---|
254
- | Project members | people who have access to the project |
255
- | Anyone with the link | anybody, no sign-in at all |
256
-
257
- - The plan decides which levels exist: on Free an app opens only inside xflow, Start adds the
258
- direct address and "Anyone with the link".
259
- - At the closed level the platform signs people in, and a function receives the person in
260
- `event.xflow.user` (`id`, `role` in the organization, `name`, `access` to the project).
261
- Keep roles inside the app (doctor, receptionist) in your own table keyed by `user.id`.
262
- - People outside the organization (patients, pickers, clients) are not xflow accounts: make the
263
- app "Anyone with the link" and give it its own sign-in, see below.
264
- - At "Anyone with the link" the platform signs nobody in: `event.xflow.user` is null for
265
- visitors. Accounts, sign-in and who sees which rows are the code of your app, see below.
266
- - `usePlatformAuth()` in the frontend knows the person only inside the xflow frame, and even
267
- there it is a hint for drawing the interface, edited from the console by anyone. On the
268
- direct address it answers `anonymous`. Guard data in the function, by `event.xflow`.
269
-
270
- ## Which functions an app may call
271
-
272
- The root `xflow.json` names the callers of each function:
273
-
274
- {
275
- "projectId": "…",
276
- "functions": {
277
- "list-slots": { "callers": ["zapis", "priem"] },
278
- "book-slot": { "callers": ["zapis"] }
279
- }
280
- }
281
-
282
- - A caller is an app folder of this project (`apps/zapis` is `"zapis"`). A function without
283
- callers is left out of `functions` or given `"callers": []`, the same thing.
284
- - A function nobody lists has no callers: no app reaches it. That is a workflow,
285
- run by a schedule, a key or `xflow functions invoke`.
286
- - Callers ship with `xflow deploy functions` and come back with its `--revision N`.
287
- - The app calls `xflow.functions.invoke('book-slot', { body })` from `src/lib/xflow.ts`. The call
288
- goes to `/_fn/book-slot` on the app's own address: no function addresses, tokens or keys in
289
- the bundle, nothing to rebuild when a function changes.
290
- - A refusal throws `XFlowError` with `status` and `code`: `not_bound` (404, add the caller and
291
- deploy functions), `not_found` (404, the function is not deployed), `login_required` and
292
- `invalid_credential` (401, the visitor has to sign in again), `no_access`, `role_too_low`,
293
- `functions_stopped` (403, minutes ran out or the plan is unpaid: a person settles it),
294
- `rate_limited` (429), `unavailable` (503, retry later). Branch on `code`, never on the message.
295
-
296
- ## Your function is open to the whole internet
297
-
298
- A function is open when an app at "Anyone with the link" is among its callers. `xflow deploy functions` says so in a line of its own, `xflow functions list`
299
- prints `open`, the web interface marks it.
300
-
301
- Open means anybody can call it with curl, without your app: the browser is not a gate.
302
- Hiding the address, checking `Origin` or `Referer`, a token baked into the bundle: all of it is
303
- readable or forgeable and protects nothing. Write every open function as if the request came
304
- from a stranger, because it may:
305
-
306
- - Check every input: types, lengths, allowed values; refuse fields you do not expect. Put input
307
- into SQL as parameters only (`$1`), never into the text.
308
- - Return only what this caller needs: no `select *` into a response, no rows of other people,
309
- no internal fields, no stack traces.
310
- - Make writes safe to repeat: take a request id from the client and put a unique index on it,
311
- so a retry or a replay changes nothing.
312
- - Deduplicate and limit per visitor where it costs: `event.xflow.ip` is the visitor's address,
313
- set by the platform proxy and not by the caller. The proxy caps requests per address; the
314
- organization pays for every minute a function runs.
315
- - A form open to strangers needs a captcha: Cloudflare Turnstile, with the token checked in the
316
- function against `https://challenges.cloudflare.com/turnstile/v0/siteverify` and the secret
317
- from `xflow env set`. The site key is public and stays in the source. A check in the browser
318
- checks nothing.
319
- - `minRole` on an open function refuses every anonymous visitor (`login_required`): use it only
320
- where members call.
321
-
322
- ## Sign-in in a public app
323
-
324
- At the closed levels do none of this: the platform already knows the person.
325
-
326
- - Passwords: store only a hash, `crypto.scrypt` from Node with a random salt per user, compare
327
- with `crypto.timingSafeEqual`. Never log a password, never return it, never keep it in the
328
- browser.
329
- - Session: after sign-in the function makes a random token (`crypto.randomBytes(32)`), stores
330
- its SHA-256 and an expiry in a table and returns the token in the response body. The app keeps
331
- it in `localStorage` and sends it on every call in the `X-Session` header. Not a cookie: Cloud
332
- Functions drop `Cookie` and `Set-Cookie` both ways. `localStorage` belongs to one address, a
333
- neighbouring `*.xflowpage.ru` site does not read it.
334
- - Any script on the page reads that token, so: an expiry of hours that each call extends,
335
- sign-out and a password change delete the session rows, no user text rendered as HTML
336
- (`dangerouslySetInnerHTML`), no scripts loaded from outside the build.
337
- - Never put the token into an address (`?session=`, a link): it stays in history and logs. A file
338
- the function makes for this person is fetched with the header and opened as a `blob:` URL.
339
- - Not the `Authorization` header: `/_fn` replaces it with the platform's own. Nor headers named
340
- `X-Xflow-*`, `X-Project-Pass`, `X-Server-Key`, `X-Function-Key`: the platform strips them.
341
- - Every function that serves a signed-in visitor reads `X-Session` from `event.headers` (match
342
- the name in any case), finds the session and filters rows by that user in SQL. A hidden button
343
- is not access control.
344
- - Count failed sign-ins per email and per `event.xflow.ip` in a table and slow them down.
345
- - Never in the frontend: keys, secrets, the list of users, hashes, a role check that is the only
346
- guard.
347
- - The platform sends no email for your app: confirmation and reset letters need a mail provider
348
- whose key goes into `xflow env set`.
349
- - `xflow dev` does not carry `X-Session` and calls functions as you (a member).
350
- Try the sign-in on the app address.
351
-
352
- ## A site for people outside the organization
353
-
354
- - A landing page, a presentation, an invoice is an app at "Anyone with the link" (a paid plan):
355
- built, published and rolled back like any app, opened by its address without signing in.
356
- - Search engines are asked not to index apps: do not promise SEO.
357
-
358
- ## Cloud functions
359
-
360
- `functions/<name>/index.ts` in the project root exports `handler` (`index.js` and `index.mjs`
361
- are read too), `export const handler` and `exports.handler` both work. The request arrives as
362
- `event.httpMethod`, `event.headers`, `event.queryStringParameters` and `event.body`, a string
363
- (`JSON.parse` it; `event.isBase64Encoded` marks a binary body). The handler returns
364
- `{ statusCode, body }` where `body` is a JSON string, and may add `headers`.
365
- `xflow functions list` shows status, callers, `open`, keys, last call and last error.
366
-
367
- - Dependencies (`pg`, a mail client) go into the root `package.json`; the platform installs them
368
- on every deploy, and a lock file next to it makes that exact.
369
- - Code shared by functions lives in a root folder such as `lib/`, imported by relative path: each
370
- function is bundled with what it imports. A folder in `functions/` without `index.*` is not a
371
- function. Read `process.env` in `functions/<name>/` itself: `xflow env check` looks only there.
372
-
373
- Keep one shape inside that string across the project: `{ success: true, data }` when it worked,
374
- `{ success: false, error: { message, code } }` when it did not. Branch the frontend on
375
- `error.code`, never on `error.message`.
376
-
377
- A function whose code and variables did not change is not shipped again, and one that fails to
378
- ship fails the whole deploy.
379
-
380
- Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it as you,
381
- through the platform, and prints the body alone, ready for a pipe (`--data '{"a":1}'`, `--data-file
382
- body.json`, `--method GET`; 4xx and 5xx exit non-zero; Windows PowerShell 5.1 prepends a BOM when
383
- piping to another program, so read it with `ConvertFrom-Json` or Python's `utf-8-sig`); `xflow functions logs <name>` shows
384
- the crashes and 5xx answers with stacks and console output, the latest 10 (`--limit N` for more).
385
- Only failed calls are logged. Empty output does not clear a function that fails at module start,
386
- runs over its time cap or out of memory: those never reach the logs, `invoke` shows them.
387
-
388
- The body has to be JSON and the CLI checks it before calling. In PowerShell the double quotes
389
- never reach the CLI (`--data '{"a":1}'` arrives as `{a:1}`): escape them (`--data '{\"a\":1}'`)
390
- or pass a file. It bites `--payload` of a schedule the same way. A command that worked writes
391
- nothing to stderr, so a red line there means it really failed.
392
-
393
- ### Who is calling
394
-
395
- The platform checks every call before your code runs and hands the answer over in `event.xflow`:
396
-
397
- ```js
398
- exports.handler = async (event) => {
399
- const { caller, slug, user, ip } = event.xflow
400
- // caller: 'app' (an app of this project), 'service' (another function of this
401
- // project), 'external' (an outside service with a key), 'schedule',
402
- // 'platform' (a person through xflow functions invoke or the web interface)
403
- // slug: the address name of the calling app (<id>--<org>), null otherwise
404
- // user: { id, role, name, access } of a signed-in person, null for anonymous
405
- // visitors and keys. access: 'view' | 'edit' on this project
406
- // ip: the visitor's address, null for a schedule
407
- }
408
- ```
409
-
410
- Never trust an identity that arrives in the body or in a header of the request: the page lives
411
- on someone else's computer. `event.xflow` is the only identity the platform stands behind.
412
-
413
- Project access is separate from the role: a person who only views the project (`access:
414
- 'view'`) still reaches every function. A function that writes refuses them:
415
-
416
- ```js
417
- if (event.xflow.user && event.xflow.user.access !== 'edit') {
418
- return { statusCode: 403, body: JSON.stringify({ error: 'Read-only access' }) }
419
- }
420
- ```
421
-
422
- To remember who did something, store `user.id` and `user.name` in the row: the name is never
423
- empty, and the app has no other way to look up other people.
424
-
425
- A function that changes data should say who may call it instead of checking the role by hand:
426
-
427
- ```js
428
- exports.minRole = 'developer' // 'member' | 'developer' | 'owner'
429
- ```
430
-
431
- The platform refuses anything below that role before your code runs; it applies to people
432
- (apps, `functions invoke`), not to keys and schedules. `member` is the people who only
433
- use the apps (clinic employees, managers), `developer` and `owner` build them. In the web
434
- interface these roles are called «Просмотр» (`member`), «Редактор» (`developer`) and «Админ»
435
- (`owner`). Without the line
436
- every member of the project can call the function. Losing access
437
- closes the function within a minute.
438
-
439
- - Another function of the project: `fetch(`${process.env.XFLOW_FN_URL}/<name>`, { headers:
440
- { 'X-Server-Key': process.env.XFLOW_SERVER_KEY } })`. The key opens the functions of its own
441
- project only, whatever their callers.
442
- - An outside service (a payment webhook, a bot, a CRM) calls
443
- `https://fn.xflowpage.ru/<projectId>/<name>` (`xflow functions list` prints it) with a key of
444
- that function in `X-Function-Key` or `?key=`. Keys are issued by a person on the function's
445
- screen in the web interface, never by you: ask the user. A function holds at most two keys,
446
- the second one to replace the first without downtime.
447
- - `exports.access` does nothing any more: who may call is `callers`.
448
-
449
- ## Secrets and connected accounts
450
-
451
- Keys and passwords live on the platform, not in the repository: `xflow env set SMTP_PASSWORD=…`
452
- writes one, `xflow env` lists the names, `xflow env check` names the variables your functions
453
- read but the platform does not have. Values never come back out: the only place they exist is
454
- inside the running function. Apps get no variables at all.
455
-
456
- A variable belongs to one project: functions of other projects never see it, and each project
457
- keeps its own value under the same name.
458
-
459
- `DATABASE_URL` and every name starting with `XFLOW` are filled in by the platform: writing them
460
- is refused.
461
-
462
- Some variables come from a connected account instead of from you. Credentials of an account
463
- linked to the project show up in `xflow env` marked read-only, under a prefix chosen at link
464
- time (`YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_CLIENT_LOGIN`): read them like any other variable.
465
- If calls to that service start failing on authorisation, `xflow status` says whether the token
466
- is merely expiring (any deploy of the functions renews it) or the account was disconnected on
467
- the provider's side, which only a human can reconnect.
468
-
469
- `xflow connections` lists those accounts: the ones linked to this project, with the alias and
470
- the state of the access, and the ones the organization has but this project does not use yet,
471
- marked `available, not linked`. Check it before telling anyone a service is unavailable.
472
-
473
- `xflow connections link "Яндекс Метрика" --as YANDEX_METRIKA` is that link and
474
- `xflow connections unlink YANDEX_METRIKA` undoes it; `xflow help connections` has the naming
475
- rules. In the project folder unlink refuses while a function still reads one of the variables,
476
- naming those functions, so read that list before reaching for `--yes`.
477
-
478
- Linking needs the `connections:link` right on the key, and only accounts granted to the owner
479
- of the key personally can be linked at all. A key cannot grant it to itself: ask the person.
480
- Connecting a new account and switching one off stay with a person too.
481
-
482
- ### Asking an API what it returns
483
-
484
- ```
485
- xflow connections call WB /api/v1/supplier/stocks?dateFrom=2026-09-01
486
- xflow connections call "Яндекс Метрика" /management/v1/counters
487
- xflow connections call OZON /v1/product/list --method POST --data '{}' \
488
- --header '{"Client-Id":"{CLIENT_ID}"}'
489
- ```
490
-
491
- The platform attaches the credentials and makes the request itself: no deploy, and the key never
492
- reaches you. A linked account is called by its alias, an `available, not linked` one by its
493
- name: linking first is not needed to look. Calling is looking, not wiring: to use the account
494
- in code, link it and deploy functions.
495
-
496
- `{NAME}` in the path or in a header value is filled in from the fields of the connection. Take
497
- the names from `xflow connections`, which prints them in braces: an invented name is left in the
498
- text and the request goes out anyway, into somebody's live account. Ask for the shape, not the
499
- volume: one day and `limit=1`; `--json | jq '.response.body | keys'` gives the field names
500
- without the body entering your context. `POST` and `DELETE` change things in a live account and
501
- nothing undoes that: ask the person first when the endpoint is not clearly a read.
502
-
503
- ## Schedules
504
-
505
- `xflow schedules set report "0 3 ? * * *"` runs a function daily at 03:00. Six fields, UTC,
506
- and exactly one of day-of-month / day-of-week must be `?`. A run reaches the handler as a POST
507
- with `event.xflow.caller === 'schedule'`, and `--payload '{"mode":"full"}'` (or
508
- `--payload-file body.json`) is its body. The function has to be deployed first.
509
- A schedule is the function plus its cron: `set` with the same pair only changes the payload, a
510
- new cron is one more schedule next to the old one. The plan caps the count for the whole
511
- organization, so take the old one off with `xflow schedules rm report "<old cron>"`.
512
-
513
- ```
514
- xflow env set SMTP_PASSWORD=... # secrets first: values ride the next deploy
515
- # write functions/report/index.ts, reading process.env.SMTP_PASSWORD literally
516
- xflow deploy functions -m "Nightly report"
517
- xflow schedules set report "0 3 ? * * *" --payload '{"mode":"full"}'
518
- xflow functions invoke report # run it once
519
- xflow functions logs report # crashes of real runs
520
- ```
521
-
522
- ## Database
523
-
524
- Schema changes are files: `migrations/0001_init.sql`, `migrations/0002_orders.sql` in the
525
- project root, applied in filename order by `xflow db migrate` from any folder of the project.
526
- `xflow db status` shows what is applied and what waits. Never edit an applied file, write a new
527
- migration instead. Every project has exactly one database of its own, shared by all its apps and
528
- functions.
529
-
530
- The browser never reaches the database directly. The app reads and writes through a cloud
531
- function, and inside the handler the connection string is already there:
532
-
533
- ```js
534
- const { Client } = require('pg')
535
- const db = new Client({ connectionString: process.env.DATABASE_URL })
536
- ```
537
-
538
- The platform passes `DATABASE_URL` only to functions that mention it and sets the project schema
539
- on every connection, so plain table names (`select * from tasks`) hit your project.
540
-
541
- `migrations/` says what you did; `xflow db schema [table]` says what is in the database now, and
542
- `xflow db query 'select ...'` reads it inside a READ ONLY transaction (as many rows as its
543
- own `limit` asks, 50 without one, 300 at most; `--json` prints long values whole). Single quotes hold in PowerShell, bash
544
- and zsh, cmd.exe wants double ones; a query with quotes of its own goes in a `.sql` file outside
545
- the project, `xflow db query ../q.sql`.
546
-
547
- A one-off data fix is `xflow db exec 'update ...'` (or `../fix.sql`), not a migration: one
548
- INSERT, UPDATE, DELETE or MERGE, saved at once, never re-run, no trace in the migration history.
549
- Schema changes are refused there. Deleting rows, or updating more than 100 in a table (cascades
550
- and triggers count), asks for `--allow-destructive`, and deleting also for the right below.
551
- There is no undo: read the rows with `db query` first.
552
-
553
- The platform keeps no database history and no backups, so anything that destroys data
554
- (`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE`) needs both the `--allow-destructive` flag and
555
- the right to destroy data on the key, which is off by default and only its owner turns on. It is
556
- judged by the result, not the text: a migration that dropped or emptied a table, dropped a column
557
- or deleted rows is rolled back, however the SQL was written. `BEGIN`/`COMMIT` in a migration are
558
- refused: each one already runs in its own transaction. When the refusal is about the right, the flag changes nothing: say what
559
- needs deleting and why, and let the person decide. With both in place the affected tables are
560
- dumped first (up to 50 000 rows each) and kept for 7 days; getting a dump back is a request to
561
- the person, no command does it. Check with `--dry-run`.
562
-
563
- Pass `DATABASE_URL` as is, without an `ssl` option. The database is PostgreSQL 18, so do not
564
- tune it from migrations:
565
- - Installed: `pg_trgm`. `gen_random_uuid()` and `uuidv7()` are built in. Never `uuid-ossp`.
566
- Anything else (`postgis`, `pgvector`) is a request to the person.
567
- - A statement is cut after 10 seconds (in a migration through `xflow db migrate`, after 2 minutes),
568
- waiting for a lock after 10, a transaction left open after 60. Connections to the database of
569
- one project: Free 10, Start 25, Business 50. A query that needs longer is a wrong query (a missing index, or a report that belongs in a
570
- scheduled function writing a summary table).
571
-
572
- Calls of one project run at the same time in one process. A `Client`, even one made at module
573
- level, gets its own pooled connection for each call, so `connect()` twice is fine. Keep
574
- per-request data out of module-level variables. Session state (`SET` without `LOCAL`, temp tables,
575
- advisory locks) lasts until the call ends, and a transaction left open is rolled back: set what
576
- the call needs inside the handler. A `Client` that listens for `notification` keeps its own
577
- connection.
578
-
579
- ## File storage
580
-
581
- **Static assets of an app** (product photos, video, PDFs, big artwork) you upload yourself:
582
-
583
- ```bash
584
- xflow storage push ./media --to media # a folder, its structure kept
585
- xflow storage ls # what is there, with addresses and sizes
586
- ```
587
-
588
- `push` prints the address of every file (`/_files/<id>`) and skips what is already stored under
589
- the same path and size; `--replace` overwrites a file whose size differs and keeps its address.
590
- Use the address as is inside an app: the app's own host serves it to whoever may open the app,
591
- including anonymous visitors of a public one. Icons, fonts and small artwork stay in the
592
- repository.
593
-
594
- Deleting needs a right of its own, off by default, so `xflow storage rm` may answer that the key
595
- was not granted it. Ask the person and do not look for a way around: whatever the users of the
596
- app uploaded lives in the same folders, and there is no undo. A folder is
597
- `xflow storage rm --folder <path> --yes`.
598
-
599
- **Files that users of the app upload** go through a cloud function of your own: it asks the
600
- platform for a one-time link, the browser sends the bytes straight to storage, and a second call
601
- records the file:
602
-
603
- ```js
604
- const link = await fetch(`${process.env.XFLOW_API_URL}/api/storage/project/upload-url`, {
605
- method: 'POST',
606
- headers: {
607
- 'Content-Type': 'application/json',
608
- 'X-Server-Key': process.env.XFLOW_SERVER_KEY,
609
- },
610
- body: JSON.stringify({ fileName, fileSize, contentType, folderPath: 'invoices' }),
611
- }).then((r) => r.json())
612
- ```
613
-
614
- `confirm` takes the returned `s3Key` and answers with the file and its address; `delete` takes
615
- that same `url`. No endpoint lists files back, so write the address into a table of your own in
616
- the same call. What a visitor may do with files is decided inside that function; never send the
617
- key to the frontend.
618
-
619
- - A name already taken in that folder is refused before the link is issued; `overwrite: true`
620
- replaces the bytes and keeps the address.
621
- - 200 MB per file and the organization's space (databases and files together), checked again
622
- on `confirm`.
623
- - Confirm only after the PUT has finished, and send on the PUT exactly the `Content-Type` you
624
- named: the link is signed for it, a different or missing type gets 403.
625
- - Assets only: pages, scripts and executables (`html`, `js`, `exe`, …) are refused with
626
- `forbidden_type`.
627
-
628
- A refusal comes back as `{ error, code }` (`invalid_name`, `file_too_large`, `quota_exceeded`,
629
- `not_uploaded`, `duplicate_name`, `forbidden_type`, `not_found`, …): branch on `code`.
630
-
631
- ## Organizations and keys
632
-
633
- One key per organization, stored side by side: `xflow org` lists them, `xflow org switch
634
- <name|id>` changes the active one, `xflow help org` has the precedence rules.
635
-
636
- Inside a project folder commands follow the organization the folder is bound to, whatever the
637
- active one is. `--project <id>` and `--app <id>` ignore the folder whole (`xflow help project`
638
- lists the commands that take them). A "not found" on a project you know exists usually means the
639
- key belongs to another organization: check `xflow org`; signing in to a missing organization is
640
- `xflow login`, and that needs a person with a browser.
641
-
642
- A key has its owner's level in each project. With view-only access reading works (`projects
643
- get`, logs, `db query`, `pull`), and every change fails with `Your access to this project is
644
- view-only`. That is not a bug to work around: tell the person, edit access is theirs to ask for.
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
- - Dropdowns, checkboxes, radios and dates through `Select`, `Checkbox`, `RadioGroup`,
657
- `DatePicker` from `src/components/ui`: a bare `<select>` opens the system list in Safari
658
- and Firefox, out of the app's style.
659
- - Every screen starts with a clear header; header, filters and content in one column.
660
- - Draw all four states: loading, error, empty, data. An empty screen says what appears here and
661
- offers an action.
662
- - Colour only when it communicates: status, action, error, success. Grey carries the structure.
663
- - Density over air: people scan and compare. Numbers in a column, `tabular-nums`.
664
- - Name the shape first: list, list with detail, dashboard, form, board.
665
- - Seed 5-10 believable rows in the first migration: a screen with zero rows looks unfinished.
666
- - Give the app its own page title and icon.
667
-
668
- ## Errors from a deployed app
669
-
670
- `xflow logs` prints what broke in the browser on the app addresses: unhandled errors, rejected
671
- promises and 5xx responses; in an app folder that app, in the project root every app.
672
- `xflow functions logs [name]` prints function crashes with the tail of their console, and failed
673
- calls (timeout, out of memory, 5xx; no console for those); `--all` adds every call as its status
674
- and time, `--cursor` (printed at the end) goes to older ones. A person sees the same in the web
675
- interface: browser errors in the «Ошибки» (Errors) panel of the app, function crashes on the
676
- «Ошибки» tab of the function's screen. `xflow db logs` prints what the database refused: failed
677
- and timed-out queries with their SQL (cut to 2 KB), dropped connections. Kept for 7 days, 1 day on
678
- the Free plan.
679
-
680
- Run an app locally with `xflow dev` in its folder, not a bare `npm run dev`: every `/_fn/<name>`
681
- call then goes to the platform as this app, with the same callers and access as on its address,
682
- and a function someone else deployed answers at once. A bare `npm run dev` has no key, so its
683
- function calls fail. The server answers `127.0.0.1` alone, because it holds that key. A local
684
- run reports nothing to the logs.
685
-
686
- ## Environment
687
-
688
- For CI and agent runs: `XFLOW_TOKEN` replaces `xflow login`, `XFLOW_API_URL` points at another
689
- platform host. There is no `.env` from the platform: the bundle carries no tokens or addresses.
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 site for people outside the organization (a landing page, presentation or invoice).
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. A person signed in to several organizations names the one that gets it:
16
+ `xflow init <dir> --org <name>` (ask them which one if unsure; `xflow org` lists them).
17
+
18
+ ## First rule
19
+
20
+ Check commands and flags against `xflow help` and `xflow help <command>`, not against
21
+ memory. If a command is not in the help output, it does not exist: guessing flags is
22
+ pointless. The CLI prints a hint with almost every error, read it in full, it usually
23
+ contains the fix.
24
+
25
+ The public documentation, in Russian, goes deeper than this file: its table of contents is
26
+ https://docs.getxflow.com/llms.txt, every page has a Markdown copy. Read the page you need
27
+ instead of guessing; give the person https://docs.getxflow.com when they ask how something works.
28
+
29
+ ## Keeping these instructions current
30
+
31
+ These instructions ship with xflow CLI 1.10.0, so the copy you are reading can be older than
32
+ the CLI answering your commands.
33
+
34
+ When the CLI says this file does not match it, run `xflow skills --refresh` and then **read
35
+ again the file it names**: refreshing only writes to disk, and until you re-read it you are
36
+ still working from the old rules. Do it yourself, no need to ask.
37
+
38
+ Updating the CLI is not yours. `xflow update` replaces a program shared by every project on
39
+ the machine: offer the update in your summary. The exception is a platform refusing the CLI as too old (`outdated_cli`):
40
+ nothing runs until it is updated, so say what you are doing and run it.
41
+
42
+ ## Hard rules
43
+
44
+ These mistakes cost the most because nothing fails at the moment they are made, or
45
+ the error points away from the cause. The sections below carry the details.
46
+
47
+ 1. Read environment variables literally: `process.env.API_KEY`. Destructuring
48
+ (`const { API_KEY } = process.env`), a name built from an expression, `?.` and
49
+ spreading the environment all read as no mention of the variable at all. The
50
+ build rejects them, and `xflow env check` names them before that.
51
+ 2. A value written with `xflow env set` reaches the functions on the next
52
+ `xflow deploy functions`, not at the moment it is written.
53
+ 3. Never delete a `functions/<name>/` directory unless the user asked for that
54
+ function to go. The next deploy removes it from the cloud together with its keys and
55
+ schedules, and that is final.
56
+ 4. A function an app calls needs that app in its `callers` in the root
57
+ `xflow.json`. Without it the call answers 404 `not_bound`, and a new function is not
58
+ reachable from anywhere.
59
+ 5. Every function called by an app open to anyone can be called by
60
+ anybody on the internet with curl. Write it as such, see **Your function is open to the
61
+ whole internet**.
62
+ 6. An already applied migration is never re-run, and `xflow db migrate` refuses a
63
+ name it already holds with different content. A schema change is always a new file.
64
+ 7. When `xflow db migrate` answers `db_write_locked`, or an INSERT or CREATE TABLE in a
65
+ function fails with `database size limit ... reached` (53100), the organization's space
66
+ (databases and files together) is full and Postgres itself refuses. Reads, updates and deletes keep working, so the fix is deleting data
67
+ (`xflow db exec` or a migration), never a rewrite of the SQL. Write comes back within the hour. The limit
68
+ is shared by the organization: `xflow db list` shows the size of each project database.
69
+ 8. Heavy media does not travel with the sources. Photos, video and PDFs go up with
70
+ `xflow storage push`, not into the repository: the archive is capped at 10 MB.
71
+
72
+ ## Projects and apps
73
+
74
+ - A project is a box: one database with its migrations, the cloud functions, secrets and
75
+ members. It has no code of its own.
76
+ - An app is code the platform builds and serves at `https://<id>--<org>.xflowpage.ru`
77
+ (`<id>` is six random characters the platform gives the app, `<org>` the name of the
78
+ organization): its own versions, its own access level. A project holds one or more apps,
79
+ and all of them use the same database and the same functions.
80
+ - A project without apps is fine (`xflow init --no-app`): its functions are workflows, woken
81
+ by a schedule, by an outside service holding a key, or by `xflow functions invoke`.
82
+ - `xflow projects get` shows the apps with their folders, addresses and access levels, and
83
+ who may call each function.
84
+
85
+ ## Folder layout
86
+
87
+ One folder per project, apps in subfolders:
88
+
89
+ clinic/
90
+ xflow.json projectId, and who may call each function
91
+ package.json dependencies of the functions
92
+ functions/<name>/index.ts cloud functions, shared by every app
93
+ migrations/0001_init.sql the one database of the project
94
+ apps/
95
+ priem/ an app: a Vite project with its own xflow.json (appId, build)
96
+ zapis/
97
+
98
+ - Two kinds of history: the project root (functions, migrations, `xflow.json`) is one, each
99
+ app is another. `apps/**` never travels with the root, the root never travels with an app.
100
+ - An app sees only its own folder when it is built. What two apps share is data and logic,
101
+ and that lives in functions. Functions never go inside an app folder: the build refuses
102
+ `functions/` there (`functions-in-app`).
103
+ - `xflow apps add <folder>` creates an app, lays the template into `apps/<folder>` and prints
104
+ its address. Never create an app folder by hand: without its `appId` it is nobody's.
105
+ `--color <color>` sets the tile the user sees in the project; a wrong one is answered with
106
+ the list of colors. Its icon and a later color change belong to the person in the web
107
+ interface.
108
+ - Rename an app folder freely: the app is its `appId`.
109
+ - Commands follow the folder you stand in: inside `apps/zapis` they mean that app, anywhere
110
+ else in the project they mean the project. `--project <id>` and `--app <id>` name them
111
+ without a folder.
112
+ - Neither `xflow.json` is edited by hand except `functions.<name>.callers` in the root one.
113
+
114
+ ## Plan limits
115
+
116
+ The organization runs on a plan with limits: apps, active connections, people in the
117
+ organization, space shared by databases and files, function minutes, builds per month and at once, how often
118
+ schedules may run, and which access levels an app may have. The number of functions, schedules
119
+ and projects is not capped; at most 15 new projects a day (UTC) per organization, deleted ones count. Most are finite; a plan
120
+ may leave one uncapped, and then `whoami` prints `no limit` instead of a ceiling.
121
+ A paid plan comes in packages (Start S/M/L, Business M/L/XL): the package sets the space,
122
+ database GB-hours (1 GB of memory a database actually uses for an hour awake; a sleeping
123
+ one uses none) and function minutes, the plan sets the rest. More space is a bigger package
124
+ chosen by a person in «Тариф» (Plan); there is no separate purchase of space.
125
+ `xflow whoami` prints every one of them next to what is used, and the rights of your own key
126
+ with them: destroying data (a migration or `db exec`), deleting files from storage, moving a database and
127
+ linking a connected account each need a right. Reading it before a long task turns a refusal
128
+ halfway through the work into a sentence said before it starts.
129
+
130
+ - Connections are counted while active: switching an unused one off frees the seat without
131
+ deleting it.
132
+ - Function minutes are shared by the whole organization: every app and schedule spends
133
+ them. When they run out, every function of the organization stops (`functions_stopped`).
134
+ - An unpaid plan stops the functions at once, and five days later freezes the organization:
135
+ every command except `xflow whoami` answers `subscription_blocked`,
136
+ apps show a "suspended" page. Nothing is deleted and no code fixes it: tell the
137
+ person that the owner pays the invoice in the organization settings, section «Тариф» (Plan).
138
+ - The cap on how long one function call may run is written into the function when it is
139
+ deployed, so a plan change reaches the cloud only with the next `xflow deploy functions` of
140
+ each project, in both directions.
141
+ - The functions of one project take a plan-set rate: Free 5 calls a second (a burst of 60),
142
+ Start 20 (300), Business 50 (750). A call over the rate gets 429 at once. Calls at the same time:
143
+ Free 4, Start 10, Business 25, the rest wait in a queue. Fan out in batches of that size, never
144
+ all items of a list in parallel.
145
+
146
+ A limit refusal is not a bad request. The CLI prints a line starting with `Plan limit:`, the
147
+ API answers `code: "forbidden"` with a `limit` object (`code`, `used`, `limit`). Retrying, renaming or rewriting the code changes nothing: tell the user
148
+ what ran out and stop, only the owner lifts it. A schedule refused for running too often is one
149
+ of these too, not a bad cron string: the minimum interval comes from the plan, never guess it.
150
+
151
+ ## Deploying
152
+
153
+ 1. Change the code. In an app folder `npm run typecheck` is a two-second check, `npm run build`
154
+ a fast way to see build errors early: the platform builds again in its own sandbox.
155
+ 2. `xflow deploy functions -m "what changed"` sends the project root, checks it, applies the
156
+ new migrations, ships the functions and their callers. A migration that fails or would
157
+ destroy data stops it before any function ships: settle it with `xflow db migrate`
158
+ (`--allow-destructive`), then deploy again. `xflow db migrate` alone changes the schema
159
+ without shipping code. Every app runs the new code the moment it finishes: there is
160
+ no staging and no publish step for server code. Say so to the user.
161
+ 3. `xflow deploy apps/zapis -m "what changed"` sends and builds one app; inside `apps/zapis` a
162
+ plain `xflow deploy` does the same. In the root a plain `xflow deploy` refuses and names the
163
+ targets. The comment is required: one short line in the language the user speaks.
164
+ 4. `xflow publish` in the app folder (or with `--app <id>`) shows that build to visitors.
165
+
166
+ A change that touches data and screens goes in this order: the functions (with the migration),
167
+ then every app that shows it. `xflow deploy apps/...` warns when the root holds changes nobody
168
+ deployed.
169
+
170
+ - A build of an app is not shown to visitors until `xflow publish`. Publishing points the
171
+ address at a build and takes a second; `xflow publish <number>` (or `xflow rollback <number>`)
172
+ points it at an older kept build, and that is the rollback. The very first build of a new
173
+ app publishes itself.
174
+ - Only the app moves on publish and rollback. Functions come back with
175
+ `xflow deploy functions --revision N -m "why"` (`xflow deployments functions` lists the revisions);
176
+ `--project <id>` names the project without a folder.
177
+ - Files of the **three latest successful builds** of each app are kept, plus the live one;
178
+ `xflow deployments` marks the rest `no pages`. The history keeps 30 builds, a revision stays
179
+ while its build is in it or it is among the 10 latest. Keep anything you must not lose in
180
+ your own git repository.
181
+ - Give a person the app address the CLI printed, or the project page it printed. Name builds
182
+ by their number.
183
+ - `xflow status` says how the local copies differ from the server. A deploy rejected before
184
+ the build starts means the server holds a revision this folder has never seen: fetch it next
185
+ to yours (`xflow pull --into ./server-copy`), merge in git, deploy again. `--force` sends this
186
+ folder over it anyway; the overwritten revision is kept while it is among the last 10, and the
187
+ deploy prints the `xflow pull --revision <N>` line that brings it back. A folder with no record of the server revision (a fresh clone, a CI checkout) is
188
+ not rejected: its first deploy goes on top and says so.
189
+ - A deploy that would delete functions gone from the sources refuses until somebody agrees;
190
+ `--allow-removals` is the only way past it without a terminal. Reaching for it to get a
191
+ build running means you deleted something the user never asked you to: put the directories
192
+ back. If the removal is intended, name the functions and let the user answer.
193
+
194
+ ## Build gate
195
+
196
+ Before the sandbox starts, the platform checks the sources. Everything below blocks the build,
197
+ all violations come back together with files and lines, and a rejected attempt costs nothing.
198
+
199
+ An app:
200
+
201
+ - `package.json` with the build script named in its `xflow.json` (`npm run build` by default),
202
+ a `vite.config.*` and `vite` in dependencies. No server frameworks: `next`, `nuxt`, Remix,
203
+ `@sveltejs/kit`, `astro`.
204
+ - `index.html` in the app root with `id="root"` and the script that loads `src/main.tsx`
205
+ (`.ts`, `.jsx`, `.js` count too). Code under `src/`; root `app/`, `pages/`, `next/` are rejected.
206
+ - Keep `base` in `vite.config.js` as the template has it: the platform serves each build from
207
+ `/_b/<key>/`, and a build whose `index.html` points elsewhere is refused.
208
+ - The built app at most 15 MB unpacked, the sources at most 10 MB.
209
+ - File names in latin letters, digits, spaces and `. - _ ~` only: `логотип.png` breaks the build.
210
+ - Call functions through `xflow.functions.invoke`, never a hardcoded function address.
211
+ - No keys or tokens in the sources, no server modules (`fs`, `express`, `http`, `child_process`).
212
+ - Outside `src/components/ui` and `src/components/blocks`: no `alert()`, `confirm()`,
213
+ `prompt()` (use Dialog and Toast), no `console.log` (visitors read the console;
214
+ only crashes and 5xx reach the logs).
215
+ - A library that needs a provider (`@tanstack/react-query`, `react-redux`, `sonner`,
216
+ `react-hot-toast`, `react-dnd`) must have it mounted. A missing provider builds fine and
217
+ gives visitors a white screen.
218
+ - The platform files stay byte for byte as they arrived: `src/lib/platform-auth.ts`,
219
+ `src/contexts/platform-auth-context.tsx`, `src/hooks/use-platform-auth.ts`,
220
+ `src/utils/error-logger.ts`, `src/lib/xflow.ts`. The code keeps calling `initPlatformAuth()`
221
+ and `initErrorLogger()` and importing `index.css`, and the Tailwind
222
+ `content` globs keep covering `src/**`. A rejection names the revision to take the original
223
+ from: `xflow pull --revision N --into ./original`, then copy the file back.
224
+
225
+ The project root: function names are lowercase latin letters, digits and hyphens, up to 41
226
+ characters (`send-mail`, not `sendMail`); every name under `functions` in `xflow.json` has its
227
+ `functions/<name>/index.ts`; every caller is an app folder of this project (`unknown-caller`).
228
+
229
+ Passes with a line `Unfinished:` instead of blocking: the template's page title, no
230
+ `<link rel="icon">` in `index.html`, an `exports.access` left in a function (it does nothing).
231
+ Fix the first two before you tell the person the app is done.
232
+
233
+ ## The template is a starter kit
234
+
235
+ - `src/components/ui` and `src/components/blocks` are a starting set: change them, restyle them
236
+ or delete what you do not use. Any colors, your own color scheme, inline styles, a single
237
+ theme without dark mode: all allowed.
238
+ - The template looks like the xflow interface: warm grays, hairline controls (`ctl-solid`,
239
+ `ctl-raised`, `ctl-inset`, `ctl-select`), segments (`seg-track`, `seg-on`) and surfaces
240
+ (`surface-raised`, `surface-pop`, `surface-modal`) in `src/index.css`.
241
+ - While any template component is left, the theme token names in `:root` of `src/index.css`
242
+ and their `hsl(var(--name))` mapping in the Tailwind config stay: those components paint with
243
+ them. Values change freely; they are HSL components (`216 95% 40%`), not `#hex`. Your own
244
+ colors go under new names.
245
+ - Images and other assets: import them from `src/` (`import logo from './assets/logo.png'`)
246
+ so the build puts them next to the code. An absolute path into `public/` (`/logo.png`) goes
247
+ through the access check of the app and fails inside the xflow frame in Safari.
248
+
249
+ ## Who opens an app
250
+
251
+ A person sets this in the web interface; you cannot, and you should not ask for a way around.
252
+
253
+ | Level | Who gets in |
254
+ |---|---|
255
+ | Project members | people who have access to the project |
256
+ | Anyone with the link | anybody, no sign-in at all |
257
+
258
+ - The plan decides which levels exist: on Free an app opens only inside xflow, Start adds the
259
+ direct address and "Anyone with the link".
260
+ - At the closed level the platform signs people in, and a function receives the person in
261
+ `event.xflow.user` (`id`, `role` in the organization, `name`, `access` to the project).
262
+ Keep roles inside the app (doctor, receptionist) in your own table keyed by `user.id`.
263
+ - People outside the organization (patients, pickers, clients) are not xflow accounts: make the
264
+ app "Anyone with the link" and give it its own sign-in, see below.
265
+ - At "Anyone with the link" the platform signs nobody in: `event.xflow.user` is null for
266
+ visitors. Accounts, sign-in and who sees which rows are the code of your app, see below.
267
+ - `usePlatformAuth()` in the frontend knows the person only inside the xflow frame, and even
268
+ there it is a hint for drawing the interface, edited from the console by anyone. On the
269
+ direct address it answers `anonymous`. Guard data in the function, by `event.xflow`.
270
+
271
+ ## Which functions an app may call
272
+
273
+ The root `xflow.json` names the callers of each function:
274
+
275
+ {
276
+ "projectId": "…",
277
+ "functions": {
278
+ "list-slots": { "callers": ["zapis", "priem"] },
279
+ "book-slot": { "callers": ["zapis"] }
280
+ }
281
+ }
282
+
283
+ - A caller is an app folder of this project (`apps/zapis` is `"zapis"`). A function without
284
+ callers is left out of `functions` or given `"callers": []`, the same thing.
285
+ - A function nobody lists has no callers: no app reaches it. That is a workflow,
286
+ run by a schedule, a key or `xflow functions invoke`.
287
+ - Callers ship with `xflow deploy functions` and come back with its `--revision N`.
288
+ - The app calls `xflow.functions.invoke('book-slot', { body })` from `src/lib/xflow.ts`. The call
289
+ goes to `/_fn/book-slot` on the app's own address: no function addresses, tokens or keys in
290
+ the bundle, nothing to rebuild when a function changes.
291
+ - A refusal throws `XFlowError` with `status` and `code`: `not_bound` (404, add the caller and
292
+ deploy functions), `not_found` (404, the function is not deployed), `login_required` and
293
+ `invalid_credential` (401, the visitor has to sign in again), `no_access`, `role_too_low`,
294
+ `functions_stopped` (403, minutes ran out or the plan is unpaid: a person settles it),
295
+ `rate_limited` (429), `unavailable` (503, retry later). Branch on `code`, never on the message.
296
+
297
+ ## Your function is open to the whole internet
298
+
299
+ A function is open when an app at "Anyone with the link" is among its callers. `xflow deploy functions` says so in a line of its own, `xflow functions list`
300
+ prints `open`, the web interface marks it.
301
+
302
+ Open means anybody can call it with curl, without your app: the browser is not a gate.
303
+ Hiding the address, checking `Origin` or `Referer`, a token baked into the bundle: all of it is
304
+ readable or forgeable and protects nothing. Write every open function as if the request came
305
+ from a stranger, because it may:
306
+
307
+ - Check every input: types, lengths, allowed values; refuse fields you do not expect. Put input
308
+ into SQL as parameters only (`$1`), never into the text.
309
+ - Return only what this caller needs: no `select *` into a response, no rows of other people,
310
+ no internal fields, no stack traces.
311
+ - Make writes safe to repeat: take a request id from the client and put a unique index on it,
312
+ so a retry or a replay changes nothing.
313
+ - Deduplicate and limit per visitor where it costs: `event.xflow.ip` is the visitor's address,
314
+ set by the platform proxy and not by the caller. The proxy caps requests per address; the
315
+ organization pays for every minute a function runs.
316
+ - A form open to strangers needs a captcha: Cloudflare Turnstile, with the token checked in the
317
+ function against `https://challenges.cloudflare.com/turnstile/v0/siteverify` and the secret
318
+ from `xflow env set`. The site key is public and stays in the source. A check in the browser
319
+ checks nothing.
320
+ - `minRole` on an open function refuses every anonymous visitor (`login_required`): use it only
321
+ where members call.
322
+
323
+ ## Sign-in in a public app
324
+
325
+ At the closed levels do none of this: the platform already knows the person.
326
+
327
+ - Passwords: store only a hash, `crypto.scrypt` from Node with a random salt per user, compare
328
+ with `crypto.timingSafeEqual`. Never log a password, never return it, never keep it in the
329
+ browser.
330
+ - Session: after sign-in the function makes a random token (`crypto.randomBytes(32)`), stores
331
+ its SHA-256 and an expiry in a table and returns the token in the response body. The app keeps
332
+ it in `localStorage` and sends it on every call in the `X-Session` header. Not a cookie: Cloud
333
+ Functions drop `Cookie` and `Set-Cookie` both ways. `localStorage` belongs to one address, a
334
+ neighbouring `*.xflowpage.ru` site does not read it.
335
+ - Any script on the page reads that token, so: an expiry of hours that each call extends,
336
+ sign-out and a password change delete the session rows, no user text rendered as HTML
337
+ (`dangerouslySetInnerHTML`), no scripts loaded from outside the build.
338
+ - Never put the token into an address (`?session=`, a link): it stays in history and logs. A file
339
+ the function makes for this person is fetched with the header and opened as a `blob:` URL.
340
+ - Not the `Authorization` header: `/_fn` replaces it with the platform's own. Nor headers named
341
+ `X-Xflow-*`, `X-Project-Pass`, `X-Server-Key`, `X-Function-Key`: the platform strips them.
342
+ - Every function that serves a signed-in visitor reads `X-Session` from `event.headers` (match
343
+ the name in any case), finds the session and filters rows by that user in SQL. A hidden button
344
+ is not access control.
345
+ - Count failed sign-ins per email and per `event.xflow.ip` in a table and slow them down.
346
+ - Never in the frontend: keys, secrets, the list of users, hashes, a role check that is the only
347
+ guard.
348
+ - The platform sends no email for your app: confirmation and reset letters need a mail provider
349
+ whose key goes into `xflow env set`.
350
+ - `xflow dev` does not carry `X-Session` and calls functions as you (a member).
351
+ Try the sign-in on the app address.
352
+
353
+ ## A site for people outside the organization
354
+
355
+ - A landing page, a presentation, an invoice is an app at "Anyone with the link" (a paid plan):
356
+ built, published and rolled back like any app, opened by its address without signing in.
357
+ - Search engines are asked not to index apps: do not promise SEO.
358
+
359
+ ## Cloud functions
360
+
361
+ `functions/<name>/index.ts` in the project root exports `handler` (`index.js` and `index.mjs`
362
+ are read too), `export const handler` and `exports.handler` both work. The request arrives as
363
+ `event.httpMethod`, `event.headers`, `event.queryStringParameters` and `event.body`, a string
364
+ (`JSON.parse` it; `event.isBase64Encoded` marks a binary body). The handler returns
365
+ `{ statusCode, body }` where `body` is a JSON string, and may add `headers`.
366
+ `xflow functions list` shows status, callers, `open`, keys, last call and last error.
367
+
368
+ - Dependencies (`pg`, a mail client) go into the root `package.json`; the platform installs them
369
+ on every deploy, and a lock file next to it makes that exact.
370
+ - Code shared by functions lives in a root folder such as `lib/`, imported by relative path: each
371
+ function is bundled with what it imports. A folder in `functions/` without `index.*` is not a
372
+ function. Read `process.env` in `functions/<name>/` itself: `xflow env check` looks only there.
373
+
374
+ Keep one shape inside that string across the project: `{ success: true, data }` when it worked,
375
+ `{ success: false, error: { message, code } }` when it did not. Branch the frontend on
376
+ `error.code`, never on `error.message`.
377
+
378
+ A function whose code and variables did not change is not shipped again, and one that fails to
379
+ ship fails the whole deploy.
380
+
381
+ Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it as you,
382
+ through the platform, and prints the body alone, ready for a pipe (`--data '{"a":1}'`, `--data-file
383
+ body.json`, `--method GET`; 4xx and 5xx exit non-zero; Windows PowerShell 5.1 prepends a BOM when
384
+ piping to another program, so read it with `ConvertFrom-Json` or Python's `utf-8-sig`); `xflow functions logs <name>` shows
385
+ the crashes and 5xx answers with stacks and console output, the latest 10 (`--limit N` for more).
386
+ Only failed calls are logged. Empty output does not clear a function that fails at module start,
387
+ runs over its time cap or out of memory: those never reach the logs, `invoke` shows them.
388
+
389
+ The body has to be JSON and the CLI checks it before calling. In PowerShell the double quotes
390
+ never reach the CLI (`--data '{"a":1}'` arrives as `{a:1}`): escape them (`--data '{\"a\":1}'`)
391
+ or pass a file. It bites `--payload` of a schedule the same way. A command that worked writes
392
+ nothing to stderr, so a red line there means it really failed.
393
+
394
+ ### Who is calling
395
+
396
+ The platform checks every call before your code runs and hands the answer over in `event.xflow`:
397
+
398
+ ```js
399
+ exports.handler = async (event) => {
400
+ const { caller, slug, user, ip } = event.xflow
401
+ // caller: 'app' (an app of this project), 'service' (another function of this
402
+ // project), 'external' (an outside service with a key), 'schedule',
403
+ // 'platform' (a person through xflow functions invoke or the web interface)
404
+ // slug: the address name of the calling app (<id>--<org>), null otherwise
405
+ // user: { id, role, name, access } of a signed-in person, null for anonymous
406
+ // visitors and keys. access: 'view' | 'edit' on this project
407
+ // ip: the visitor's address, null for a schedule
408
+ }
409
+ ```
410
+
411
+ Never trust an identity that arrives in the body or in a header of the request: the page lives
412
+ on someone else's computer. `event.xflow` is the only identity the platform stands behind.
413
+
414
+ Project access is separate from the role: a person who only views the project (`access:
415
+ 'view'`) still reaches every function. A function that writes refuses them:
416
+
417
+ ```js
418
+ if (event.xflow.user && event.xflow.user.access !== 'edit') {
419
+ return { statusCode: 403, body: JSON.stringify({ error: 'Read-only access' }) }
420
+ }
421
+ ```
422
+
423
+ To remember who did something, store `user.id` and `user.name` in the row: the name is never
424
+ empty, and the app has no other way to look up other people.
425
+
426
+ A function that changes data should say who may call it instead of checking the role by hand:
427
+
428
+ ```js
429
+ exports.minRole = 'developer' // 'member' | 'developer' | 'owner'
430
+ ```
431
+
432
+ The platform refuses anything below that role before your code runs; it applies to people
433
+ (apps, `functions invoke`), not to keys and schedules. `member` is the people who only
434
+ use the apps (clinic employees, managers), `developer` and `owner` build them. In the web
435
+ interface these roles are called «Просмотр» (`member`), «Редактор» (`developer`) and «Админ»
436
+ (`owner`). Without the line
437
+ every member of the project can call the function. Losing access
438
+ closes the function within a minute.
439
+
440
+ - Another function of the project: `fetch(`${process.env.XFLOW_FN_URL}/<name>`, { headers:
441
+ { 'X-Server-Key': process.env.XFLOW_SERVER_KEY } })`. The key opens the functions of its own
442
+ project only, whatever their callers.
443
+ - An outside service (a payment webhook, a bot, a CRM) calls
444
+ `https://fn.xflowpage.ru/<projectId>/<name>` (`xflow functions list` prints it) with a key of
445
+ that function in `X-Function-Key` or `?key=`. Keys are issued by a person on the function's
446
+ screen in the web interface, never by you: ask the user. A function holds at most two keys,
447
+ the second one to replace the first without downtime.
448
+ - `exports.access` does nothing any more: who may call is `callers`.
449
+
450
+ ## Secrets and connected accounts
451
+
452
+ Keys and passwords live on the platform, not in the repository: `xflow env set SMTP_PASSWORD=…`
453
+ writes one, `xflow env` lists the names, `xflow env check` names the variables your functions
454
+ read but the platform does not have. Values never come back out: the only place they exist is
455
+ inside the running function. Apps get no variables at all.
456
+
457
+ A variable belongs to one project: functions of other projects never see it, and each project
458
+ keeps its own value under the same name.
459
+
460
+ `DATABASE_URL` and every name starting with `XFLOW` are filled in by the platform: writing them
461
+ is refused.
462
+
463
+ Some variables come from a connected account instead of from you. Credentials of an account
464
+ linked to the project show up in `xflow env` marked read-only, under a prefix chosen at link
465
+ time (`YANDEX_DIRECT_TOKEN`, `YANDEX_DIRECT_CLIENT_LOGIN`): read them like any other variable.
466
+ If calls to that service start failing on authorisation, `xflow status` says whether the token
467
+ is merely expiring (any deploy of the functions renews it) or the account was disconnected on
468
+ the provider's side, which only a human can reconnect.
469
+
470
+ `xflow connections` lists those accounts: the ones linked to this project, with the alias and
471
+ the state of the access, and the ones the organization has but this project does not use yet,
472
+ marked `available, not linked`. Check it before telling anyone a service is unavailable.
473
+
474
+ `xflow connections link "Яндекс Метрика" --as YANDEX_METRIKA` is that link and
475
+ `xflow connections unlink YANDEX_METRIKA` undoes it; `xflow help connections` has the naming
476
+ rules. In the project folder unlink refuses while a function still reads one of the variables,
477
+ naming those functions, so read that list before reaching for `--yes`.
478
+
479
+ Linking needs the `connections:link` right on the key, and only accounts granted to the owner
480
+ of the key personally can be linked at all. A key cannot grant it to itself: ask the person.
481
+ Connecting a new account and switching one off stay with a person too.
482
+
483
+ ### Asking an API what it returns
484
+
485
+ ```
486
+ xflow connections call WB /api/v1/supplier/stocks?dateFrom=2026-09-01
487
+ xflow connections call "Яндекс Метрика" /management/v1/counters
488
+ xflow connections call OZON /v1/product/list --method POST --data '{}' \
489
+ --header '{"Client-Id":"{CLIENT_ID}"}'
490
+ ```
491
+
492
+ The platform attaches the credentials and makes the request itself: no deploy, and the key never
493
+ reaches you. A linked account is called by its alias, an `available, not linked` one by its
494
+ name: linking first is not needed to look. Calling is looking, not wiring: to use the account
495
+ in code, link it and deploy functions.
496
+
497
+ `{NAME}` in the path or in a header value is filled in from the fields of the connection. Take
498
+ the names from `xflow connections`, which prints them in braces: an invented name is left in the
499
+ text and the request goes out anyway, into somebody's live account. Ask for the shape, not the
500
+ volume: one day and `limit=1`; `--json | jq '.response.body | keys'` gives the field names
501
+ without the body entering your context. `POST` and `DELETE` change things in a live account and
502
+ nothing undoes that: ask the person first when the endpoint is not clearly a read.
503
+
504
+ ## Schedules
505
+
506
+ `xflow schedules set report "0 3 ? * * *"` runs a function daily at 03:00. Six fields, UTC,
507
+ and exactly one of day-of-month / day-of-week must be `?`. A run reaches the handler as a POST
508
+ with `event.xflow.caller === 'schedule'`, and `--payload '{"mode":"full"}'` (or
509
+ `--payload-file body.json`) is its body. The function has to be deployed first.
510
+ A schedule is the function plus its cron: `set` with the same pair only changes the payload, a
511
+ new cron is one more schedule next to the old one. The plan caps the count for the whole
512
+ organization, so take the old one off with `xflow schedules rm report "<old cron>"`.
513
+
514
+ ```
515
+ xflow env set SMTP_PASSWORD=... # secrets first: values ride the next deploy
516
+ # write functions/report/index.ts, reading process.env.SMTP_PASSWORD literally
517
+ xflow deploy functions -m "Nightly report"
518
+ xflow schedules set report "0 3 ? * * *" --payload '{"mode":"full"}'
519
+ xflow functions invoke report # run it once
520
+ xflow functions logs report # crashes of real runs
521
+ ```
522
+
523
+ ## Database
524
+
525
+ Schema changes are files: `migrations/0001_init.sql`, `migrations/0002_orders.sql` in the
526
+ project root, applied in filename order by `xflow db migrate` from any folder of the project.
527
+ `xflow db status` shows what is applied and what waits. Never edit an applied file, write a new
528
+ migration instead. Every project has exactly one database of its own, shared by all its apps and
529
+ functions.
530
+
531
+ The browser never reaches the database directly. The app reads and writes through a cloud
532
+ function, and inside the handler the connection string is already there:
533
+
534
+ ```js
535
+ const { Client } = require('pg')
536
+ const db = new Client({ connectionString: process.env.DATABASE_URL })
537
+ ```
538
+
539
+ The platform passes `DATABASE_URL` only to functions that mention it and sets the project schema
540
+ on every connection, so plain table names (`select * from tasks`) hit your project.
541
+
542
+ `migrations/` says what you did; `xflow db schema [table]` says what is in the database now, and
543
+ `xflow db query 'select ...'` reads it inside a READ ONLY transaction (as many rows as its
544
+ own `limit` asks, 50 without one, 300 at most; `--json` prints long values whole). Single quotes hold in PowerShell, bash
545
+ and zsh, cmd.exe wants double ones; a query with quotes of its own goes in a `.sql` file outside
546
+ the project, `xflow db query ../q.sql`.
547
+
548
+ A one-off data fix is `xflow db exec 'update ...'` (or `../fix.sql`), not a migration: one
549
+ INSERT, UPDATE, DELETE or MERGE, saved at once, never re-run, no trace in the migration history.
550
+ Schema changes are refused there. Deleting rows, or updating more than 100 in a table (cascades
551
+ and triggers count), asks for `--allow-destructive`, and deleting also for the right below.
552
+ There is no undo: read the rows with `db query` first.
553
+
554
+ A branch is a database of its own, as the project database is now or was at a moment of its
555
+ history (6 hours back on Free, 3 days on Start, 7 days on Business), made in seconds without
556
+ copying: `xflow db branch create try-orders [--at 2h|14:05|<ISO date>]`. Try a migration or a fix
557
+ there first (`db migrate`, `db exec`, `db query` take `--branch <name>`); nothing reaches the
558
+ project database. After a data mistake, do not roll anything back: branch as of a moment before
559
+ it, check the rows with `db query --branch`, then `xflow db restore --from <branch> --table <t>
560
+ [--where '...']` puts the missing rows back by primary key and keeps all newer data
561
+ (`--overwrite` replaces rows still there and needs the destroy right). Branches per project: 2,
562
+ 5, 15 by plan; delete them when done (`xflow db branch delete <name>`). A branch is deleted on its
563
+ own when its lifetime ends: at most 1, 7 or 30 days by plan, shorter with `--ttl 12h`, more from
564
+ now with `xflow db branch extend <name>`. A branch sleeps after a quiet minute, has no functions,
565
+ and a branch of a branch is not made. People see the branches in Databases, open them there and
566
+ read their data.
567
+
568
+ Anything that destroys data
569
+ (`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE`) needs both the `--allow-destructive` flag and
570
+ the right to destroy data on the key, which is off by default and only its owner turns on. It is
571
+ judged by the result, not the text: a migration that dropped or emptied a table, dropped a column
572
+ or deleted rows is rolled back, however the SQL was written. `BEGIN`/`COMMIT` in a migration are
573
+ refused: each one already runs in its own transaction. When the refusal is about the right, the flag changes nothing: say what
574
+ needs deleting and why, and let the person decide. With both in place the affected tables are
575
+ dumped first (up to 50 000 rows each) and kept for 7 days; getting a dump back is a request to
576
+ the person, no command does it. Check with `--dry-run`.
577
+
578
+ Pass `DATABASE_URL` as is, without an `ssl` option. The database is PostgreSQL 18, so do not
579
+ tune it from migrations:
580
+ - Installed: `pg_trgm`. `gen_random_uuid()` and `uuidv7()` are built in. Never `uuid-ossp`.
581
+ Anything else (`postgis`, `pgvector`) is a request to the person.
582
+ - A statement is cut after 10 seconds (in a migration through `xflow db migrate`, after 2 minutes),
583
+ waiting for a lock after 10, a transaction left open after 60. Connections to the database of
584
+ one project: Free 10, Start 25, Business 50. A query that needs longer is a wrong query (a missing index, or a report that belongs in a
585
+ scheduled function writing a summary table).
586
+
587
+ Calls of one project run at the same time in one process. A `Client`, even one made at module
588
+ level, gets its own pooled connection for each call, so `connect()` twice is fine. Keep
589
+ per-request data out of module-level variables. Session state (`SET` without `LOCAL`, temp tables,
590
+ advisory locks) lasts until the call ends, and a transaction left open is rolled back: set what
591
+ the call needs inside the handler. A `Client` that listens for `notification` keeps its own
592
+ connection.
593
+
594
+ ## File storage
595
+
596
+ **Static assets of an app** (product photos, video, PDFs, big artwork) you upload yourself:
597
+
598
+ ```bash
599
+ xflow storage push ./media --to media # a folder, its structure kept
600
+ xflow storage ls # what is there, with addresses and sizes
601
+ ```
602
+
603
+ `push` prints the address of every file (`/_files/<id>`) and skips what is already stored under
604
+ the same path and size; `--replace` overwrites a file whose size differs and keeps its address.
605
+ Use the address as is inside an app: the app's own host serves it to whoever may open the app,
606
+ including anonymous visitors of a public one. Icons, fonts and small artwork stay in the
607
+ repository.
608
+
609
+ Deleting needs a right of its own, off by default, so `xflow storage rm` may answer that the key
610
+ was not granted it. Ask the person and do not look for a way around: whatever the users of the
611
+ app uploaded lives in the same folders, and there is no undo. A folder is
612
+ `xflow storage rm --folder <path> --yes`.
613
+
614
+ **Files that users of the app upload** go through a cloud function of your own: it asks the
615
+ platform for a one-time link, the browser sends the bytes straight to storage, and a second call
616
+ records the file:
617
+
618
+ ```js
619
+ const link = await fetch(`${process.env.XFLOW_API_URL}/api/storage/project/upload-url`, {
620
+ method: 'POST',
621
+ headers: {
622
+ 'Content-Type': 'application/json',
623
+ 'X-Server-Key': process.env.XFLOW_SERVER_KEY,
624
+ },
625
+ body: JSON.stringify({ fileName, fileSize, contentType, folderPath: 'invoices' }),
626
+ }).then((r) => r.json())
627
+ ```
628
+
629
+ `confirm` takes the returned `s3Key` and answers with the file and its address; `delete` takes
630
+ that same `url`. No endpoint lists files back, so write the address into a table of your own in
631
+ the same call. What a visitor may do with files is decided inside that function; never send the
632
+ key to the frontend.
633
+
634
+ - A name already taken in that folder is refused before the link is issued; `overwrite: true`
635
+ replaces the bytes and keeps the address.
636
+ - 200 MB per file and the organization's space (databases and files together), checked again
637
+ on `confirm`.
638
+ - Confirm only after the PUT has finished, and send on the PUT exactly the `Content-Type` you
639
+ named: the link is signed for it, a different or missing type gets 403.
640
+ - Assets only: pages, scripts and executables (`html`, `js`, `exe`, …) are refused with
641
+ `forbidden_type`.
642
+
643
+ A refusal comes back as `{ error, code }` (`invalid_name`, `file_too_large`, `quota_exceeded`,
644
+ `not_uploaded`, `duplicate_name`, `forbidden_type`, `not_found`, …): branch on `code`.
645
+
646
+ ## Organizations and keys
647
+
648
+ One key per organization, stored side by side: `xflow org` lists them, `xflow org switch
649
+ <name|id>` changes the active one, `xflow help org` has the precedence rules.
650
+
651
+ Inside a project folder commands follow the organization the folder is bound to, whatever the
652
+ active one is. `--project <id>` and `--app <id>` ignore the folder whole (`xflow help project`
653
+ lists the commands that take them). A "not found" on a project you know exists usually means the
654
+ key belongs to another organization: check `xflow org`; signing in to a missing organization is
655
+ `xflow login`, and that needs a person with a browser.
656
+
657
+ A key has its owner's level in each project. With view-only access reading works (`projects
658
+ get`, logs, `db query`, `pull`), and every change fails with `Your access to this project is
659
+ view-only`. That is not a bug to work around: tell the person, edit access is theirs to ask for.
660
+
661
+ ## Do not
662
+
663
+ - Deploy with `--force` without checking `xflow status` first.
664
+ - Invent platform commands: what is not in `xflow help` does not exist.
665
+ - Ask for a way to change an app's access level or issue a function key: that is the person's.
666
+
667
+ ## App design
668
+
669
+ - Take a block from `src/components/blocks` or a primitive from `src/components/ui` before
670
+ writing your own layout; replace or delete them when the design needs something else.
671
+ - Dropdowns, checkboxes, radios and dates through `Select`, `Checkbox`, `RadioGroup`,
672
+ `DatePicker` from `src/components/ui`: a bare `<select>` opens the system list in Safari
673
+ and Firefox, out of the app's style.
674
+ - Every screen starts with a clear header; header, filters and content in one column.
675
+ - Draw all four states: loading, error, empty, data. An empty screen says what appears here and
676
+ offers an action.
677
+ - Colour only when it communicates: status, action, error, success. Grey carries the structure.
678
+ - Density over air: people scan and compare. Numbers in a column, `tabular-nums`.
679
+ - Name the shape first: list, list with detail, dashboard, form, board.
680
+ - Seed 5-10 believable rows in the first migration: a screen with zero rows looks unfinished.
681
+ - Give the app its own page title and icon.
682
+
683
+ ## Errors from a deployed app
684
+
685
+ `xflow logs` prints what broke in the browser on the app addresses: unhandled errors, rejected
686
+ promises and 5xx responses; in an app folder that app, in the project root every app.
687
+ `xflow functions logs [name]` prints function crashes with the tail of their console, and failed
688
+ calls (timeout, out of memory, 5xx; no console for those); `--all` adds every call as its status
689
+ and time, `--cursor` (printed at the end) goes to older ones. A person sees the same in the web
690
+ interface: browser errors in the «Ошибки» (Errors) panel of the app, function crashes on the
691
+ «Ошибки» tab of the function's screen. `xflow db logs` prints what the database refused: failed
692
+ and timed-out queries with their SQL (cut to 2 KB), dropped connections. Kept for 7 days, 1 day on
693
+ the Free plan.
694
+
695
+ Run an app locally with `xflow dev` in its folder, not a bare `npm run dev`: every `/_fn/<name>`
696
+ call then goes to the platform as this app, with the same callers and access as on its address,
697
+ and a function someone else deployed answers at once. A bare `npm run dev` has no key, so its
698
+ function calls fail. The server answers `127.0.0.1` alone, because it holds that key. A local
699
+ run reports nothing to the logs.
700
+
701
+ ## Environment
702
+
703
+ For CI and agent runs: `XFLOW_TOKEN` replaces `xflow login`, `XFLOW_API_URL` points at another
704
+ platform host. There is no `.env` from the platform: the bundle carries no tokens or addresses.